Update labels and refine hooks for teams and billing modules
This commit modifies several language labels and refines hooks related to 'teams' and 'billing' modules for better clarity and consistency. It also includes the deletion of unused locale files and package dependencies transfered to 'peerDependencies'. Lastly, it introduces minor enhancements in server command, error logging functionality, and scripts to interact with Stripe.
This commit is contained in:
@@ -9,7 +9,7 @@ const features = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const paths = {
|
const paths = {
|
||||||
callback: pathsConfig.auth.callback,
|
callback: pathsConfig.auth.callback + `?next=${pathsConfig.app.accountHome}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PersonalAccountSettingsPage() {
|
function PersonalAccountSettingsPage() {
|
||||||
|
|||||||
@@ -22,10 +22,12 @@ export function SiteHeaderAccountSection(
|
|||||||
session: Session | null;
|
session: Session | null;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
|
if (!props.session) {
|
||||||
|
return <AuthButtons />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<AuthButtons />}>
|
<SuspendedPersonalAccountDropdown session={props.session} />
|
||||||
<SuspendedPersonalAccountDropdown session={props.session} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { SiteHeaderAccountSection } from '~/(marketing)/_components/site-header-
|
|||||||
import { SiteNavigation } from '~/(marketing)/_components/site-navigation';
|
import { SiteNavigation } from '~/(marketing)/_components/site-navigation';
|
||||||
import { AppLogo } from '~/components/app-logo';
|
import { AppLogo } from '~/components/app-logo';
|
||||||
|
|
||||||
export async function SiteHeader(props: { session?: Session | null }) {
|
export function SiteHeader(props: { session?: Session | null }) {
|
||||||
return (
|
return (
|
||||||
<div className={'container mx-auto'}>
|
<div className={'container mx-auto'}>
|
||||||
<div className="flex h-16 items-center justify-between">
|
<div className="flex h-16 items-center justify-between">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
import { GlobalLoader } from '@kit/ui/global-loader';
|
import { GlobalLoader } from '@kit/ui/global-loader';
|
||||||
|
|
||||||
export default GlobalLoader;
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
export default withI18n(GlobalLoader);
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ export default async function RootLayout({
|
|||||||
<html lang={lang} className={getClassName()}>
|
<html lang={lang} className={getClassName()}>
|
||||||
<body>
|
<body>
|
||||||
<RootProviders lang={lang}>{children}</RootProviders>
|
<RootProviders lang={lang}>{children}</RootProviders>
|
||||||
|
<Toaster richColors={false} />
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
<Toaster richColors={false} />
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,16 +21,23 @@
|
|||||||
"updatePasswordError": "Encountered an error. Please try again",
|
"updatePasswordError": "Encountered an error. Please try again",
|
||||||
"updatePasswordLoading": "Updating password...",
|
"updatePasswordLoading": "Updating password...",
|
||||||
"updateProfileLoading": "Updating profile...",
|
"updateProfileLoading": "Updating profile...",
|
||||||
"displayNameLabel": "Your Name",
|
"name": "Your Name",
|
||||||
|
"nameDescription": "Update your name to be displayed on your profile",
|
||||||
"emailLabel": "Email Address",
|
"emailLabel": "Email Address",
|
||||||
"profilePictureHeading": "Upload your avatar picture",
|
"accountImage": "Your Profile Picture",
|
||||||
"profilePictureSubheading": "Please choose a photo to upload as your profile picture.",
|
"accountImageDescription": "Please choose a photo to upload as your profile picture.",
|
||||||
|
"profilePictureHeading": "Upload a Profile Picture",
|
||||||
|
"profilePictureSubheading": "Choose a photo to upload as your profile picture.",
|
||||||
"updateProfileSubmitLabel": "Update Profile",
|
"updateProfileSubmitLabel": "Update Profile",
|
||||||
|
"updatePasswordCardTitle": "Update your Password",
|
||||||
|
"updatePasswordCardDescription": "Update your password to keep your account secure.",
|
||||||
"currentPassword": "Current Password",
|
"currentPassword": "Current Password",
|
||||||
"newPassword": "New Password",
|
"newPassword": "New Password",
|
||||||
"repeatPassword": "Repeat New Password",
|
"repeatPassword": "Repeat New Password",
|
||||||
"yourPassword": "Your Password",
|
"yourPassword": "Your Password",
|
||||||
"updatePasswordSubmitLabel": "Update Password",
|
"updatePasswordSubmitLabel": "Update Password",
|
||||||
|
"updateEmailCardTitle": "Update your Email",
|
||||||
|
"updateEmailCardDescription": "Update your email address you use to login to your account",
|
||||||
"newEmail": "Your New Email",
|
"newEmail": "Your New Email",
|
||||||
"repeatEmail": "Repeat Email",
|
"repeatEmail": "Repeat Email",
|
||||||
"updateEmailSubmitLabel": "Update Email Address",
|
"updateEmailSubmitLabel": "Update Email Address",
|
||||||
@@ -39,6 +46,7 @@
|
|||||||
"updateEmailLoading": "Updating your email...",
|
"updateEmailLoading": "Updating your email...",
|
||||||
"updateEmailError": "Email not updated. Please try again",
|
"updateEmailError": "Email not updated. Please try again",
|
||||||
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
|
"passwordNotMatching": "Passwords do not match. Make sure you're using the correct password",
|
||||||
|
"emailNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||||
"passwordNotChanged": "Your password has not changed",
|
"passwordNotChanged": "Your password has not changed",
|
||||||
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
|
"emailsNotMatching": "Emails do not match. Make sure you're using the correct email",
|
||||||
"updatingSameEmail": "The email chosen is the same as your current one",
|
"updatingSameEmail": "The email chosen is the same as your current one",
|
||||||
@@ -119,7 +127,7 @@
|
|||||||
"loadingUser": "Loading user details. Please wait...",
|
"loadingUser": "Loading user details. Please wait...",
|
||||||
"linkPhoneNumber": "Link Phone Number",
|
"linkPhoneNumber": "Link Phone Number",
|
||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerZoneSubheading": "Some actions cannot be undone. Please be careful.",
|
"dangerZoneDescription": "Some actions cannot be undone. Please be careful.",
|
||||||
"deleteAccount": "Delete your Account",
|
"deleteAccount": "Delete your Account",
|
||||||
"deleteAccountDescription": "This will delete your account and the organizations you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone. You will be asked to confirm this action in the next step.",
|
"deleteAccountDescription": "This will delete your account and the organizations you own. Furthermore, we will immediately cancel any active subscriptions. This action cannot be undone. You will be asked to confirm this action in the next step.",
|
||||||
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
|
"deleteProfileConfirmationInputLabel": "Type DELETE to confirm",
|
||||||
1
apps/web/public/locales/en/admin.json
Normal file
1
apps/web/public/locales/en/admin.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
1
apps/web/public/locales/en/marketing.json
Normal file
1
apps/web/public/locales/en/marketing.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"setupOrganization": "Setup Organization",
|
|
||||||
"setupOrganizationDescription": "Welcome! First, let's setup your organization.",
|
|
||||||
"organizationNameLabel": "Organization name",
|
|
||||||
"errorAlertHeading": "There was an error completing your onboarding.",
|
|
||||||
"inviteMembers": "Invite members",
|
|
||||||
"inviteMembersDescription": "Invite your team members to join your organization.",
|
|
||||||
"info": "Details",
|
|
||||||
"invites": "Invites",
|
|
||||||
"complete": "Complete",
|
|
||||||
"successStepHeading": "You're all set! You can now start using the app.",
|
|
||||||
"continue": "Continue to your Dashboard",
|
|
||||||
"settingAccount": "We're setting up your account. Please wait...",
|
|
||||||
"organizationNamePlaceholder": "Ex. Acme Inc."
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
{
|
|
||||||
"subscriptionTabSubheading": "Manage your Subscription and Billing",
|
|
||||||
"checkout": "Checkout",
|
|
||||||
"manageBillingDescription": "Visit your Customer Portal to manage your subscription and billing.",
|
|
||||||
"manageBilling": "Go to Customer Portal",
|
|
||||||
"notSubscribedToAnyPlan": "You are not currently subscribed to any plan",
|
|
||||||
"choosePlan": "Select a plan below",
|
|
||||||
"currentPlan": "Your Plan",
|
|
||||||
"subscriptionWillEndOn": "The subscription will end on {{- endDate }}",
|
|
||||||
"unknownErrorAlertHeading": "Sorry, something went wrong",
|
|
||||||
"unknownErrorAlert": "We encountered an unknown error while processing your payment. Please try again or contact support.",
|
|
||||||
"checkOutCanceledAlertHeading": "The checkout was canceled",
|
|
||||||
"checkOutCanceledAlert": "The checkout was canceled. Please contact us if you're experiencing any issues.",
|
|
||||||
"checkOutCompletedAlertHeading": "Checkout successfully completed",
|
|
||||||
"checkOutCompletedAlert": "Yay, your payment went through!",
|
|
||||||
"cancelAtPeriodEndDescription": "Your subscription is scheduled to be canceled on {{- endDate }}.",
|
|
||||||
"renewAtPeriodEndDescription": "Your subscription is scheduled to be renewed on {{- endDate }}",
|
|
||||||
"noPermissionsAlertHeading": "You don't have permissions to change the billing settings",
|
|
||||||
"noPermissionsAlertBody": "Please contact your organization owner to change the billing settings for your organization.",
|
|
||||||
"checkoutSuccessTitle": "Done! You're all set.",
|
|
||||||
"checkoutSuccessDescription": "Thank you for subscribing, we have successfully processed your subscription! A confirmation email will be sent to {{ customerEmail }}.",
|
|
||||||
"checkoutSuccessBackButton": "Proceed to App",
|
|
||||||
"status": {
|
|
||||||
"free": {
|
|
||||||
"label": "Free Plan",
|
|
||||||
"heading": "You are currently on the Free Plan",
|
|
||||||
"description": "You're on a free plan. You can upgrade to a paid plan at any time."
|
|
||||||
},
|
|
||||||
"active": {
|
|
||||||
"label": "Active",
|
|
||||||
"heading": "Your subscription is active",
|
|
||||||
"description": "Your subscription is active. You can manage your subscription and billing in the Customer Portal."
|
|
||||||
},
|
|
||||||
"trialing": {
|
|
||||||
"label": "Trial",
|
|
||||||
"heading": "You're on a trial",
|
|
||||||
"description": "Your trial will end on {{- trialEndDate }}."
|
|
||||||
},
|
|
||||||
"past_due": {
|
|
||||||
"label": "Past Due",
|
|
||||||
"heading": "Your invoice is past due",
|
|
||||||
"description": "Your invoice is past due. Please update your payment method."
|
|
||||||
},
|
|
||||||
"canceled": {
|
|
||||||
"label": "Canceled",
|
|
||||||
"heading": "Your subscription is canceled",
|
|
||||||
"description": "Your subscription is canceled. It is scheduled to end on {{- endDate }}."
|
|
||||||
},
|
|
||||||
"unpaid": {
|
|
||||||
"label": "Unpaid",
|
|
||||||
"heading": "Your invoice is unpaid",
|
|
||||||
"description": "Your invoice is unpaid. Please update your payment method."
|
|
||||||
},
|
|
||||||
"incomplete": {
|
|
||||||
"label": "Incomplete",
|
|
||||||
"heading": "We're waiting for your payment",
|
|
||||||
"description": "We're waiting for your payment to go through. Please bear with us."
|
|
||||||
},
|
|
||||||
"incomplete_expired": {
|
|
||||||
"label": "Expired",
|
|
||||||
"heading": "Your payment has expired",
|
|
||||||
"description": "Your payment has expired. Please update your payment method."
|
|
||||||
},
|
|
||||||
"paused": {
|
|
||||||
"label": "Paused",
|
|
||||||
"heading": "Your subscription is paused",
|
|
||||||
"description": "Your subscription is paused. You can resume it at any time."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -22,14 +22,14 @@
|
|||||||
"@kit/billing": "0.1.0",
|
"@kit/billing": "0.1.0",
|
||||||
"@kit/supabase": "^0.1.0",
|
"@kit/supabase": "^0.1.0",
|
||||||
"@kit/shared": "^0.1.0",
|
"@kit/shared": "^0.1.0",
|
||||||
"lucide-react": "^0.361.0"
|
"lucide-react": "^0.361.0",
|
||||||
|
"@supabase/supabase-js": "^2.39.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/prettier-config": "0.1.0",
|
"@kit/prettier-config": "0.1.0",
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "0.2.0",
|
||||||
"@kit/tailwind-config": "0.1.0",
|
"@kit/tailwind-config": "0.1.0",
|
||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0"
|
||||||
"@supabase/supabase-js": "^2.39.8"
|
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
@@ -45,4 +45,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,22 +10,27 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@kit/ui/card';
|
} from '@kit/ui/card';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export function BillingPortalCard() {
|
export function BillingPortalCard() {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Manage your Billing Details</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey="billing:billingPortalCardTitle" />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You can change your plan or cancel your subscription at any time.
|
<Trans i18nKey="billing:billingPortalCardDescription" />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className={'space-y-2'}>
|
<CardContent className={'space-y-2'}>
|
||||||
<div>
|
<div>
|
||||||
<Button>
|
<Button>
|
||||||
<span>Visit the billing portal</span>
|
<span>
|
||||||
|
<Trans i18nKey="billing:billingPortalCardButton" />
|
||||||
|
</span>
|
||||||
|
|
||||||
<ArrowUpRight className={'h-4'} />
|
<ArrowUpRight className={'h-4'} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function SuccessSessionStatus({
|
|||||||
|
|
||||||
<Heading level={3}>
|
<Heading level={3}>
|
||||||
<span className={'mr-4 font-semibold'}>
|
<span className={'mr-4 font-semibold'}>
|
||||||
<Trans i18nKey={'subscription:checkoutSuccessTitle'} />
|
<Trans i18nKey={'billing:checkoutSuccessTitle'} />
|
||||||
</span>
|
</span>
|
||||||
🎉
|
🎉
|
||||||
</Heading>
|
</Heading>
|
||||||
@@ -74,7 +74,7 @@ function SuccessSessionStatus({
|
|||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'subscription:checkoutSuccessDescription'}
|
i18nKey={'billing:checkoutSuccessDescription'}
|
||||||
values={{ customerEmail }}
|
values={{ customerEmail }}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
@@ -84,7 +84,7 @@ function SuccessSessionStatus({
|
|||||||
<Link href={redirectPath}>
|
<Link href={redirectPath}>
|
||||||
<span className={'flex items-center space-x-2.5'}>
|
<span className={'flex items-center space-x-2.5'}>
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'subscription:checkoutSuccessBackButton'} />
|
<Trans i18nKey={'billing:checkoutSuccessBackButton'} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<ChevronRight className={'h-4'} />
|
<ChevronRight className={'h-4'} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export function CurrentPlanAlert(
|
export function CurrentPlanAlert(
|
||||||
props: React.PropsWithoutRef<{
|
props: React.PropsWithoutRef<{
|
||||||
@@ -7,57 +8,47 @@ export function CurrentPlanAlert(
|
|||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
let variant: 'success' | 'warning' | 'destructive';
|
let variant: 'success' | 'warning' | 'destructive';
|
||||||
let text: string;
|
const prefix = 'billing:status';
|
||||||
let title: string;
|
|
||||||
|
const text = `${prefix}.${props.status}.description`;
|
||||||
|
const title = `${prefix}.${props.status}.heading`;
|
||||||
|
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
variant = 'success';
|
variant = 'success';
|
||||||
title = 'Active';
|
|
||||||
text = 'Your subscription is active';
|
|
||||||
break;
|
break;
|
||||||
case 'trialing':
|
case 'trialing':
|
||||||
variant = 'success';
|
variant = 'success';
|
||||||
title = 'Trial';
|
|
||||||
text = 'You are currently on a trial';
|
|
||||||
break;
|
break;
|
||||||
case 'past_due':
|
case 'past_due':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
title = 'Past Due';
|
|
||||||
text = 'Your subscription payment is past due';
|
|
||||||
break;
|
break;
|
||||||
case 'canceled':
|
case 'canceled':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
title = 'Canceled';
|
|
||||||
text = 'You have canceled your subscription';
|
|
||||||
break;
|
break;
|
||||||
case 'unpaid':
|
case 'unpaid':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
title = 'Unpaid';
|
|
||||||
text = 'Your subscription payment is unpaid';
|
|
||||||
break;
|
break;
|
||||||
case 'incomplete':
|
case 'incomplete':
|
||||||
variant = 'warning';
|
variant = 'warning';
|
||||||
title = 'Incomplete';
|
|
||||||
text = 'We are processing your subscription payment';
|
|
||||||
break;
|
break;
|
||||||
case 'incomplete_expired':
|
case 'incomplete_expired':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
title = 'Incomplete Expired';
|
|
||||||
text = 'Your subscription payment has expired';
|
|
||||||
break;
|
break;
|
||||||
case 'paused':
|
case 'paused':
|
||||||
variant = 'warning';
|
variant = 'warning';
|
||||||
title = 'Paused';
|
|
||||||
text = 'Your subscription is paused';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert variant={variant}>
|
<Alert variant={variant}>
|
||||||
<AlertTitle>{title}</AlertTitle>
|
<AlertTitle>
|
||||||
|
<Trans i18nKey={title} />
|
||||||
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>{text}</AlertDescription>
|
<AlertDescription>
|
||||||
|
<Trans i18nKey={text} />
|
||||||
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export function CurrentPlanBadge(
|
export function CurrentPlanBadge(
|
||||||
props: React.PropsWithoutRef<{
|
props: React.PropsWithoutRef<{
|
||||||
@@ -7,42 +8,38 @@ export function CurrentPlanBadge(
|
|||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
let variant: 'success' | 'warning' | 'destructive';
|
let variant: 'success' | 'warning' | 'destructive';
|
||||||
let text: string;
|
const text = `billing:status.${props.status}.badge`;
|
||||||
|
|
||||||
switch (props.status) {
|
switch (props.status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
variant = 'success';
|
variant = 'success';
|
||||||
text = 'Active';
|
|
||||||
break;
|
break;
|
||||||
case 'trialing':
|
case 'trialing':
|
||||||
variant = 'success';
|
variant = 'success';
|
||||||
text = 'Trialing';
|
|
||||||
break;
|
break;
|
||||||
case 'past_due':
|
case 'past_due':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
text = 'Past due';
|
|
||||||
break;
|
break;
|
||||||
case 'canceled':
|
case 'canceled':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
text = 'Canceled';
|
|
||||||
break;
|
break;
|
||||||
case 'unpaid':
|
case 'unpaid':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
text = 'Unpaid';
|
|
||||||
break;
|
break;
|
||||||
case 'incomplete':
|
case 'incomplete':
|
||||||
variant = 'warning';
|
variant = 'warning';
|
||||||
text = 'Incomplete';
|
|
||||||
break;
|
break;
|
||||||
case 'incomplete_expired':
|
case 'incomplete_expired':
|
||||||
variant = 'destructive';
|
variant = 'destructive';
|
||||||
text = 'Incomplete expired';
|
|
||||||
break;
|
break;
|
||||||
case 'paused':
|
case 'paused':
|
||||||
variant = 'warning';
|
variant = 'warning';
|
||||||
text = 'Paused';
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Badge variant={variant}>{text}</Badge>;
|
return (
|
||||||
|
<Badge variant={variant}>
|
||||||
|
<Trans i18nKey={text} />
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,12 @@ export function CurrentPlanCard({
|
|||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Your Plan</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey="billing:planCardTitle" />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
You can change your plan or cancel your subscription at any time.
|
<Trans i18nKey="billing:planCardDescription" />
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -53,13 +55,20 @@ export function CurrentPlanCard({
|
|||||||
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
|
's-6 fill-green-500 text-white dark:fill-white dark:text-black'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>{product.name}</span>
|
<span>{product.name}</span>
|
||||||
<CurrentPlanBadge status={subscription.status} />
|
<CurrentPlanBadge status={subscription.status} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'text-muted-foreground'}>
|
<div className={'text-muted-foreground'}>
|
||||||
Renews every {subscription.interval} at{' '}
|
<Trans
|
||||||
<span>{product.currency}</span> <span>{plan.price}</span>
|
i18nKey="billing:planRenewal"
|
||||||
|
values={{
|
||||||
|
interval: subscription.interval,
|
||||||
|
currency: product.currency,
|
||||||
|
price: plan.price,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -70,12 +79,16 @@ export function CurrentPlanCard({
|
|||||||
<div>
|
<div>
|
||||||
<Accordion type="single" collapsible>
|
<Accordion type="single" collapsible>
|
||||||
<AccordionItem value="features">
|
<AccordionItem value="features">
|
||||||
<AccordionTrigger>Plan details</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Trans i18nKey="billing:planDetails" />
|
||||||
|
</AccordionTrigger>
|
||||||
|
|
||||||
<AccordionContent className="space-y-2.5">
|
<AccordionContent className="space-y-2.5">
|
||||||
<If condition={subscription.status === 'trialing'}>
|
<If condition={subscription.status === 'trialing'}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium">Your trial ends on</span>
|
<span className="font-medium">
|
||||||
|
<Trans i18nKey="billing:trialEndsOn" />
|
||||||
|
</span>
|
||||||
|
|
||||||
<div className={'text-muted-foreground'}>
|
<div className={'text-muted-foreground'}>
|
||||||
<span>
|
<span>
|
||||||
@@ -118,12 +131,16 @@ export function CurrentPlanCard({
|
|||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<span className="font-medium">Features</span>
|
<span className="font-medium">Features</span>
|
||||||
|
|
||||||
<ul>
|
<ul className={'flex flex-col space-y-0.5'}>
|
||||||
{product.features.map((item) => {
|
{product.features.map((item) => {
|
||||||
return (
|
return (
|
||||||
<li key={item} className="flex items-center space-x-0.5">
|
<li
|
||||||
|
key={item}
|
||||||
|
className="flex items-center space-x-0.5"
|
||||||
|
>
|
||||||
<CheckCircle2 className="h-4 text-green-500" />
|
<CheckCircle2 className="h-4 text-green-500" />
|
||||||
<span>
|
|
||||||
|
<span className={'text-muted-foreground'}>
|
||||||
<Trans i18nKey={item} defaults={item} />
|
<Trans i18nKey={item} defaults={item} />
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ export function EmbeddedCheckout(
|
|||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
return (
|
return <LazyCheckout {...props} />;
|
||||||
<LazyCheckout onClose={props.onClose} checkoutToken={props.checkoutToken} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function LazyCheckout(
|
function LazyCheckout(
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '@kit/ui/dialog';
|
} from '@kit/ui/dialog';
|
||||||
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
import { ErrorBoundary } from '@kit/ui/error-boundary';
|
||||||
import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
|
import { Form, FormControl, FormItem, FormLabel } from '@kit/ui/form';
|
||||||
import { Heading } from '@kit/ui/heading';
|
|
||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
@@ -28,12 +27,12 @@ function DeleteAccountContainer() {
|
|||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<div className={'flex flex-col space-y-1'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<Heading level={6}>
|
<span className={'text-sm font-medium'}>
|
||||||
<Trans i18nKey={'profile:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</Heading>
|
</span>
|
||||||
|
|
||||||
<p className={'text-muted-foreground text-sm'}>
|
<p className={'text-muted-foreground text-sm'}>
|
||||||
<Trans i18nKey={'profile:deleteAccountDescription'} />
|
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -49,14 +48,14 @@ function DeleteAccountModal() {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
<Button data-test={'delete-account-button'} variant={'destructive'}>
|
||||||
<Trans i18nKey={'profile:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'profile:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ function DeleteAccountForm() {
|
|||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-2'}>
|
<div className={'flex flex-col space-y-2'}>
|
||||||
<div>
|
<div>
|
||||||
<Trans i18nKey={'profile:deleteAccountDescription'} />
|
<Trans i18nKey={'account:deleteAccountDescription'} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -94,7 +93,7 @@ function DeleteAccountForm() {
|
|||||||
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'profile:deleteProfileConfirmationInputLabel'} />
|
<Trans i18nKey={'account:deleteProfileConfirmationInputLabel'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -127,7 +126,7 @@ function DeleteAccountSubmitButton() {
|
|||||||
value={'delete'}
|
value={'delete'}
|
||||||
variant={'destructive'}
|
variant={'destructive'}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'profile:deleteAccount'} />
|
<Trans i18nKey={'account:deleteAccount'} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -138,7 +137,7 @@ function DeleteAccountErrorAlert() {
|
|||||||
<ExclamationTriangleIcon className={'h-4'} />
|
<ExclamationTriangleIcon className={'h-4'} />
|
||||||
|
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'profile:deleteAccountErrorHeading'} />
|
<Trans i18nKey={'account:deleteAccountErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@kit/ui/card';
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@kit/ui/card';
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { AccountDangerZone } from './account-danger-zone';
|
import { AccountDangerZone } from './account-danger-zone';
|
||||||
import { UpdateAccountDetailsFormContainer } from './update-account-details-form-container';
|
import { UpdateAccountDetailsFormContainer } from './update-account-details-form-container';
|
||||||
@@ -22,7 +29,13 @@ export function PersonalAccountSettingsContainer(
|
|||||||
<div className={'flex w-full flex-col space-y-8 pb-32'}>
|
<div className={'flex w-full flex-col space-y-8 pb-32'}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Your Profile Picture</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'account:accountImage'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'account:accountImageDescription'} />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -32,7 +45,13 @@ export function PersonalAccountSettingsContainer(
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Your Details</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'account:name'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'account:nameDescription'} />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -42,7 +61,13 @@ export function PersonalAccountSettingsContainer(
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Update your Email</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'account:updateEmailCardTitle'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'account:updateEmailCardDescription'} />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -52,7 +77,13 @@ export function PersonalAccountSettingsContainer(
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Update your Password</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'account:updatePasswordCardTitle'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'account:updatePasswordCardDescription'} />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
@@ -63,7 +94,13 @@ export function PersonalAccountSettingsContainer(
|
|||||||
<If condition={props.features.enableAccountDeletion}>
|
<If condition={props.features.enableAccountDeletion}>
|
||||||
<Card className={'border-destructive border-2'}>
|
<Card className={'border-destructive border-2'}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Danger Zone</CardTitle>
|
<CardTitle>
|
||||||
|
<Trans i18nKey={'account:dangerZone'} />
|
||||||
|
</CardTitle>
|
||||||
|
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey={'account:dangerZoneDescription'} />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ function MultiFactorAuthSetupModal(
|
|||||||
const onEnrollSuccess = useCallback(() => {
|
const onEnrollSuccess = useCallback(() => {
|
||||||
props.setIsOpen(false);
|
props.setIsOpen(false);
|
||||||
|
|
||||||
return toast.success(t(`profile:multiFactorSetupSuccess`));
|
return toast.success(t(`multiFactorSetupSuccess`));
|
||||||
}, [props, t]);
|
}, [props, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,7 +41,7 @@ function MultiFactorAuthSetupModal(
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'profile:setupMfaButtonLabel'} />
|
<Trans i18nKey={'account:setupMfaButtonLabel'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ function MultiFactorAuthSetupForm({
|
|||||||
return (
|
return (
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<Trans i18nKey={'profile:multiFactorSetupError'} />
|
<Trans i18nKey={'account:multiFactorSetupError'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -132,7 +132,7 @@ function MultiFactorAuthSetupForm({
|
|||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans i18nKey={'profile:verificationCode'} />
|
<Trans i18nKey={'account:verificationCode'} />
|
||||||
|
|
||||||
<OtpInput
|
<OtpInput
|
||||||
onInvalid={() => setVerificationCode('')}
|
onInvalid={() => setVerificationCode('')}
|
||||||
@@ -140,16 +140,16 @@ function MultiFactorAuthSetupForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'profile:verifyActivationCodeDescription'} />
|
<Trans i18nKey={'account:verifyActivationCodeDescription'} />
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2'}>
|
<div className={'flex justify-end space-x-2'}>
|
||||||
<Button disabled={!verificationCode} type={'submit'}>
|
<Button disabled={!verificationCode} type={'submit'}>
|
||||||
{state.loading ? (
|
{state.loading ? (
|
||||||
<Trans i18nKey={'profile:verifyingCode'} />
|
<Trans i18nKey={'account:verifyingCode'} />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey={'profile:enableMfaFactor'} />
|
<Trans i18nKey={'account:enableMfaFactor'} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +210,7 @@ function FactorQrCode({
|
|||||||
return (
|
return (
|
||||||
<div className={'flex w-full flex-col space-y-2'}>
|
<div className={'flex w-full flex-col space-y-2'}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<Trans i18nKey={'profile:qrCodeError'} />
|
<Trans i18nKey={'account:qrCodeError'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -231,7 +231,7 @@ function FactorQrCode({
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<p>
|
<p>
|
||||||
<span className={'text-base'}>
|
<span className={'text-base'}>
|
||||||
<Trans i18nKey={'profile:multiFactorModalHeading'} />
|
<Trans i18nKey={'account:multiFactorModalHeading'} />
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -264,18 +264,18 @@ function FactorNameForm(
|
|||||||
>
|
>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans i18nKey={'profile:factorNameLabel'} />
|
<Trans i18nKey={'account:factorNameLabel'} />
|
||||||
|
|
||||||
<Input autoComplete={'off'} required name={inputName} />
|
<Input autoComplete={'off'} required name={inputName} />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'profile:factorNameHint'} />
|
<Trans i18nKey={'account:factorNameHint'} />
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
<div className={'flex justify-end space-x-2'}>
|
<div className={'flex justify-end space-x-2'}>
|
||||||
<Button type={'submit'}>
|
<Button type={'submit'}>
|
||||||
<Trans i18nKey={'profile:factorNameSubmitLabel'} />
|
<Trans i18nKey={'account:factorNameSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
@@ -18,13 +17,10 @@ import { Input } from '@kit/ui/input';
|
|||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { useUpdateAccountData } from '../../hooks/use-update-account';
|
import { useUpdateAccountData } from '../../hooks/use-update-account';
|
||||||
|
import { AccountDetailsSchema } from '../../schema/account-details.schema';
|
||||||
|
|
||||||
type UpdateUserDataParams = Database['public']['Tables']['accounts']['Update'];
|
type UpdateUserDataParams = Database['public']['Tables']['accounts']['Update'];
|
||||||
|
|
||||||
const AccountInfoSchema = z.object({
|
|
||||||
displayName: z.string().min(2).max(100),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function UpdateAccountDetailsForm({
|
export function UpdateAccountDetailsForm({
|
||||||
displayName,
|
displayName,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
@@ -35,10 +31,10 @@ export function UpdateAccountDetailsForm({
|
|||||||
onUpdate: (user: Partial<UpdateUserDataParams>) => void;
|
onUpdate: (user: Partial<UpdateUserDataParams>) => void;
|
||||||
}) {
|
}) {
|
||||||
const updateAccountMutation = useUpdateAccountData(userId);
|
const updateAccountMutation = useUpdateAccountData(userId);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation('account');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(AccountInfoSchema),
|
resolver: zodResolver(AccountDetailsSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
displayName,
|
displayName,
|
||||||
},
|
},
|
||||||
@@ -51,10 +47,10 @@ export function UpdateAccountDetailsForm({
|
|||||||
onUpdate(data);
|
onUpdate(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
return toast.promise(promise, {
|
return toast.promise(() => promise, {
|
||||||
success: t(`profile:updateProfileSuccess`),
|
success: t(`updateProfileSuccess`),
|
||||||
error: t(`profile:updateProfileError`),
|
error: t(`updateProfileError`),
|
||||||
loading: t(`profile:updateProfileLoading`),
|
loading: t(`:pdateProfileLoading`),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,7 +68,7 @@ export function UpdateAccountDetailsForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'profile:displayNameLabel'} />
|
<Trans i18nKey={'account:name'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -92,7 +88,7 @@ export function UpdateAccountDetailsForm({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={updateAccountMutation.isPending}>
|
<Button disabled={updateAccountMutation.isPending}>
|
||||||
<Trans i18nKey={'profile:updateProfileSubmitLabel'} />
|
<Trans i18nKey={'account:updateProfileSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
import ImageUploader from '@kit/ui/image-uploader';
|
import { ImageUploader } from '@kit/ui/image-uploader';
|
||||||
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
import { LoadingOverlay } from '@kit/ui/loading-overlay';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
@@ -42,10 +42,10 @@ function UploadProfileAvatarForm(props: {
|
|||||||
onAvatarUpdated: () => void;
|
onAvatarUpdated: () => void;
|
||||||
}) {
|
}) {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
const { t } = useTranslation('profile');
|
const { t } = useTranslation('account');
|
||||||
|
|
||||||
const createToaster = useCallback(
|
const createToaster = useCallback(
|
||||||
(promise: Promise<unknown>) => {
|
(promise: () => Promise<unknown>) => {
|
||||||
return toast.promise(promise, {
|
return toast.promise(promise, {
|
||||||
success: t(`updateProfileSuccess`),
|
success: t(`updateProfileSuccess`),
|
||||||
error: t(`updateProfileError`),
|
error: t(`updateProfileError`),
|
||||||
@@ -68,37 +68,39 @@ function UploadProfileAvatarForm(props: {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const promise = removeExistingStorageFile().then(() =>
|
const promise = () =>
|
||||||
uploadUserProfilePhoto(client, file, props.userId)
|
removeExistingStorageFile().then(() =>
|
||||||
.then((pictureUrl) => {
|
uploadUserProfilePhoto(client, file, props.userId)
|
||||||
|
.then((pictureUrl) => {
|
||||||
|
return client
|
||||||
|
.from('accounts')
|
||||||
|
.update({
|
||||||
|
picture_url: pictureUrl,
|
||||||
|
})
|
||||||
|
.eq('id', props.userId)
|
||||||
|
.throwOnError();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
props.onAvatarUpdated();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
createToaster(promise);
|
||||||
|
} else {
|
||||||
|
const promise = () =>
|
||||||
|
removeExistingStorageFile()
|
||||||
|
.then(() => {
|
||||||
return client
|
return client
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.update({
|
.update({
|
||||||
picture_url: pictureUrl,
|
picture_url: null,
|
||||||
})
|
})
|
||||||
.eq('id', props.userId)
|
.eq('id', props.userId)
|
||||||
.throwOnError();
|
.throwOnError();
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
props.onAvatarUpdated();
|
props.onAvatarUpdated();
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
|
||||||
createToaster(promise);
|
|
||||||
} else {
|
|
||||||
const promise = removeExistingStorageFile()
|
|
||||||
.then(() => {
|
|
||||||
return client
|
|
||||||
.from('accounts')
|
|
||||||
.update({
|
|
||||||
picture_url: null,
|
|
||||||
})
|
|
||||||
.eq('id', props.userId)
|
|
||||||
.throwOnError();
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
props.onAvatarUpdated();
|
|
||||||
});
|
|
||||||
|
|
||||||
createToaster(promise);
|
createToaster(promise);
|
||||||
}
|
}
|
||||||
@@ -110,11 +112,11 @@ function UploadProfileAvatarForm(props: {
|
|||||||
<ImageUploader value={props.pictureUrl} onValueChange={onValueChange}>
|
<ImageUploader value={props.pictureUrl} onValueChange={onValueChange}>
|
||||||
<div className={'flex flex-col space-y-1'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<span className={'text-sm'}>
|
<span className={'text-sm'}>
|
||||||
<Trans i18nKey={'profile:profilePictureHeading'} />
|
<Trans i18nKey={'account:profilePictureHeading'} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span className={'text-xs'}>
|
<span className={'text-xs'}>
|
||||||
<Trans i18nKey={'profile:profilePictureSubheading'} />
|
<Trans i18nKey={'account:profilePictureSubheading'} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</ImageUploader>
|
</ImageUploader>
|
||||||
@@ -142,9 +144,7 @@ async function uploadUserProfilePhoto(
|
|||||||
const extension = photoFile.name.split('.').pop();
|
const extension = photoFile.name.split('.').pop();
|
||||||
const fileName = await getAvatarFileName(userId, extension);
|
const fileName = await getAvatarFileName(userId, extension);
|
||||||
|
|
||||||
const result = await bucket.upload(fileName, bytes, {
|
const result = await bucket.upload(fileName, bytes);
|
||||||
upsert: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result.error) {
|
if (!result.error) {
|
||||||
return bucket.getPublicUrl(fileName).data.publicUrl;
|
return bucket.getPublicUrl(fileName).data.publicUrl;
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import type { User } from '@supabase/gotrue-js';
|
import type { User } from '@supabase/gotrue-js';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
@@ -25,32 +22,13 @@ import { If } from '@kit/ui/if';
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
const UpdateEmailSchema = z
|
import { UpdateEmailSchema } from '../../schema/update-email.schema';
|
||||||
.object({
|
|
||||||
email: z.string().email(),
|
|
||||||
repeatEmail: z.string().email(),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(values) => {
|
|
||||||
return values.email === values.repeatEmail;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ['repeatEmail'],
|
|
||||||
message: 'Emails do not match',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
function createEmailResolver(currentEmail: string) {
|
function createEmailResolver(currentEmail: string, errorMessage: string) {
|
||||||
return zodResolver(
|
return zodResolver(
|
||||||
UpdateEmailSchema.refine(
|
UpdateEmailSchema.withTranslation(errorMessage).refine((schema) => {
|
||||||
(values) => {
|
return schema.email !== currentEmail;
|
||||||
return values.email !== currentEmail;
|
}),
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ['email'],
|
|
||||||
message: 'New email must be different from current email',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,31 +39,31 @@ export function UpdateEmailForm({
|
|||||||
user: User;
|
user: User;
|
||||||
callbackPath: string;
|
callbackPath: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation('account');
|
||||||
const updateUserMutation = useUpdateUser();
|
const updateUserMutation = useUpdateUser();
|
||||||
|
|
||||||
const updateEmail = useCallback(
|
const updateEmail = ({ email }: { email: string }) => {
|
||||||
(email: string) => {
|
// then, we update the user's email address
|
||||||
const redirectTo = new URL(callbackPath, window.location.host).toString();
|
const promise = async () => {
|
||||||
|
const redirectTo = new URL(
|
||||||
|
callbackPath,
|
||||||
|
window.location.origin,
|
||||||
|
).toString();
|
||||||
|
|
||||||
// then, we update the user's email address
|
await updateUserMutation.mutateAsync({ email, redirectTo });
|
||||||
const promise = updateUserMutation.mutateAsync({ email, redirectTo });
|
};
|
||||||
|
|
||||||
return toast.promise(promise, {
|
toast.promise(promise, {
|
||||||
success: t(`profile:updateEmailSuccess`),
|
success: t(`updateEmailSuccess`),
|
||||||
loading: t(`profile:updateEmailLoading`),
|
loading: t(`updateEmailLoading`),
|
||||||
error: (error: Error) => {
|
error: t(`updateEmailError`),
|
||||||
return error.message ?? t(`profile:updateEmailError`);
|
});
|
||||||
},
|
};
|
||||||
});
|
|
||||||
},
|
|
||||||
[callbackPath, t, updateUserMutation],
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentEmail = user.email;
|
const currentEmail = user.email;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: createEmailResolver(currentEmail!),
|
resolver: createEmailResolver(currentEmail!, t('emailNotMatching')),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: '',
|
email: '',
|
||||||
repeatEmail: '',
|
repeatEmail: '',
|
||||||
@@ -97,18 +75,16 @@ export function UpdateEmailForm({
|
|||||||
<form
|
<form
|
||||||
className={'flex flex-col space-y-4'}
|
className={'flex flex-col space-y-4'}
|
||||||
data-test={'update-email-form'}
|
data-test={'update-email-form'}
|
||||||
onSubmit={form.handleSubmit((values) => {
|
onSubmit={form.handleSubmit(updateEmail)}
|
||||||
return updateEmail(values.email);
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<If condition={updateUserMutation.data}>
|
<If condition={updateUserMutation.data}>
|
||||||
<Alert variant={'success'}>
|
<Alert variant={'success'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'profile:updateEmailSuccess'} />
|
<Trans i18nKey={'account:updateEmailSuccess'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:updateEmailSuccessMessage'} />
|
<Trans i18nKey={'account:updateEmailSuccessMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -118,7 +94,7 @@ export function UpdateEmailForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'profile:newEmail'} />
|
<Trans i18nKey={'account:newEmail'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -141,7 +117,7 @@ export function UpdateEmailForm({
|
|||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'profile:repeatEmail'} />
|
<Trans i18nKey={'account:repeatEmail'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -160,8 +136,8 @@ export function UpdateEmailForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button>
|
<Button disabled={updateUserMutation.isPending}>
|
||||||
<Trans i18nKey={'profile:updateEmailSubmitLabel'} />
|
<Trans i18nKey={'account:updateEmailSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ export function UpdatePasswordFormContainer(
|
|||||||
return <LoadingOverlay fullPage={false} />;
|
return <LoadingOverlay fullPage={false} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const canUpdatePassword = user.identities?.some(
|
const canUpdatePassword = user.identities?.some(
|
||||||
(item) => item.provider === `email`,
|
(item) => item.provider === `email`,
|
||||||
);
|
);
|
||||||
@@ -32,7 +36,7 @@ export function UpdatePasswordFormContainer(
|
|||||||
function WarnCannotUpdatePasswordAlert() {
|
function WarnCannotUpdatePasswordAlert() {
|
||||||
return (
|
return (
|
||||||
<Alert variant={'warning'}>
|
<Alert variant={'warning'}>
|
||||||
<Trans i18nKey={'profile:cannotUpdatePassword'} />
|
<Trans i18nKey={'account:cannotUpdatePassword'} />
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import type { User } from '@supabase/gotrue-js';
|
import type { User } from '@supabase/gotrue-js';
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ import { zodResolver } from '@hookform/resolvers/zod';
|
|||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
import { useUpdateUser } from '@kit/supabase/hooks/use-update-user-mutation';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
@@ -26,21 +25,7 @@ import { Input } from '@kit/ui/input';
|
|||||||
import { Label } from '@kit/ui/label';
|
import { Label } from '@kit/ui/label';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
const PasswordUpdateSchema = z
|
import { PasswordUpdateSchema } from '../../schema/update-password.schema';
|
||||||
.object({
|
|
||||||
currentPassword: z.string().min(8).max(99),
|
|
||||||
newPassword: z.string().min(8).max(99),
|
|
||||||
repeatPassword: z.string().min(8).max(99),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(values) => {
|
|
||||||
return values.newPassword === values.repeatPassword;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ['repeatPassword'],
|
|
||||||
message: 'Passwords do not match',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const UpdatePasswordForm = ({
|
export const UpdatePasswordForm = ({
|
||||||
user,
|
user,
|
||||||
@@ -49,57 +34,58 @@ export const UpdatePasswordForm = ({
|
|||||||
user: User;
|
user: User;
|
||||||
callbackPath: string;
|
callbackPath: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation('account');
|
||||||
const updateUserMutation = useUpdateUser();
|
const updateUserMutation = useUpdateUser();
|
||||||
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
const [needsReauthentication, setNeedsReauthentication] = useState(false);
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(PasswordUpdateSchema),
|
resolver: zodResolver(
|
||||||
|
PasswordUpdateSchema.withTranslation(t('passwordNotMatching')),
|
||||||
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
currentPassword: '',
|
|
||||||
newPassword: '',
|
newPassword: '',
|
||||||
repeatPassword: '',
|
repeatPassword: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const updatePasswordFromCredential = useCallback(
|
const updatePasswordFromCredential = (password: string) => {
|
||||||
(password: string) => {
|
const redirectTo = [window.location.origin, callbackPath].join('');
|
||||||
const redirectTo = [window.location.origin, callbackPath].join('');
|
|
||||||
|
|
||||||
const promise = updateUserMutation
|
const promise = updateUserMutation
|
||||||
.mutateAsync({ password, redirectTo })
|
.mutateAsync({ password, redirectTo })
|
||||||
.then(() => {
|
.catch((error) => {
|
||||||
form.reset();
|
if (
|
||||||
})
|
typeof error === 'string' &&
|
||||||
.catch((error) => {
|
error?.includes('Password update requires reauthentication')
|
||||||
if (error?.includes('Password update requires reauthentication')) {
|
) {
|
||||||
setNeedsReauthentication(true);
|
setNeedsReauthentication(true);
|
||||||
}
|
} else {
|
||||||
});
|
throw error;
|
||||||
|
}
|
||||||
toast.promise(promise, {
|
|
||||||
success: t(`profile:updatePasswordSuccess`),
|
|
||||||
error: t(`profile:updatePasswordError`),
|
|
||||||
loading: t(`profile:updatePasswordLoading`),
|
|
||||||
});
|
});
|
||||||
},
|
|
||||||
[callbackPath, updateUserMutation, t, form],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatePasswordCallback = useCallback(
|
toast.promise(() => promise, {
|
||||||
async ({ newPassword }: { newPassword: string }) => {
|
success: t(`updatePasswordSuccess`),
|
||||||
const email = user.email;
|
error: t(`updatePasswordError`),
|
||||||
|
loading: t(`updatePasswordLoading`),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// if the user does not have an email assigned, it's possible they
|
const updatePasswordCallback = async ({
|
||||||
// don't have an email/password factor linked, and the UI is out of sync
|
newPassword,
|
||||||
if (!email) {
|
}: {
|
||||||
return Promise.reject(t(`profile:cannotUpdatePassword`));
|
newPassword: string;
|
||||||
}
|
}) => {
|
||||||
|
const email = user.email;
|
||||||
|
|
||||||
updatePasswordFromCredential(newPassword);
|
// if the user does not have an email assigned, it's possible they
|
||||||
},
|
// don't have an email/password factor linked, and the UI is out of sync
|
||||||
[user.email, updatePasswordFromCredential, t],
|
if (!email) {
|
||||||
);
|
return Promise.reject(t(`cannotUpdatePassword`));
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePasswordFromCredential(newPassword);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -111,11 +97,11 @@ export const UpdatePasswordForm = ({
|
|||||||
<If condition={updateUserMutation.data}>
|
<If condition={updateUserMutation.data}>
|
||||||
<Alert variant={'success'}>
|
<Alert variant={'success'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'profile:updatePasswordSuccess'} />
|
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:updatePasswordSuccessMessage'} />
|
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -123,11 +109,11 @@ export const UpdatePasswordForm = ({
|
|||||||
<If condition={needsReauthentication}>
|
<If condition={needsReauthentication}>
|
||||||
<Alert variant={'warning'}>
|
<Alert variant={'warning'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'profile:needsReauthentication'} />
|
<Trans i18nKey={'account:needsReauthentication'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:needsReauthenticationDescription'} />
|
<Trans i18nKey={'account:needsReauthenticationDescription'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -139,7 +125,7 @@ export const UpdatePasswordForm = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans i18nKey={'profile:newPassword'} />
|
<Trans i18nKey={'account:newPassword'} />
|
||||||
</Label>
|
</Label>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
@@ -165,7 +151,7 @@ export const UpdatePasswordForm = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Label>
|
<Label>
|
||||||
<Trans i18nKey={'profile:repeatPassword'} />
|
<Trans i18nKey={'account:repeatPassword'} />
|
||||||
</Label>
|
</Label>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
@@ -185,8 +171,8 @@ export const UpdatePasswordForm = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button>
|
<Button disabled={updateUserMutation.isPending}>
|
||||||
<Trans i18nKey={'profile:updatePasswordSubmitLabel'} />
|
<Trans i18nKey={'account:updatePasswordSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const AccountDetailsSchema = z.object({
|
||||||
|
displayName: z.string().min(2).max(100),
|
||||||
|
});
|
||||||
20
packages/features/accounts/src/schema/update-email.schema.ts
Normal file
20
packages/features/accounts/src/schema/update-email.schema.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const UpdateEmailSchema = {
|
||||||
|
withTranslation: (errorMessage: string) => {
|
||||||
|
return z
|
||||||
|
.object({
|
||||||
|
email: z.string().email(),
|
||||||
|
repeatEmail: z.string().email(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(values) => {
|
||||||
|
return values.email === values.repeatEmail;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ['repeatEmail'],
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const PasswordUpdateSchema = {
|
||||||
|
withTranslation: (errorMessage: string) => {
|
||||||
|
return z
|
||||||
|
.object({
|
||||||
|
newPassword: z.string().min(8).max(99),
|
||||||
|
repeatPassword: z.string().min(8).max(99),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(values) => {
|
||||||
|
return values.newPassword === values.repeatPassword;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ['repeatPassword'],
|
||||||
|
message: errorMessage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -80,9 +80,9 @@ function VerifyOtpForm({
|
|||||||
|
|
||||||
<Button disabled={verifyOtpMutation.isPending || !verifyCode}>
|
<Button disabled={verifyOtpMutation.isPending || !verifyCode}>
|
||||||
{verifyOtpMutation.isPending ? (
|
{verifyOtpMutation.isPending ? (
|
||||||
<Trans i18nKey={'profile:verifyingCode'} />
|
<Trans i18nKey={'account:verifyingCode'} />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey={'profile:submitVerificationCode'} />
|
<Trans i18nKey={'account:submitVerificationCode'} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function MultiFactorChallengeContainer({
|
|||||||
<form onSubmit={onSubmitClicked}>
|
<form onSubmit={onSubmitClicked}>
|
||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<span className={'text-sm'}>
|
<span className={'text-sm'}>
|
||||||
<Trans i18nKey={'profile:verifyActivationCodeDescription'} />
|
<Trans i18nKey={'account:verifyActivationCodeDescription'} />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className={'flex w-full flex-col space-y-2.5'}>
|
<div className={'flex w-full flex-col space-y-2.5'}>
|
||||||
@@ -67,7 +67,7 @@ export function MultiFactorChallengeContainer({
|
|||||||
<If condition={verifyMFAChallenge.error}>
|
<If condition={verifyMFAChallenge.error}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:invalidVerificationCode'} />
|
<Trans i18nKey={'account:invalidVerificationCode'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</If>
|
</If>
|
||||||
@@ -75,9 +75,9 @@ export function MultiFactorChallengeContainer({
|
|||||||
|
|
||||||
<Button disabled={verifyMFAChallenge.isPending || !verifyCode}>
|
<Button disabled={verifyMFAChallenge.isPending || !verifyCode}>
|
||||||
{verifyMFAChallenge.isPending ? (
|
{verifyMFAChallenge.isPending ? (
|
||||||
<Trans i18nKey={'profile:verifyingCode'} />
|
<Trans i18nKey={'account:verifyingCode'} />
|
||||||
) : (
|
) : (
|
||||||
<Trans i18nKey={'profile:submitVerificationCode'} />
|
<Trans i18nKey={'account:submitVerificationCode'} />
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +154,7 @@ function FactorsListContainer({
|
|||||||
<Spinner />
|
<Spinner />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Trans i18nKey={'profile:loadingFactors'} />
|
<Trans i18nKey={'account:loadingFactors'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -165,7 +165,7 @@ function FactorsListContainer({
|
|||||||
<div className={'w-full'}>
|
<div className={'w-full'}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:factorsListError'} />
|
<Trans i18nKey={'account:factorsListError'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,7 +178,7 @@ function FactorsListContainer({
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<div>
|
<div>
|
||||||
<Heading level={6}>
|
<Heading level={6}>
|
||||||
<Trans i18nKey={'profile:selectFactor'} />
|
<Trans i18nKey={'account:selectFactor'} />
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ function SuccessState() {
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'profile:updatePasswordSuccess'} />
|
<Trans i18nKey={'account:updatePasswordSuccess'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'profile:updatePasswordSuccessMessage'} />
|
<Trans i18nKey={'account:updatePasswordSuccessMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ export function SignInMethodsContainer(props: {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const nextPath = useSearchParams().get('next') ?? props.paths.home;
|
const nextPath = useSearchParams().get('next') ?? props.paths.home;
|
||||||
|
|
||||||
const redirectUrl = new URL(
|
const redirectUrl = isBrowser()
|
||||||
props.paths.callback,
|
? new URL(props.paths.callback, window?.location.origin).toString()
|
||||||
isBrowser() ? window?.location.origin : '',
|
: '';
|
||||||
).toString();
|
|
||||||
|
|
||||||
const onSignIn = () => {
|
const onSignIn = () => {
|
||||||
router.replace(nextPath);
|
router.replace(nextPath);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function CreateTeamAccountDialog(
|
|||||||
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
<Dialog open={props.isOpen} onOpenChange={props.setIsOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'organization:createOrganizationModalHeading'} />
|
<Trans i18nKey={'teams:createOrganizationModalHeading'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<CreateOrganizationAccountForm />
|
<CreateOrganizationAccountForm />
|
||||||
@@ -77,7 +77,7 @@ function CreateOrganizationAccountForm() {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'organization:organizationNameLabel'} />
|
<Trans i18nKey={'teams:organizationNameLabel'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -105,7 +105,7 @@ function CreateOrganizationAccountForm() {
|
|||||||
data-test={'confirm-create-organization-button'}
|
data-test={'confirm-create-organization-button'}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:createOrganizationSubmitLabel'} />
|
<Trans i18nKey={'teams:createOrganizationSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -117,11 +117,11 @@ function CreateOrganizationErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:createOrganizationErrorHeading'} />
|
<Trans i18nKey={'teams:createOrganizationErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:createOrganizationErrorMessage'} />
|
<Trans i18nKey={'teams:createOrganizationErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -90,11 +90,11 @@ function RemoveInvitationErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:deleteInvitationErrorTitle'} />
|
<Trans i18nKey={'teams:deleteInvitationErrorTitle'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:deleteInvitationErrorMessage'} />
|
<Trans i18nKey={'teams:deleteInvitationErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ export const UpdateInvitationDialog: React.FC<{
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'organization:updateMemberRoleModalHeading'} />
|
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans i18nKey={'organization:updateMemberRoleModalDescription'} />
|
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ function UpdateInvitationForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
||||||
<Trans i18nKey={'organization:updateRoleSubmitLabel'} />
|
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -150,11 +150,11 @@ function UpdateRoleErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:updateRoleErrorHeading'} />
|
<Trans i18nKey={'teams:updateRoleErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:updateRoleErrorMessage'} />
|
<Trans i18nKey={'teams:updateRoleErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ function InviteMembersForm({
|
|||||||
<Plus className={'h-4'} />
|
<Plus className={'h-4'} />
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
<Trans i18nKey={'organization:addAnotherMemberButtonLabel'} />
|
<Trans i18nKey={'teams:addAnotherMemberButtonLabel'} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ function RemoveMemberForm({
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onMemberRemoved}
|
onClick={onMemberRemoved}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:removeMemberSubmitLabel'} />
|
<Trans i18nKey={'teams:removeMemberSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -95,11 +95,11 @@ function RemoveMemberErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:removeMemberErrorHeading'} />
|
<Trans i18nKey={'teams:removeMemberErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:removeMemberErrorMessage'} />
|
<Trans i18nKey={'teams:removeMemberErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ function TransferOrganizationOwnershipForm({
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'organization:transferOwnershipDisclaimer'}
|
i18nKey={'teams:transferOwnershipDisclaimer'}
|
||||||
values={{
|
values={{
|
||||||
member: targetDisplayName,
|
member: targetDisplayName,
|
||||||
}}
|
}}
|
||||||
@@ -153,9 +153,9 @@ function TransferOrganizationOwnershipForm({
|
|||||||
>
|
>
|
||||||
<If
|
<If
|
||||||
condition={pending}
|
condition={pending}
|
||||||
fallback={<Trans i18nKey={'organization:transferOwnership'} />}
|
fallback={<Trans i18nKey={'teams:transferOwnership'} />}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:transferringOwnership'} />
|
<Trans i18nKey={'teams:transferringOwnership'} />
|
||||||
</If>
|
</If>
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
@@ -167,11 +167,11 @@ function TransferOwnershipErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:transferOrganizationErrorHeading'} />
|
<Trans i18nKey={'teams:transferOrganizationErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:transferOrganizationErrorMessage'} />
|
<Trans i18nKey={'teams:transferOrganizationErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -43,11 +43,11 @@ export const UpdateMemberRoleDialog: React.FC<{
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'organization:updateMemberRoleModalHeading'} />
|
<Trans i18nKey={'teams:updateMemberRoleModalHeading'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
<Trans i18nKey={'organization:updateMemberRoleModalDescription'} />
|
<Trans i18nKey={'teams:updateMemberRoleModalDescription'} />
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ function UpdateMemberForm({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
<Button data-test={'confirm-update-member-role'} disabled={pending}>
|
||||||
<Trans i18nKey={'organization:updateRoleSubmitLabel'} />
|
<Trans i18nKey={'teams:updateRoleSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -151,11 +151,11 @@ function UpdateRoleErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:updateRoleErrorHeading'} />
|
<Trans i18nKey={'teams:updateRoleErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans i18nKey={'organization:updateRoleErrorMessage'} />
|
<Trans i18nKey={'teams:updateRoleErrorMessage'} />
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ function DeleteOrganizationContainer(props: { account: AccountData }) {
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<div className={'flex flex-col space-y-1'}>
|
<div className={'flex flex-col space-y-1'}>
|
||||||
<Heading level={6}>
|
<Heading level={6}>
|
||||||
<Trans i18nKey={'organization:deleteOrganization'} />
|
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<p className={'text-sm text-gray-500'}>
|
<p className={'text-sm text-gray-500'}>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'organization:deleteOrganizationDescription'}
|
i18nKey={'teams:deleteOrganizationDescription'}
|
||||||
values={{
|
values={{
|
||||||
organizationName: props.account.name,
|
organizationName: props.account.name,
|
||||||
}}
|
}}
|
||||||
@@ -66,14 +66,14 @@ function DeleteOrganizationContainer(props: { account: AccountData }) {
|
|||||||
type={'button'}
|
type={'button'}
|
||||||
variant={'destructive'}
|
variant={'destructive'}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:deleteOrganization'} />
|
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans i18nKey={'organization:deletingOrganization'} />
|
<Trans i18nKey={'teams:deletingOrganization'} />
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ function DeleteOrganizationForm({ name, id }: { name: string; id: string }) {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'organization:deleteOrganizationDisclaimer'}
|
i18nKey={'teams:deleteOrganizationDisclaimer'}
|
||||||
values={{
|
values={{
|
||||||
organizationName: name,
|
organizationName: name,
|
||||||
}}
|
}}
|
||||||
@@ -119,7 +119,7 @@ function DeleteOrganizationForm({ name, id }: { name: string; id: string }) {
|
|||||||
<input type="hidden" value={id} name={'id'} />
|
<input type="hidden" value={id} name={'id'} />
|
||||||
|
|
||||||
<Label>
|
<Label>
|
||||||
<Trans i18nKey={'organization:organizationNameInputLabel'} />
|
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
name={'name'}
|
name={'name'}
|
||||||
@@ -132,7 +132,7 @@ function DeleteOrganizationForm({ name, id }: { name: string; id: string }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<span className={'text-xs'}>
|
<span className={'text-xs'}>
|
||||||
<Trans i18nKey={'organization:deleteOrganizationInputField'} />
|
<Trans i18nKey={'teams:deleteOrganizationInputField'} />
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
@@ -154,7 +154,7 @@ function DeleteOrganizationSubmitButton() {
|
|||||||
disabled={pending}
|
disabled={pending}
|
||||||
variant={'destructive'}
|
variant={'destructive'}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:deleteOrganization'} />
|
<Trans i18nKey={'teams:deleteOrganization'} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ function LeaveOrganizationContainer(props: { account: AccountData }) {
|
|||||||
<div className={'flex flex-col space-y-4'}>
|
<div className={'flex flex-col space-y-4'}>
|
||||||
<p>
|
<p>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'organization:leaveOrganizationDescription'}
|
i18nKey={'teams:leaveOrganizationDescription'}
|
||||||
values={{
|
values={{
|
||||||
organizationName: props.account.name,
|
organizationName: props.account.name,
|
||||||
}}
|
}}
|
||||||
@@ -179,16 +179,14 @@ function LeaveOrganizationContainer(props: { account: AccountData }) {
|
|||||||
type={'button'}
|
type={'button'}
|
||||||
variant={'destructive'}
|
variant={'destructive'}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:leaveOrganization'} />
|
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans
|
<Trans i18nKey={'teams:leavingOrganizationModalHeading'} />
|
||||||
i18nKey={'organization:leavingOrganizationModalHeading'}
|
|
||||||
/>
|
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -200,7 +198,7 @@ function LeaveOrganizationContainer(props: { account: AccountData }) {
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'organization:leaveOrganizationDisclaimer'}
|
i18nKey={'teams:leaveOrganizationDisclaimer'}
|
||||||
values={{
|
values={{
|
||||||
organizationName: props.account?.name,
|
organizationName: props.account?.name,
|
||||||
}}
|
}}
|
||||||
@@ -230,7 +228,7 @@ function LeaveOrganizationSubmitButton() {
|
|||||||
disabled={pending}
|
disabled={pending}
|
||||||
variant={'destructive'}
|
variant={'destructive'}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:leaveOrganization'} />
|
<Trans i18nKey={'teams:leaveOrganization'} />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -239,7 +237,7 @@ function LeaveOrganizationErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:leaveOrganizationErrorHeading'} />
|
<Trans i18nKey={'teams:leaveOrganizationErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
@@ -253,7 +251,7 @@ function DeleteOrganizationErrorAlert() {
|
|||||||
return (
|
return (
|
||||||
<Alert variant={'destructive'}>
|
<Alert variant={'destructive'}>
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans i18nKey={'organization:deleteOrganizationErrorHeading'} />
|
<Trans i18nKey={'teams:deleteOrganizationErrorHeading'} />
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
|
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
|
|||||||
@@ -66,9 +66,7 @@ export const UpdateOrganizationForm = (props: {
|
|||||||
return (
|
return (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans
|
<Trans i18nKey={'teams:organizationNameInputLabel'} />
|
||||||
i18nKey={'organization:organizationNameInputLabel'}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -90,7 +88,7 @@ export const UpdateOrganizationForm = (props: {
|
|||||||
data-test={'update-organization-submit-button'}
|
data-test={'update-organization-submit-button'}
|
||||||
disabled={updateAccountData.isPending}
|
disabled={updateAccountData.isPending}
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'organization:updateOrganizationSubmitLabel'} />
|
<Trans i18nKey={'teams:updateOrganizationSubmitLabel'} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ export const I18N_COOKIE_NAME = 'lang';
|
|||||||
export const defaultI18nNamespaces = [
|
export const defaultI18nNamespaces = [
|
||||||
'common',
|
'common',
|
||||||
'auth',
|
'auth',
|
||||||
'organization',
|
'account',
|
||||||
'profile',
|
'teams',
|
||||||
'subscription',
|
'billing',
|
||||||
'onboarding',
|
'marketing',
|
||||||
|
'admin',
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getI18nSettings(
|
export function getI18nSettings(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createBrowserClient } from '@supabase/ssr';
|
|||||||
|
|
||||||
import { Database } from '../database.types';
|
import { Database } from '../database.types';
|
||||||
|
|
||||||
let client: ReturnType<typeof createBrowserClient>;
|
let client: ReturnType<typeof createBrowserClient<Database>>;
|
||||||
|
|
||||||
export function getSupabaseBrowserClient<GenericSchema = Database>() {
|
export function getSupabaseBrowserClient<GenericSchema = Database>() {
|
||||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
@@ -12,7 +12,9 @@ export function getSupabaseBrowserClient<GenericSchema = Database>() {
|
|||||||
invariant(SUPABASE_URL, `Supabase URL was not provided`);
|
invariant(SUPABASE_URL, `Supabase URL was not provided`);
|
||||||
invariant(SUPABASE_ANON_KEY, `Supabase Anon key was not provided`);
|
invariant(SUPABASE_ANON_KEY, `Supabase Anon key was not provided`);
|
||||||
|
|
||||||
if (client) return client;
|
if (client) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
client = createBrowserClient<GenericSchema>(SUPABASE_URL, SUPABASE_ANON_KEY);
|
client = createBrowserClient<GenericSchema>(SUPABASE_URL, SUPABASE_ANON_KEY);
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function AuthRedirectListener({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// keep this running for the whole session unless the component was unmounted
|
// keep this running for the whole session unless the component was unmounted
|
||||||
const listener = client.auth.onAuthStateChange((_, user) => {
|
const listener = client.auth.onAuthStateChange((_, user) => {
|
||||||
console.log(_, user);
|
|
||||||
// log user out if user is falsy
|
// log user out if user is falsy
|
||||||
// and if the current path is a private route
|
// and if the current path is a private route
|
||||||
const shouldRedirectUser = !user && isPrivateRoute(pathName);
|
const shouldRedirectUser = !user && isPrivateRoute(pathName);
|
||||||
@@ -47,10 +46,6 @@ function AuthRedirectListener({
|
|||||||
void router.refresh();
|
void router.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && isPrivateRoute(pathName)) {
|
|
||||||
return revalidateUserSession();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// destroy listener on un-mounts
|
// destroy listener on un-mounts
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { useSupabase } from './use-supabase';
|
import { useSupabase } from './use-supabase';
|
||||||
import { useRevalidateUserSession } from './use-user-session';
|
|
||||||
|
|
||||||
export function useSignOut() {
|
export function useSignOut() {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
const revalidateUserSession = useRevalidateUserSession();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
await client.auth.signOut();
|
await client.auth.signOut();
|
||||||
await revalidateUserSession();
|
await queryClient.invalidateQueries();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ type Params = UserAttributes & { redirectTo: string };
|
|||||||
|
|
||||||
export function useUpdateUser() {
|
export function useUpdateUser() {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
const mutationKey = ['auth', 'update-user'];
|
const mutationKey = ['supabase:user'];
|
||||||
|
|
||||||
const mutationFn = async (attributes: Params) => {
|
const mutationFn = async (attributes: Params) => {
|
||||||
const { redirectTo, ...params } = attributes;
|
const { redirectTo, ...params } = attributes;
|
||||||
@@ -17,10 +17,14 @@ export function useUpdateUser() {
|
|||||||
emailRedirectTo: redirectTo,
|
emailRedirectTo: redirectTo,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw response.error;
|
throw response.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('response.data:', response.data);
|
||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useSupabase } from './use-supabase';
|
|||||||
export function useUser() {
|
export function useUser() {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryKey = ['user'];
|
const queryKey = ['supabase:user'];
|
||||||
|
|
||||||
const queryFn = async () => {
|
const queryFn = async () => {
|
||||||
const response = await client.auth.getUser();
|
const response = await client.auth.getUser();
|
||||||
|
|||||||
@@ -38,7 +38,11 @@
|
|||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"sonner": "^1.4.41",
|
"sonner": "^1.4.41",
|
||||||
"lucide-react": "0.307.0",
|
"lucide-react": "0.307.0",
|
||||||
"class-variance-authority": "^0.7.0"
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.49.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/eslint-config": "0.2.0",
|
"@kit/eslint-config": "0.2.0",
|
||||||
@@ -47,13 +51,9 @@
|
|||||||
"@kit/tsconfig": "0.1.0",
|
"@kit/tsconfig": "0.1.0",
|
||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"date-fns": "^3.2.0",
|
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"react": "18.2.0",
|
|
||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hook-form": "^7.49.2",
|
|
||||||
"tailwindcss": "3.4.1",
|
"tailwindcss": "3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { Component } from 'react';
|
import { Component } from 'react';
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Button } from '../shadcn/button';
|
|||||||
import { ImageUploadInput } from './image-upload-input';
|
import { ImageUploadInput } from './image-upload-input';
|
||||||
import { Trans } from './trans';
|
import { Trans } from './trans';
|
||||||
|
|
||||||
function ImageUploader(
|
export function ImageUploader(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
value: string | null | undefined;
|
value: string | null | undefined;
|
||||||
onValueChange: (value: File | null) => unknown;
|
onValueChange: (value: File | null) => unknown;
|
||||||
@@ -87,8 +87,6 @@ function ImageUploader(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageUploader;
|
|
||||||
|
|
||||||
function FallbackImage(
|
function FallbackImage(
|
||||||
props: React.PropsWithChildren<{
|
props: React.PropsWithChildren<{
|
||||||
descriptionSection?: React.ReactNode;
|
descriptionSection?: React.ReactNode;
|
||||||
|
|||||||
1455
pnpm-lock.yaml
generated
1455
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user