3
.env
3
.env
@@ -68,3 +68,6 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
|||||||
|
|
||||||
# Configure Medusa password secret for Keycloak users
|
# Configure Medusa password secret for Keycloak users
|
||||||
MEDUSA_PASSWORD_SECRET=ODEwMGNiMmUtOGMxYS0xMWYwLWJlZDYtYTM3YzYyMWY0NGEzCg==
|
MEDUSA_PASSWORD_SECRET=ODEwMGNiMmUtOGMxYS0xMWYwLWJlZDYtYTM3YzYyMWY0NGEzCg==
|
||||||
|
|
||||||
|
# False by default
|
||||||
|
MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false
|
||||||
|
|||||||
@@ -26,6 +26,22 @@ EMAIL_PORT=1025 # or 465 for SSL
|
|||||||
EMAIL_TLS=false
|
EMAIL_TLS=false
|
||||||
NODE_TLS_REJECT_UNAUTHORIZED=0
|
NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||||
|
|
||||||
|
# MEDIPOST
|
||||||
|
|
||||||
|
MEDIPOST_URL=https://meditest.medisoft.ee:7443/Medipost/MedipostServlet
|
||||||
|
MEDIPOST_USER=trvurgtst
|
||||||
|
MEDIPOST_PASSWORD=SRB48HZMV
|
||||||
|
MEDIPOST_RECIPIENT=trvurgtst
|
||||||
|
MEDIPOST_MESSAGE_SENDER=trvurgtst
|
||||||
|
MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=true
|
||||||
|
|
||||||
|
#MEDIPOST_URL=https://medipost2.medisoft.ee:8443/Medipost/MedipostServlet
|
||||||
|
#MEDIPOST_USER=medreport
|
||||||
|
#MEDIPOST_PASSWORD=
|
||||||
|
#MEDIPOST_RECIPIENT=HTI
|
||||||
|
#MEDIPOST_MESSAGE_SENDER=medreport
|
||||||
|
#MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false
|
||||||
|
|
||||||
# MEDUSA
|
# MEDUSA
|
||||||
MEDUSA_BACKEND_URL=http://localhost:9000
|
MEDUSA_BACKEND_URL=http://localhost:9000
|
||||||
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
|
MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { Button } from '@kit/ui/button';
|
|||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { authConfig, featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
import { featureFlagsConfig, pathsConfig } from '@kit/shared/config';
|
||||||
|
import { useAuthConfig } from '@kit/shared/hooks';
|
||||||
|
|
||||||
const ModeToggle = dynamic(() =>
|
const ModeToggle = dynamic(() =>
|
||||||
import('@kit/ui/mode-toggle').then((mod) => ({
|
import('@kit/ui/mode-toggle').then((mod) => ({
|
||||||
@@ -57,6 +58,8 @@ export function SiteHeaderAccountSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AuthButtons() {
|
function AuthButtons() {
|
||||||
|
const { config } = useAuthConfig();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'animate-in fade-in flex gap-x-2.5 duration-500'}>
|
<div className={'animate-in fade-in flex gap-x-2.5 duration-500'}>
|
||||||
<div className={'hidden md:flex'}>
|
<div className={'hidden md:flex'}>
|
||||||
@@ -65,14 +68,17 @@ function AuthButtons() {
|
|||||||
</If>
|
</If>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{config && (
|
||||||
<div className={'flex gap-x-2.5'}>
|
<div className={'flex gap-x-2.5'}>
|
||||||
|
{(config.providers.password || config.providers.oAuth.length > 0) && (
|
||||||
<Button className={'block'} asChild variant={'ghost'}>
|
<Button className={'block'} asChild variant={'ghost'}>
|
||||||
<Link href={pathsConfig.auth.signIn}>
|
<Link href={pathsConfig.auth.signIn}>
|
||||||
<Trans i18nKey={'auth:signIn'} />
|
<Trans i18nKey={'auth:signIn'} />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{authConfig.providers.password && (
|
{config.providers.password && (
|
||||||
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
<Button asChild className="text-xs md:text-sm" variant={'default'}>
|
||||||
<Link href={pathsConfig.auth.signUp}>
|
<Link href={pathsConfig.auth.signUp}>
|
||||||
<Trans i18nKey={'auth:signUp'} />
|
<Trans i18nKey={'auth:signUp'} />
|
||||||
@@ -80,6 +86,7 @@ function AuthButtons() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { SignInMethodsContainer } from '@kit/auth/sign-in';
|
import { Providers, SignInMethodsContainer } from '@kit/auth/sign-in';
|
||||||
import { authConfig, pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -9,9 +9,11 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
export default function PasswordOption({
|
export default function PasswordOption({
|
||||||
inviteToken,
|
inviteToken,
|
||||||
returnPath,
|
returnPath,
|
||||||
|
providers,
|
||||||
}: {
|
}: {
|
||||||
inviteToken?: string;
|
inviteToken?: string;
|
||||||
returnPath?: string;
|
returnPath?: string;
|
||||||
|
providers: Providers;
|
||||||
}) {
|
}) {
|
||||||
const signUpPath =
|
const signUpPath =
|
||||||
pathsConfig.auth.signUp +
|
pathsConfig.auth.signUp +
|
||||||
@@ -39,7 +41,7 @@ export default function PasswordOption({
|
|||||||
<SignInMethodsContainer
|
<SignInMethodsContainer
|
||||||
inviteToken={inviteToken}
|
inviteToken={inviteToken}
|
||||||
paths={paths}
|
paths={paths}
|
||||||
providers={authConfig.providers}
|
providers={providers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={'flex justify-center'}>
|
<div className={'flex justify-center'}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { pathsConfig, authConfig } from '@kit/shared/config';
|
import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
@@ -24,11 +24,23 @@ async function SignInPage({ searchParams }: SignInPageProps) {
|
|||||||
const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } =
|
const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } =
|
||||||
await searchParams;
|
await searchParams;
|
||||||
|
|
||||||
|
const authConfig = await getServerAuthConfig();
|
||||||
|
|
||||||
if (authConfig.providers.password) {
|
if (authConfig.providers.password) {
|
||||||
return <PasswordOption inviteToken={inviteToken} returnPath={returnPath} />;
|
return (
|
||||||
|
<PasswordOption
|
||||||
|
inviteToken={inviteToken}
|
||||||
|
returnPath={returnPath}
|
||||||
|
providers={authConfig.providers}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authConfig.providers.oAuth.includes('keycloak')) {
|
||||||
return <SignInPageClientRedirect />;
|
return <SignInPageClientRedirect />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(SignInPage);
|
export default withI18n(SignInPage);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from 'next/link';
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
import { SignUpMethodsContainer } from '@kit/auth/sign-up';
|
||||||
import { authConfig, pathsConfig } from '@kit/shared/config';
|
import { getServerAuthConfig, pathsConfig } from '@kit/shared/config';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Heading } from '@kit/ui/heading';
|
import { Heading } from '@kit/ui/heading';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
@@ -38,6 +38,8 @@ async function SignUpPage({ searchParams }: Props) {
|
|||||||
pathsConfig.auth.signIn +
|
pathsConfig.auth.signIn +
|
||||||
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
(inviteToken ? `?invite_token=${inviteToken}` : '');
|
||||||
|
|
||||||
|
const authConfig = await getServerAuthConfig();
|
||||||
|
|
||||||
if (!authConfig.providers.password) {
|
if (!authConfig.providers.password) {
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
@@ -55,8 +57,7 @@ async function SignUpPage({ searchParams }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SignUpMethodsContainer
|
<SignUpMethodsContainer
|
||||||
providers={authConfig.providers}
|
authConfig={authConfig}
|
||||||
displayTermsCheckbox={authConfig.displayTermsCheckbox}
|
|
||||||
inviteToken={inviteToken}
|
inviteToken={inviteToken}
|
||||||
paths={paths}
|
paths={paths}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { ExternalLink } from '@/public/assets/external-link';
|
import { ExternalLink } from '@/public/assets/external-link';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -19,44 +22,70 @@ import {
|
|||||||
import { Input } from '@kit/ui/input';
|
import { Input } from '@kit/ui/input';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { UpdateAccountSchema } from '../_lib/schemas/update-account.schema';
|
import { UpdateAccountSchemaClient } from '../_lib/schemas/update-account.schema';
|
||||||
import { onUpdateAccount } from '../_lib/server/update-account';
|
import { onUpdateAccount } from '../_lib/server/update-account';
|
||||||
import { z } from 'zod';
|
import { toast } from '@kit/ui/sonner';
|
||||||
|
import { pathsConfig } from '@/packages/shared/src/config';
|
||||||
|
|
||||||
type UpdateAccountFormValues = z.infer<typeof UpdateAccountSchema>;
|
type UpdateAccountFormValues = z.infer<ReturnType<typeof UpdateAccountSchemaClient>>;
|
||||||
|
|
||||||
export function UpdateAccountForm({
|
export function UpdateAccountForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
|
isEmailUser,
|
||||||
}: {
|
}: {
|
||||||
defaultValues: UpdateAccountFormValues,
|
defaultValues: UpdateAccountFormValues,
|
||||||
|
isEmailUser: boolean,
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation('account');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(UpdateAccountSchema),
|
resolver: zodResolver(UpdateAccountSchemaClient({ isEmailUser })),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { firstName, lastName, personalCode, email, weight, height, userConsent } = defaultValues;
|
const { firstName, lastName, personalCode, email, userConsent } = defaultValues;
|
||||||
|
|
||||||
|
const defaultValues_weight = "weight" in defaultValues ? defaultValues.weight : null;
|
||||||
|
const defaultValues_height = "height" in defaultValues ? defaultValues.height : null;
|
||||||
|
|
||||||
const hasFirstName = !!firstName;
|
const hasFirstName = !!firstName;
|
||||||
const hasLastName = !!lastName;
|
const hasLastName = !!lastName;
|
||||||
const hasPersonalCode = !!personalCode;
|
const hasPersonalCode = !!personalCode;
|
||||||
const hasEmail = !!email;
|
const hasEmail = !!email;
|
||||||
const hasWeight = !!weight;
|
|
||||||
const hasHeight = !!height;
|
|
||||||
const hasUserConsent = !!userConsent;
|
|
||||||
|
|
||||||
const onUpdateAccountOptions = async (values: UpdateAccountFormValues) =>
|
const onUpdateAccountOptions = async (values: UpdateAccountFormValues) => {
|
||||||
onUpdateAccount({
|
const loading = toast.loading(t('updateAccount.updateAccountLoading'));
|
||||||
...values,
|
try {
|
||||||
...(hasFirstName && { firstName }),
|
const response = await onUpdateAccount({
|
||||||
...(hasLastName && { lastName }),
|
firstName: values.firstName || firstName,
|
||||||
...(hasPersonalCode && { personalCode }),
|
lastName: values.lastName || lastName,
|
||||||
...(hasEmail && { email }),
|
personalCode: values.personalCode || personalCode,
|
||||||
...(hasWeight && { weight: values.weight ?? weight }),
|
email: values.email || email,
|
||||||
...(hasHeight && { height: values.height ?? height }),
|
phone: values.phone,
|
||||||
...(hasUserConsent && { userConsent: values.userConsent ?? userConsent }),
|
weight: ((("weight" in values && values.weight) ?? defaultValues_weight) || null) as number,
|
||||||
|
height: ((("height" in values && values.height) ?? defaultValues_height) || null) as number,
|
||||||
|
userConsent: values.userConsent ?? userConsent,
|
||||||
|
city: values.city,
|
||||||
});
|
});
|
||||||
|
if (!response) {
|
||||||
|
throw new Error('Failed to update account');
|
||||||
|
}
|
||||||
|
toast.dismiss(loading);
|
||||||
|
toast.success(t('updateAccount.updateAccountSuccess'));
|
||||||
|
|
||||||
|
if (response.hasUnseenMembershipConfirmation) {
|
||||||
|
router.push(pathsConfig.auth.membershipConfirmation);
|
||||||
|
} else {
|
||||||
|
router.push(pathsConfig.app.selectPackage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.info("promiseresult error", error);
|
||||||
|
toast.error(t('updateAccount.updateAccountError'));
|
||||||
|
toast.dismiss(loading);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -66,14 +95,14 @@ export function UpdateAccountForm({
|
|||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name="firstName"
|
name="firstName"
|
||||||
disabled={hasFirstName}
|
disabled={hasFirstName && !isEmailUser}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'common:formField:firstName'} />
|
<Trans i18nKey={'common:formField:firstName'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} autoFocus={!hasFirstName} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -82,14 +111,14 @@ export function UpdateAccountForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="lastName"
|
name="lastName"
|
||||||
disabled={hasLastName}
|
disabled={hasLastName && !isEmailUser}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
<Trans i18nKey={'common:formField:lastName'} />
|
<Trans i18nKey={'common:formField:lastName'} />
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input {...field} autoFocus={hasFirstName && !hasLastName} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@@ -98,7 +127,7 @@ export function UpdateAccountForm({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="personalCode"
|
name="personalCode"
|
||||||
disabled={hasPersonalCode}
|
disabled={hasPersonalCode && !isEmailUser}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
@@ -143,6 +172,8 @@ export function UpdateAccountForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!isEmailUser && (
|
||||||
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
name="city"
|
name="city"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@@ -209,6 +240,8 @@ export function UpdateAccountForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="userConsent"
|
name="userConsent"
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import Isikukood from 'isikukood';
|
||||||
import parsePhoneNumber from 'libphonenumber-js/min';
|
import parsePhoneNumber from 'libphonenumber-js/min';
|
||||||
|
|
||||||
export const UpdateAccountSchema = z.object({
|
const updateAccountSchema = {
|
||||||
firstName: z
|
firstName: z
|
||||||
.string({
|
.string({
|
||||||
error: 'First name is required',
|
error: 'First name is required',
|
||||||
@@ -11,18 +12,27 @@ export const UpdateAccountSchema = z.object({
|
|||||||
.string({
|
.string({
|
||||||
error: 'Last name is required',
|
error: 'Last name is required',
|
||||||
})
|
})
|
||||||
.nonempty(),
|
.nonempty({
|
||||||
personalCode: z
|
error: 'common:formFieldError.stringNonEmpty',
|
||||||
.string({
|
}),
|
||||||
error: 'Personal code is required',
|
personalCode: z.string().refine(
|
||||||
})
|
(val) => {
|
||||||
.nonempty(),
|
try {
|
||||||
|
return new Isikukood(val).validate();
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'common:formFieldError.invalidPersonalCode',
|
||||||
|
},
|
||||||
|
),
|
||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
message: 'Email is required',
|
message: 'Email is required',
|
||||||
}),
|
}),
|
||||||
phone: z
|
phone: z
|
||||||
.string({
|
.string({
|
||||||
error: 'Phone number is required',
|
error: 'error:invalidPhone',
|
||||||
})
|
})
|
||||||
.nonempty()
|
.nonempty()
|
||||||
.refine(
|
.refine(
|
||||||
@@ -59,4 +69,34 @@ export const UpdateAccountSchema = z.object({
|
|||||||
userConsent: z.boolean().refine((val) => val === true, {
|
userConsent: z.boolean().refine((val) => val === true, {
|
||||||
message: 'Must be true',
|
message: 'Must be true',
|
||||||
}),
|
}),
|
||||||
|
} as const;
|
||||||
|
export const UpdateAccountSchemaServer = z.object({
|
||||||
|
firstName: updateAccountSchema.firstName,
|
||||||
|
lastName: updateAccountSchema.lastName,
|
||||||
|
personalCode: updateAccountSchema.personalCode,
|
||||||
|
email: updateAccountSchema.email,
|
||||||
|
phone: updateAccountSchema.phone,
|
||||||
|
city: updateAccountSchema.city,
|
||||||
|
weight: updateAccountSchema.weight.nullable(),
|
||||||
|
height: updateAccountSchema.height.nullable(),
|
||||||
|
userConsent: updateAccountSchema.userConsent,
|
||||||
|
});
|
||||||
|
export const UpdateAccountSchemaClient = ({ isEmailUser }: { isEmailUser: boolean }) => z.object({
|
||||||
|
firstName: updateAccountSchema.firstName,
|
||||||
|
lastName: updateAccountSchema.lastName,
|
||||||
|
personalCode: updateAccountSchema.personalCode,
|
||||||
|
email: updateAccountSchema.email,
|
||||||
|
phone: updateAccountSchema.phone,
|
||||||
|
...(isEmailUser
|
||||||
|
? {
|
||||||
|
city: z.string().optional(),
|
||||||
|
weight: z.number().optional(),
|
||||||
|
height: z.number().optional(),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
city: updateAccountSchema.city,
|
||||||
|
weight: updateAccountSchema.weight,
|
||||||
|
height: updateAccountSchema.height,
|
||||||
|
}),
|
||||||
|
userConsent: updateAccountSchema.userConsent,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import { updateCustomer } from '@lib/data/customer';
|
import { updateCustomer } from '@lib/data/customer';
|
||||||
|
|
||||||
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
import { AccountSubmitData, createAuthApi } from '@kit/auth/api';
|
||||||
@@ -10,8 +8,7 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
|||||||
|
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
|
|
||||||
|
import { UpdateAccountSchemaServer } from '../schemas/update-account.schema';
|
||||||
import { UpdateAccountSchema } from '../schemas/update-account.schema';
|
|
||||||
|
|
||||||
export const onUpdateAccount = enhanceAction(
|
export const onUpdateAccount = enhanceAction(
|
||||||
async (params: AccountSubmitData) => {
|
async (params: AccountSubmitData) => {
|
||||||
@@ -40,14 +37,11 @@ export const onUpdateAccount = enhanceAction(
|
|||||||
|
|
||||||
const hasUnseenMembershipConfirmation =
|
const hasUnseenMembershipConfirmation =
|
||||||
await api.hasUnseenMembershipConfirmation();
|
await api.hasUnseenMembershipConfirmation();
|
||||||
|
return {
|
||||||
if (hasUnseenMembershipConfirmation) {
|
hasUnseenMembershipConfirmation,
|
||||||
redirect(pathsConfig.auth.membershipConfirmation);
|
|
||||||
} else {
|
|
||||||
redirect(pathsConfig.app.selectPackage);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
schema: UpdateAccountSchema,
|
schema: UpdateAccountSchemaServer,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { signOutAction } from '@/lib/actions/sign-out';
|
import { signOutAction } from '@/lib/actions/sign-out';
|
||||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
|
||||||
|
|
||||||
import { BackButton } from '@kit/shared/components/back-button';
|
import { BackButton } from '@kit/shared/components/back-button';
|
||||||
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
import { MedReportLogo } from '@kit/shared/components/med-report-logo';
|
||||||
@@ -15,13 +14,10 @@ import { loadCurrentUserAccount } from '@/app/home/(user)/_lib/server/load-user-
|
|||||||
import { toTitleCase } from '~/lib/utils';
|
import { toTitleCase } from '~/lib/utils';
|
||||||
|
|
||||||
async function UpdateAccount() {
|
async function UpdateAccount() {
|
||||||
const client = getSupabaseServerClient();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { user },
|
|
||||||
} = await client.auth.getUser();
|
|
||||||
const isKeycloakUser = user?.app_metadata?.provider === 'keycloak';
|
const isKeycloakUser = user?.app_metadata?.provider === 'keycloak';
|
||||||
|
const isEmailUser = user?.app_metadata?.provider === 'email';
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(pathsConfig.auth.signIn);
|
redirect(pathsConfig.auth.signIn);
|
||||||
@@ -55,7 +51,7 @@ async function UpdateAccount() {
|
|||||||
<p className="text-muted-foreground pt-1 text-sm">
|
<p className="text-muted-foreground pt-1 text-sm">
|
||||||
<Trans i18nKey={'account:updateAccount:description'} />
|
<Trans i18nKey={'account:updateAccount:description'} />
|
||||||
</p>
|
</p>
|
||||||
<UpdateAccountForm defaultValues={defaultValues} />
|
<UpdateAccountForm defaultValues={defaultValues} isEmailUser={isEmailUser} />
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat md:block"></div>
|
<div className="hidden w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat md:block"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip';
|
||||||
import { pathsConfig } from '@kit/shared/config';
|
import { pathsConfig } from '@kit/shared/config';
|
||||||
@@ -22,14 +23,15 @@ export default async function AnalysisResultsPage({
|
|||||||
id: string;
|
id: string;
|
||||||
}>;
|
}>;
|
||||||
}) {
|
}) {
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
|
|
||||||
const { id: analysisOrderId } = await params;
|
const { id: analysisOrderId } = await params;
|
||||||
|
|
||||||
const analysisResponse = await loadUserAnalysis(Number(analysisOrderId));
|
const [{ account }, analysisResponse] = await Promise.all([
|
||||||
|
loadCurrentUserAccount(),
|
||||||
|
loadUserAnalysis(Number(analysisOrderId)),
|
||||||
|
]);
|
||||||
|
|
||||||
if (!account?.id || !analysisResponse) {
|
if (!account?.id) {
|
||||||
return null;
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
await createPageViewLog({
|
await createPageViewLog({
|
||||||
@@ -37,6 +39,19 @@ export default async function AnalysisResultsPage({
|
|||||||
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
action: PageViewAction.VIEW_ANALYSIS_RESULTS,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!analysisResponse) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader
|
||||||
|
title={<Trans i18nKey="analysis-results:pageTitle" />}
|
||||||
|
description={<Trans i18nKey="analysis-results:descriptionEmpty" />}
|
||||||
|
/>
|
||||||
|
<PageBody className="gap-4">
|
||||||
|
</PageBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
|
|||||||
@@ -28,9 +28,13 @@ function BookingPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppBreadcrumbs />
|
<AppBreadcrumbs />
|
||||||
<h3 className="mt-8">
|
<HomeLayoutPageHeader
|
||||||
|
title={<Trans i18nKey={'booking:title'} />}
|
||||||
|
description={<Trans i18nKey={'booking:description'} />}
|
||||||
|
/>
|
||||||
|
<h4 className="mt-8">
|
||||||
<Trans i18nKey="booking:noCategories" />
|
<Trans i18nKey="booking:noCategories" />
|
||||||
</h3>
|
</h4>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,15 @@ const env = () => z
|
|||||||
error: 'NEXT_PUBLIC_SITE_URL is required',
|
error: 'NEXT_PUBLIC_SITE_URL is required',
|
||||||
})
|
})
|
||||||
.min(1),
|
.min(1),
|
||||||
|
isEnabledDispatchOnMontonioCallback: z
|
||||||
|
.boolean({
|
||||||
|
error: 'MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK is required',
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
.parse({
|
.parse({
|
||||||
emailSender: process.env.EMAIL_SENDER,
|
emailSender: process.env.EMAIL_SENDER,
|
||||||
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
|
||||||
|
isEnabledDispatchOnMontonioCallback: process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true',
|
||||||
});
|
});
|
||||||
|
|
||||||
const sendEmail = async ({
|
const sendEmail = async ({
|
||||||
@@ -163,7 +168,7 @@ async function sendAnalysisPackageOrderEmail({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function processMontonioCallback(orderToken: string) {
|
export async function processMontonioCallback(orderToken: string) {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error("Account not found in context");
|
throw new Error("Account not found in context");
|
||||||
}
|
}
|
||||||
@@ -205,7 +210,9 @@ export async function processMontonioCallback(orderToken: string) {
|
|||||||
console.error("Missing email to send order result email", orderResult);
|
console.error("Missing email to send order result email", orderResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (env().isEnabledDispatchOnMontonioCallback) {
|
||||||
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements });
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true, orderId };
|
return { success: true, orderId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Cart from '../../_components/cart';
|
|||||||
import { listProductTypes } from '@lib/data/products';
|
import { listProductTypes } from '@lib/data/products';
|
||||||
import CartTimer from '../../_components/cart/cart-timer';
|
import CartTimer from '../../_components/cart/cart-timer';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
export async function generateMetadata() {
|
export async function generateMetadata() {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -17,9 +18,9 @@ export async function generateMetadata() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CartPage() {
|
async function CartPage() {
|
||||||
const cart = await retrieveCart().catch((error) => {
|
const cart = await retrieveCart().catch((error) => {
|
||||||
console.error(error);
|
console.error("Failed to retrieve cart", error);
|
||||||
return notFound();
|
return notFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,3 +51,5 @@ export default async function CartPage() {
|
|||||||
</PageBody>
|
</PageBody>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withI18n(CartPage);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function OrderAnalysisPage() {
|
async function OrderAnalysisPage() {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ async function OrderHealthAnalysisPage() {
|
|||||||
description={<Trans i18nKey={'order-health-analysis:description'} />}
|
description={<Trans i18nKey={'order-health-analysis:description'} />}
|
||||||
/>
|
/>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
|
<h4 className="mt-8">
|
||||||
|
<Trans i18nKey="booking:noCategories" />
|
||||||
|
</h4>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ async function OrdersPage() {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{analysisOrders.length === 0 && (
|
||||||
|
<h5 className="mt-6">
|
||||||
|
<Trans i18nKey="orders:noOrders" />
|
||||||
|
</h5>
|
||||||
|
)}
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const generateMetadata = async () => {
|
|||||||
async function UserHomePage() {
|
async function UserHomePage() {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
const api = createAccountsApi(client);
|
const api = createAccountsApi(client);
|
||||||
const bmiThresholds = await api.fetchBmiThresholds();
|
const bmiThresholds = await api.fetchBmiThresholds();
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,15 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white flex flex-col txt-medium gap-y-2">
|
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans i18nKey={'cart:locations.description'} />
|
||||||
|
</p>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
onSubmit={form.handleSubmit((data) => onSubmit(data))}
|
||||||
className="w-full mb-2 flex gap-x-2"
|
className="w-full mb-2 flex gap-x-2 flex-1"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
value={form.watch('locationId')}
|
value={form.watch('locationId')}
|
||||||
@@ -106,11 +110,6 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
<Trans i18nKey={'cart:locations.description'} />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow className="w-full" data-testid="product-row">
|
<TableRow className="w-full" data-testid="product-row">
|
||||||
<TableCell className="text-left w-[100%] px-6">
|
<TableCell className="text-left w-[100%] px-4 sm:px-6">
|
||||||
<p
|
<p
|
||||||
className="txt-medium-plus text-ui-fg-base"
|
className="txt-medium-plus text-ui-fg-base"
|
||||||
data-testid="product-title"
|
data-testid="product-title"
|
||||||
@@ -26,11 +26,11 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
</p>
|
</p>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="px-6">
|
<TableCell className="px-4 sm:px-6">
|
||||||
{item.quantity}
|
{item.quantity}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-6">
|
<TableCell className="min-w-[80px] px-4 sm:px-6">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: item.unit_price,
|
value: item.unit_price,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
@@ -38,7 +38,7 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
})}
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="min-w-[80px] px-6">
|
<TableCell className="min-w-[80px] px-4 sm:px-6 text-right">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: item.total,
|
value: item.total,
|
||||||
currencyCode,
|
currencyCode,
|
||||||
@@ -46,7 +46,7 @@ export default function CartItem({ item, currencyCode }: {
|
|||||||
})}
|
})}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|
||||||
<TableCell className="text-right px-6">
|
<TableCell className="text-right px-4 sm:px-6">
|
||||||
<span className="flex gap-x-1 justify-end w-[60px]">
|
<span className="flex gap-x-1 justify-end w-[60px]">
|
||||||
<CartItemDelete id={item.id} />
|
<CartItemDelete id={item.id} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -22,19 +22,19 @@ export default function CartItems({ cart, items, productColumnLabelKey }: {
|
|||||||
<Table className="rounded-lg border border-separate">
|
<Table className="rounded-lg border border-separate">
|
||||||
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
<TableHeader className="text-ui-fg-subtle txt-medium-plus">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-4 sm:px-6">
|
||||||
<Trans i18nKey={productColumnLabelKey} />
|
<Trans i18nKey={productColumnLabelKey} />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-4 sm:px-6">
|
||||||
<Trans i18nKey="cart:table.quantity" />
|
<Trans i18nKey="cart:table.quantity" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-6 min-w-[100px]">
|
<TableHead className="px-4 sm:px-6 min-w-[100px]">
|
||||||
<Trans i18nKey="cart:table.price" />
|
<Trans i18nKey="cart:table.price" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-6 min-w-[100px]">
|
<TableHead className="px-4 sm:px-6 min-w-[100px] text-right">
|
||||||
<Trans i18nKey="cart:table.total" />
|
<Trans i18nKey="cart:table.total" />
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="px-6">
|
<TableHead className="px-4 sm:px-6">
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
|
|||||||
24
app/home/(user)/_components/cart/discount-code-actions.ts
Normal file
24
app/home/(user)/_components/cart/discount-code-actions.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { applyPromotions } from "@lib/data/cart"
|
||||||
|
|
||||||
|
export async function addPromotionCodeAction(code: string) {
|
||||||
|
try {
|
||||||
|
await applyPromotions([code]);
|
||||||
|
return { success: true, message: 'Discount code applied successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error applying promotion code:', error);
|
||||||
|
return { success: false, message: 'Failed to apply discount code' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removePromotionCodeAction(codeToRemove: string, appliedCodes: string[]) {
|
||||||
|
try {
|
||||||
|
const updatedCodes = appliedCodes.filter((appliedCode) => appliedCode !== codeToRemove);
|
||||||
|
await applyPromotions(updatedCodes);
|
||||||
|
return { success: true, message: 'Discount code removed successfully' };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error removing promotion code:', error);
|
||||||
|
return { success: false, message: 'Failed to remove discount code' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
import { Badge, Text } from "@medusajs/ui"
|
import { Badge, Text } from "@medusajs/ui"
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import React, { useActionState } from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { applyPromotions, submitPromotionForm } from "@lib/data/cart"
|
|
||||||
import { convertToLocale } from "@lib/util/money"
|
import { convertToLocale } from "@lib/util/money"
|
||||||
import { StoreCart, StorePromotion } from "@medusajs/types"
|
import { StoreCart, StorePromotion } from "@medusajs/types"
|
||||||
import Trash from "@modules/common/icons/trash"
|
import Trash from "@modules/common/icons/trash"
|
||||||
@@ -16,6 +15,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
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 { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { addPromotionCodeAction, removePromotionCodeAction } from "./discount-code-actions";
|
||||||
|
|
||||||
const DiscountCodeSchema = z.object({
|
const DiscountCodeSchema = z.object({
|
||||||
code: z.string().min(1),
|
code: z.string().min(1),
|
||||||
@@ -31,42 +31,35 @@ export default function DiscountCode({ cart }: {
|
|||||||
const { promotions = [] } = cart;
|
const { promotions = [] } = cart;
|
||||||
|
|
||||||
const removePromotionCode = async (code: string) => {
|
const removePromotionCode = async (code: string) => {
|
||||||
const validPromotions = promotions.filter(
|
const appliedCodes = promotions
|
||||||
(promotion) => promotion.code !== code,
|
.filter((p) => p.code !== undefined)
|
||||||
)
|
.map((p) => p.code!)
|
||||||
|
|
||||||
await applyPromotions(
|
const loading = toast.loading(t('cart:discountCode.removeLoading'));
|
||||||
validPromotions.filter((p) => p.code === undefined).map((p) => p.code!),
|
|
||||||
{
|
const result = await removePromotionCodeAction(code, appliedCodes)
|
||||||
onSuccess: () => {
|
|
||||||
|
toast.dismiss(loading);
|
||||||
|
if (result.success) {
|
||||||
toast.success(t('cart:discountCode.removeSuccess'));
|
toast.success(t('cart:discountCode.removeSuccess'));
|
||||||
},
|
} else {
|
||||||
onError: () => {
|
|
||||||
toast.error(t('cart:discountCode.removeError'));
|
toast.error(t('cart:discountCode.removeError'));
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addPromotionCode = async (code: string) => {
|
const addPromotionCode = async (code: string) => {
|
||||||
const codes = promotions
|
const loading = toast.loading(t('cart:discountCode.addLoading'));
|
||||||
.filter((p) => p.code === undefined)
|
const result = await addPromotionCodeAction(code)
|
||||||
.map((p) => p.code!)
|
|
||||||
codes.push(code.toString())
|
|
||||||
|
|
||||||
await applyPromotions(codes, {
|
toast.dismiss(loading);
|
||||||
onSuccess: () => {
|
if (result.success) {
|
||||||
toast.success(t('cart:discountCode.addSuccess'));
|
toast.success(t('cart:discountCode.addSuccess'));
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
toast.error(t('cart:discountCode.addError'));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
form.reset()
|
form.reset()
|
||||||
|
} else {
|
||||||
|
toast.error(t('cart:discountCode.addError'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [message, formAction] = useActionState(submitPromotionForm, null)
|
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
|
const form = useForm<z.infer<typeof DiscountCodeSchema>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -76,11 +69,15 @@ export default function DiscountCode({ cart }: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white flex flex-col txt-medium">
|
<div className="w-full h-full bg-white flex flex-col txt-medium gap-y-4">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
||||||
|
</p>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
onSubmit={form.handleSubmit((data) => addPromotionCode(data.code))}
|
||||||
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2"
|
className="w-full mb-2 flex gap-x-2 sm:flex-row flex-col gap-y-2 flex-1"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
name={'code'}
|
name={'code'}
|
||||||
@@ -96,14 +93,14 @@ export default function DiscountCode({ cart }: {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="h-full"
|
className="h-min"
|
||||||
>
|
>
|
||||||
<Trans i18nKey={'cart:discountCode.apply'} />
|
<Trans i18nKey={'cart:discountCode.apply'} />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{promotions.length > 0 ? (
|
{promotions.length > 0 && (
|
||||||
<div className="w-full flex items-center mt-4">
|
<div className="w-full flex items-center mt-4">
|
||||||
<div className="flex flex-col w-full gap-y-2">
|
<div className="flex flex-col w-full gap-y-2">
|
||||||
<p>
|
<p>
|
||||||
@@ -117,12 +114,12 @@ export default function DiscountCode({ cart }: {
|
|||||||
className="flex items-center justify-between w-full max-w-full mb-2"
|
className="flex items-center justify-between w-full max-w-full mb-2"
|
||||||
data-testid="discount-row"
|
data-testid="discount-row"
|
||||||
>
|
>
|
||||||
<Text className="flex gap-x-1 items-baseline txt-small-plus w-4/5 pr-1">
|
<Text className="flex gap-x-1 items-baseline text-sm w-4/5 pr-1">
|
||||||
<span className="truncate" data-testid="discount-code">
|
<span className="truncate" data-testid="discount-code">
|
||||||
<Badge
|
<Badge
|
||||||
color={promotion.is_automatic ? "green" : "grey"}
|
color={promotion.is_automatic ? "green" : "grey"}
|
||||||
size="small"
|
size="small"
|
||||||
className="px-4"
|
className="px-4 text-sm"
|
||||||
>
|
>
|
||||||
{promotion.code}
|
{promotion.code}
|
||||||
</Badge>{" "}
|
</Badge>{" "}
|
||||||
@@ -135,7 +132,7 @@ export default function DiscountCode({ cart }: {
|
|||||||
"percentage"
|
"percentage"
|
||||||
? `${promotion.application_method.value}%`
|
? `${promotion.application_method.value}%`
|
||||||
: convertToLocale({
|
: convertToLocale({
|
||||||
amount: promotion.application_method.value,
|
amount: Number(promotion.application_method.value),
|
||||||
currency_code:
|
currency_code:
|
||||||
promotion.application_method
|
promotion.application_method
|
||||||
.currency_code,
|
.currency_code,
|
||||||
@@ -173,10 +170,6 @@ export default function DiscountCode({ cart }: {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
<Trans i18nKey={'cart:discountCode.subtitle'} />
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -78,14 +78,14 @@ export default function Cart({
|
|||||||
</div>
|
</div>
|
||||||
{hasCartItems && (
|
{hasCartItems && (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-end gap-x-4 px-6 pt-4">
|
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 pt-2 sm:pt-4">
|
||||||
<div className="mr-[36px]">
|
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||||
<Trans i18nKey="cart:subtotal" />
|
<Trans i18nKey="cart:order.subtotal" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-[116px]">
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
<p className="text-sm">
|
<p className="text-sm text-right">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.subtotal,
|
value: cart.subtotal,
|
||||||
currencyCode: cart.currency_code,
|
currencyCode: cart.currency_code,
|
||||||
@@ -94,14 +94,14 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-x-4 px-6 py-2">
|
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6 py-2 sm:py-4">
|
||||||
<div className="mr-[36px]">
|
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||||
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
<p className="ml-0 font-bold text-sm text-muted-foreground">
|
||||||
<Trans i18nKey="cart:promotionsTotal" />
|
<Trans i18nKey="cart:order.promotionsTotal" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-[116px]">
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
<p className="text-sm">
|
<p className="text-sm text-right">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.discount_total,
|
value: cart.discount_total,
|
||||||
currencyCode: cart.currency_code,
|
currencyCode: cart.currency_code,
|
||||||
@@ -110,14 +110,14 @@ export default function Cart({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-x-4 px-6">
|
<div className="flex sm:justify-end gap-x-4 px-4 sm:px-6">
|
||||||
<div className="mr-[36px]">
|
<div className="w-full sm:w-auto sm:mr-[42px]">
|
||||||
<p className="ml-0 font-bold text-sm">
|
<p className="ml-0 font-bold text-sm">
|
||||||
<Trans i18nKey="cart:total" />
|
<Trans i18nKey="cart:order.total" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mr-[116px]">
|
<div className={`sm:mr-[112px] sm:w-[50px]`}>
|
||||||
<p className="text-sm">
|
<p className="text-sm text-right">
|
||||||
{formatCurrency({
|
{formatCurrency({
|
||||||
value: cart.total,
|
value: cart.total,
|
||||||
currencyCode: cart.currency_code,
|
currencyCode: cart.currency_code,
|
||||||
@@ -129,7 +129,7 @@ export default function Cart({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex sm:flex-row flex-col gap-y-6 py-8 gap-x-4">
|
<div className="flex sm:flex-row flex-col gap-y-6 py-4 sm:py-8 gap-x-4">
|
||||||
{IS_DISCOUNT_SHOWN && (
|
{IS_DISCOUNT_SHOWN && (
|
||||||
<Card
|
<Card
|
||||||
className="flex flex-col justify-between w-full sm:w-1/2"
|
className="flex flex-col justify-between w-full sm:w-1/2"
|
||||||
@@ -139,7 +139,7 @@ export default function Cart({
|
|||||||
<Trans i18nKey="cart:discountCode.title" />
|
<Trans i18nKey="cart:discountCode.title" />
|
||||||
</h5>
|
</h5>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="h-full">
|
||||||
<DiscountCode cart={{ ...cart }} />
|
<DiscountCode cart={{ ...cart }} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -154,7 +154,7 @@ export default function Cart({
|
|||||||
<Trans i18nKey="cart:locations.title" />
|
<Trans i18nKey="cart:locations.title" />
|
||||||
</h5>
|
</h5>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="h-full">
|
||||||
<AnalysisLocation cart={{ ...cart }} synlabAnalyses={synlabAnalyses} />
|
<AnalysisLocation cart={{ ...cart }} synlabAnalyses={synlabAnalyses} />
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -136,10 +136,10 @@ const ComparePackagesModal = async ({
|
|||||||
{isIncludedInStandard && <CheckWithBackground />}
|
{isIncludedInStandard && <CheckWithBackground />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
{(isIncludedInStandard || isIncludedInStandardPlus) && <CheckWithBackground />}
|
{isIncludedInStandardPlus && <CheckWithBackground />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="center" className="py-6">
|
<TableCell align="center" className="py-6">
|
||||||
{(isIncludedInStandard || isIncludedInStandardPlus || isIncludedInPremium) && <CheckWithBackground />}
|
{isIncludedInPremium && <CheckWithBackground />}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
|
|
||||||
export default function DashboardCards() {
|
export default function DashboardCards() {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-4 lg:px-4">
|
<div className="flex gap-4">
|
||||||
<Card
|
<Card
|
||||||
variant="gradient-success"
|
variant="gradient-success"
|
||||||
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"
|
className="xs:w-1/2 sm:w-auto flex w-full flex-col justify-between"
|
||||||
|
|||||||
@@ -146,14 +146,21 @@ export default function Dashboard({
|
|||||||
}) {
|
}) {
|
||||||
const height = account.accountParams?.height || 0;
|
const height = account.accountParams?.height || 0;
|
||||||
const weight = account.accountParams?.weight || 0;
|
const weight = account.accountParams?.weight || 0;
|
||||||
const { age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!);
|
|
||||||
|
let age: number = 0;
|
||||||
|
let gender: { label: string; value: string } | null = null;
|
||||||
|
try {
|
||||||
|
({ age = 0, gender } = PersonalCode.parsePersonalCode(account.personal_code!));
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse personal code", e);
|
||||||
|
}
|
||||||
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
|
const bmiStatus = getBmiStatus(bmiThresholds, { age, height, weight });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="xs:grid-cols-2 grid auto-rows-fr gap-3 sm:grid-cols-4 lg:grid-cols-5">
|
<div className="xs:grid-cols-2 grid auto-rows-fr gap-3 sm:grid-cols-4 lg:grid-cols-5">
|
||||||
{cards({
|
{cards({
|
||||||
gender: gender.label,
|
gender: gender?.label,
|
||||||
age,
|
age,
|
||||||
height,
|
height,
|
||||||
weight,
|
weight,
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import { formatCurrency } from '@/packages/shared/src/utils';
|
|||||||
export type OrderAnalysisCard = Pick<
|
export type OrderAnalysisCard = Pick<
|
||||||
StoreProduct, 'title' | 'description' | 'subtitle'
|
StoreProduct, 'title' | 'description' | 'subtitle'
|
||||||
> & {
|
> & {
|
||||||
isAvailable: boolean;
|
|
||||||
variant: { id: string };
|
variant: { id: string };
|
||||||
price: number | null;
|
price: number | null;
|
||||||
};
|
};
|
||||||
@@ -58,13 +57,12 @@ export default function OrderAnalysesCards({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid 2xs:grid-cols-3 gap-6 mt-4">
|
<div className="grid xs:grid-cols-3 gap-6 mt-4">
|
||||||
{analyses.map(({
|
{analyses.map(({
|
||||||
title,
|
title,
|
||||||
variant,
|
variant,
|
||||||
description,
|
description,
|
||||||
subtitle,
|
subtitle,
|
||||||
isAvailable,
|
|
||||||
price,
|
price,
|
||||||
}) => {
|
}) => {
|
||||||
const formattedPrice = typeof price === 'number'
|
const formattedPrice = typeof price === 'number'
|
||||||
@@ -77,7 +75,7 @@ export default function OrderAnalysesCards({
|
|||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
key={title}
|
key={title}
|
||||||
variant={isAvailable ? "gradient-success" : "gradient-warning"}
|
variant="gradient-success"
|
||||||
className="flex flex-col justify-between"
|
className="flex flex-col justify-between"
|
||||||
>
|
>
|
||||||
<CardHeader className="flex-row">
|
<CardHeader className="flex-row">
|
||||||
@@ -86,7 +84,6 @@ export default function OrderAnalysesCards({
|
|||||||
>
|
>
|
||||||
<HeartPulse className="size-4 fill-green-500" />
|
<HeartPulse className="size-4 fill-green-500" />
|
||||||
</div>
|
</div>
|
||||||
{isAvailable && (
|
|
||||||
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
<div className='ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-warning'>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -97,9 +94,9 @@ export default function OrderAnalysesCards({
|
|||||||
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
{variantAddingToCart === variant.id ? <Loader2 className="size-4 stroke-2 animate-spin" /> : <ShoppingCart className="size-4 stroke-2" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardFooter className="flex flex-col items-start gap-2">
|
<CardFooter className="flex gap-2">
|
||||||
|
<div className="flex flex-col items-start gap-2 flex-1">
|
||||||
<h5>
|
<h5>
|
||||||
{title}
|
{title}
|
||||||
{description && (
|
{description && (
|
||||||
@@ -116,16 +113,15 @@ export default function OrderAnalysesCards({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{isAvailable && subtitle && (
|
{subtitle && (
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
)}
|
)}
|
||||||
{!isAvailable && (
|
</div>
|
||||||
<CardDescription>
|
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
||||||
<Trans i18nKey={'order-analysis:analysisNotAvailable'} />
|
<span>{formattedPrice}</span>
|
||||||
</CardDescription>
|
</div>
|
||||||
)}
|
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function CartTotals({ medusaOrder }: {
|
|||||||
<div className="flex flex-col gap-y-2 txt-medium text-ui-fg-subtle ">
|
<div className="flex flex-col gap-y-2 txt-medium text-ui-fg-subtle ">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="flex gap-x-1 items-center">
|
<span className="flex gap-x-1 items-center">
|
||||||
<Trans i18nKey="cart:orderConfirmed.subtotal" />
|
<Trans i18nKey="cart:order.subtotal" />
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="cart-subtotal" data-value={subtotal || 0}>
|
<span data-testid="cart-subtotal" data-value={subtotal || 0}>
|
||||||
{formatCurrency({ value: subtotal ?? 0, currencyCode: currency_code, locale: language })}
|
{formatCurrency({ value: subtotal ?? 0, currencyCode: currency_code, locale: language })}
|
||||||
@@ -32,7 +32,7 @@ export default function CartTotals({ medusaOrder }: {
|
|||||||
</div>
|
</div>
|
||||||
{!!discount_total && (
|
{!!discount_total && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span><Trans i18nKey="cart:orderConfirmed.discount" /></span>
|
<span><Trans i18nKey="cart:order.promotionsTotal" /></span>
|
||||||
<span
|
<span
|
||||||
className="text-ui-fg-interactive"
|
className="text-ui-fg-interactive"
|
||||||
data-testid="cart-discount"
|
data-testid="cart-discount"
|
||||||
@@ -43,17 +43,17 @@ export default function CartTotals({ medusaOrder }: {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex justify-between">
|
{/* <div className="flex justify-between">
|
||||||
<span className="flex gap-x-1 items-center ">
|
<span className="flex gap-x-1 items-center ">
|
||||||
<Trans i18nKey="cart:orderConfirmed.taxes" />
|
<Trans i18nKey="cart:orderConfirmed.taxes" />
|
||||||
</span>
|
</span>
|
||||||
<span data-testid="cart-taxes" data-value={tax_total || 0}>
|
<span data-testid="cart-taxes" data-value={tax_total || 0}>
|
||||||
{formatCurrency({ value: tax_total ?? 0, currencyCode: currency_code, locale: language })}
|
{formatCurrency({ value: tax_total ?? 0, currencyCode: currency_code, locale: language })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div> */}
|
||||||
{!!gift_card_total && (
|
{!!gift_card_total && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span><Trans i18nKey="cart:orderConfirmed.giftCard" /></span>
|
<span><Trans i18nKey="cart:order.giftCard" /></span>
|
||||||
<span
|
<span
|
||||||
className="text-ui-fg-interactive"
|
className="text-ui-fg-interactive"
|
||||||
data-testid="cart-gift-card-amount"
|
data-testid="cart-gift-card-amount"
|
||||||
@@ -67,7 +67,7 @@ export default function CartTotals({ medusaOrder }: {
|
|||||||
</div>
|
</div>
|
||||||
<div className="h-px w-full border-b border-gray-200 my-4" />
|
<div className="h-px w-full border-b border-gray-200 my-4" />
|
||||||
<div className="flex items-center justify-between text-ui-fg-base mb-2 txt-medium ">
|
<div className="flex items-center justify-between text-ui-fg-base mb-2 txt-medium ">
|
||||||
<span className="font-bold"><Trans i18nKey="cart:orderConfirmed.total" /></span>
|
<span className="font-bold"><Trans i18nKey="cart:order.total" /></span>
|
||||||
<span
|
<span
|
||||||
className="txt-xlarge-plus"
|
className="txt-xlarge-plus"
|
||||||
data-testid="cart-total"
|
data-testid="cart-total"
|
||||||
|
|||||||
@@ -7,15 +7,23 @@ export default function OrderDetails({ order }: {
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div>
|
||||||
|
<span className="font-bold">
|
||||||
|
<Trans i18nKey="cart:orderConfirmed.orderNumber" />:{" "}
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
{order.medusa_order_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span className="font-bold">
|
||||||
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
|
<Trans i18nKey="cart:orderConfirmed.orderDate" />:{" "}
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
|
{formatDate(order.created_at, 'dd.MM.yyyy HH:mm')}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</div>
|
||||||
<span className="text-ui-fg-interactive">
|
|
||||||
<Trans i18nKey="cart:orderConfirmed.orderNumber" />: <span data-testid="order-id">{order.medusa_order_id}</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createPageViewLog, PageViewAction } from "~/lib/services/audit/pageView
|
|||||||
import { loadCurrentUserAccount } from "../../_lib/server/load-user-account";
|
import { loadCurrentUserAccount } from "../../_lib/server/load-user-account";
|
||||||
|
|
||||||
export async function logAnalysisResultsNavigateAction(analysisOrderId: string) {
|
export async function logAnalysisResultsNavigateAction(analysisOrderId: string) {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async function analysesLoader() {
|
|||||||
const categoryProducts = category
|
const categoryProducts = category
|
||||||
? await listProducts({
|
? await listProducts({
|
||||||
countryCode,
|
countryCode,
|
||||||
queryParams: { limit: 100, category_id: category.id },
|
queryParams: { limit: 100, category_id: category.id, order: 'title' },
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -51,8 +51,10 @@ async function analysesLoader() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
analyses:
|
analyses:
|
||||||
categoryProducts?.response.products.map<OrderAnalysisCard>(
|
categoryProducts?.response.products
|
||||||
({ title, description, subtitle, variants, status, metadata }) => {
|
.filter(({ status, metadata }) => status === 'published' && !!metadata?.analysisIdOriginal)
|
||||||
|
.map<OrderAnalysisCard>(
|
||||||
|
({ title, description, subtitle, variants }) => {
|
||||||
const variant = variants![0]!;
|
const variant = variants![0]!;
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
@@ -61,8 +63,6 @@ async function analysesLoader() {
|
|||||||
variant: {
|
variant: {
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
},
|
},
|
||||||
isAvailable:
|
|
||||||
status === 'published' && !!metadata?.analysisIdOriginal,
|
|
||||||
price: variant.calculated_price?.calculated_amount ?? null,
|
price: variant.calculated_price?.calculated_amount ?? null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ async function analysisPackagesWithVariantLoader({
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function analysisPackagesLoader() {
|
async function analysisPackagesLoader() {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,12 @@ export const loadUserAccount = cache(accountLoader);
|
|||||||
|
|
||||||
export async function loadCurrentUserAccount() {
|
export async function loadCurrentUserAccount() {
|
||||||
const user = await requireUserInServerComponent();
|
const user = await requireUserInServerComponent();
|
||||||
return user?.id
|
const userId = user?.id;
|
||||||
? await loadUserAccount(user.id)
|
if (!userId) {
|
||||||
: null;
|
return { account: null, user: null };
|
||||||
|
}
|
||||||
|
const account = await loadUserAccount(userId);
|
||||||
|
return { account, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function accountLoader(userId: string) {
|
async function accountLoader(userId: string) {
|
||||||
|
|||||||
@@ -28,20 +28,15 @@ async function workspaceLoader() {
|
|||||||
|
|
||||||
const workspacePromise = api.getAccountWorkspace();
|
const workspacePromise = api.getAccountWorkspace();
|
||||||
|
|
||||||
// TODO!: remove before deploy to prod
|
const [accounts, workspace, user] = await Promise.all([
|
||||||
const tempAccountsPromise = () => api.loadTempUserAccounts();
|
|
||||||
|
|
||||||
const [accounts, workspace, user, tempVisibleAccounts] = await Promise.all([
|
|
||||||
accountsPromise(),
|
accountsPromise(),
|
||||||
workspacePromise,
|
workspacePromise,
|
||||||
requireUserInServerComponent(),
|
requireUserInServerComponent(),
|
||||||
tempAccountsPromise(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
workspace,
|
workspace,
|
||||||
user,
|
user,
|
||||||
tempVisibleAccounts,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Trans } from 'react-i18next';
|
|||||||
import { AccountWithParams } from '@kit/accounts/api';
|
import { AccountWithParams } from '@kit/accounts/api';
|
||||||
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
import { Card, CardTitle } from '@kit/ui/card';
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -25,7 +24,6 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@kit/ui/select';
|
} from '@kit/ui/select';
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
import { Switch } from '@kit/ui/switch';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AccountSettings,
|
AccountSettings,
|
||||||
@@ -131,7 +129,11 @@ export default function AccountSettingsForm({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
placeholder="cm"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -150,7 +152,11 @@ export default function AccountSettingsForm({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} />
|
<Input
|
||||||
|
placeholder="kg"
|
||||||
|
type="number"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export const accountSettingsSchema = z.object({
|
|||||||
email: z.email({ error: 'error:invalidEmail' }).nullable(),
|
email: z.email({ error: 'error:invalidEmail' }).nullable(),
|
||||||
phone: z.e164({ error: 'error:invalidPhone' }),
|
phone: z.e164({ error: 'error:invalidPhone' }),
|
||||||
accountParams: z.object({
|
accountParams: z.object({
|
||||||
height: z.coerce.number({ error: 'error:invalidNumber' }),
|
height: z.coerce.number({ error: 'error:invalidNumber' }).gt(0),
|
||||||
weight: z.coerce.number({ error: 'error:invalidNumber' }),
|
weight: z.coerce.number({ error: 'error:invalidNumber' }).gt(0),
|
||||||
isSmoker: z.boolean().optional().nullable(),
|
isSmoker: z.boolean().optional().nullable(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const generateMetadata = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
async function PersonalAccountSettingsPage() {
|
async function PersonalAccountSettingsPage() {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<div className="mx-auto w-full bg-white p-6">
|
<div className="mx-auto w-full bg-white p-6">
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { CardTitle } from '@kit/ui/card';
|
|
||||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
|
||||||
|
|
||||||
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
import { loadCurrentUserAccount } from '../../_lib/server/load-user-account';
|
||||||
import AccountPreferencesForm from '../_components/account-preferences-form';
|
import AccountPreferencesForm from '../_components/account-preferences-form';
|
||||||
import SettingsSectionHeader from '../_components/settings-section-header';
|
import SettingsSectionHeader from '../_components/settings-section-header';
|
||||||
|
|
||||||
export default async function PreferencesPage() {
|
export default async function PreferencesPage() {
|
||||||
const account = await loadCurrentUserAccount();
|
const { account } = await loadCurrentUserAccount();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full bg-white p-6">
|
<div className="mx-auto w-full bg-white p-6">
|
||||||
@@ -16,7 +12,6 @@ export default async function PreferencesPage() {
|
|||||||
titleKey="account:preferencesTabLabel"
|
titleKey="account:preferencesTabLabel"
|
||||||
descriptionKey="account:preferencesTabDescription"
|
descriptionKey="account:preferencesTabDescription"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AccountPreferencesForm account={account} />
|
<AccountPreferencesForm account={account} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
|
|
||||||
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
import { createAccountsApi } from '@/packages/features/accounts/src/server/api';
|
||||||
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client';
|
||||||
|
|
||||||
@@ -12,8 +11,7 @@ export default async function HomeLayout({
|
|||||||
}) {
|
}) {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const user = await requireUserInServerComponent();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
const api = createAccountsApi(client);
|
const api = createAccountsApi(client);
|
||||||
|
|
||||||
const hasAccountTeamMembership = await api.hasAccountTeamMembership(
|
const hasAccountTeamMembership = await api.hasAccountTeamMembership(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
|
|
||||||
import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal';
|
import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal';
|
||||||
import { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages';
|
import { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages';
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const { t } = await createI18nServerInstance();
|
const { t } = await createI18nServerInstance();
|
||||||
@@ -27,6 +28,10 @@ async function SelectPackagePage() {
|
|||||||
const { analysisPackageElements, analysisPackages, countryCode } =
|
const { analysisPackageElements, analysisPackages, countryCode } =
|
||||||
await loadAnalysisPackages();
|
await loadAnalysisPackages();
|
||||||
|
|
||||||
|
if (analysisPackageElements.length === 0) {
|
||||||
|
return redirect(pathsConfig.app.home);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto my-24 flex flex-col items-center space-y-12">
|
<div className="container mx-auto my-24 flex flex-col items-center space-y-12">
|
||||||
<MedReportLogo />
|
<MedReportLogo />
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export const defaultI18nNamespaces = [
|
|||||||
'booking',
|
'booking',
|
||||||
'order-analysis-package',
|
'order-analysis-package',
|
||||||
'order-analysis',
|
'order-analysis',
|
||||||
|
'order-health-analysis',
|
||||||
'cart',
|
'cart',
|
||||||
'orders',
|
'orders',
|
||||||
'analysis-results',
|
'analysis-results',
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function composeOrderTestResponseXML({
|
|||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId, "AL")}
|
${getPais(USER, RECIPIENT, orderId, "AL")}
|
||||||
<Vastus>
|
<Vastus>
|
||||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||||
${getClientInstitution({ index: 1 })}
|
${getClientInstitution({ index: 1 })}
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ export async function composeOrderXML({
|
|||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
<Saadetis xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="TellimusLOINC.xsd">
|
||||||
${getPais(USER, RECIPIENT, orderCreatedAt, orderId)}
|
${getPais(USER, RECIPIENT, orderId)}
|
||||||
<Tellimus cito="EI">
|
<Tellimus cito="EI">
|
||||||
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
<ValisTellimuseId>${orderId}</ValisTellimuseId>
|
||||||
${getClientInstitution()}
|
${getClientInstitution()}
|
||||||
|
|||||||
@@ -38,8 +38,7 @@ export async function handleAddToCart({
|
|||||||
countryCode: string;
|
countryCode: string;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
@@ -70,8 +69,7 @@ export async function handleDeleteCartItem({ lineId }: { lineId: string }) {
|
|||||||
|
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const cartId = await getCartId();
|
const cartId = await getCartId();
|
||||||
const user = await requireUserInServerComponent();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
@@ -96,8 +94,7 @@ export async function handleNavigateToPayment({
|
|||||||
paymentSessionId: string;
|
paymentSessionId: string;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
@@ -137,8 +134,7 @@ export async function handleLineItemTimeout({
|
|||||||
lineItem: StoreCartLineItem;
|
lineItem: StoreCartLineItem;
|
||||||
}) {
|
}) {
|
||||||
const supabase = getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const user = await requireUserInServerComponent();
|
const { account, user } = await loadCurrentUserAccount();
|
||||||
const account = await loadCurrentUserAccount();
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new Error('Account not found');
|
throw new Error('Account not found');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const getPais = (
|
|||||||
<Pakett versioon="20">${packageName}</Pakett>
|
<Pakett versioon="20">${packageName}</Pakett>
|
||||||
<Saatja>${sender}</Saatja>
|
<Saatja>${sender}</Saatja>
|
||||||
<Saaja>${recipient}</Saaja>
|
<Saaja>${recipient}</Saaja>
|
||||||
<Aeg>${format(createdAt, DATE_TIME_FORMAT)}</Aeg>
|
<Aeg>${format(new Date(), DATE_TIME_FORMAT)}</Aeg>
|
||||||
<SaadetisId>${orderId}</SaadetisId>
|
<SaadetisId>${orderId}</SaadetisId>
|
||||||
<Email>info@medreport.ee</Email>
|
<Email>info@medreport.ee</Email>
|
||||||
</Pais>`;
|
</Pais>`;
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export default class PersonalCode {
|
|||||||
if (age >= 60) {
|
if (age >= 60) {
|
||||||
return '60';
|
return '60';
|
||||||
}
|
}
|
||||||
throw new Error('Age range not supported');
|
throw new Error('Age range not supported, age=' + age);
|
||||||
})();
|
})();
|
||||||
const gender = (() => {
|
const gender = (() => {
|
||||||
const gender = parsed.getGender();
|
const gender = parsed.getGender();
|
||||||
|
|||||||
@@ -265,11 +265,13 @@ function FactorQrCode({
|
|||||||
z.object({
|
z.object({
|
||||||
factorName: z.string().min(1),
|
factorName: z.string().min(1),
|
||||||
qrCode: z.string().min(1),
|
qrCode: z.string().min(1),
|
||||||
|
totpSecret: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
factorName: '',
|
factorName: '',
|
||||||
qrCode: '',
|
qrCode: '',
|
||||||
|
totpSecret: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -319,6 +321,7 @@ function FactorQrCode({
|
|||||||
if (data.type === 'totp') {
|
if (data.type === 'totp') {
|
||||||
form.setValue('factorName', name);
|
form.setValue('factorName', name);
|
||||||
form.setValue('qrCode', data.totp.qr_code);
|
form.setValue('qrCode', data.totp.qr_code);
|
||||||
|
form.setValue('totpSecret', data.totp.secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
// dispatch event to set factor ID
|
// dispatch event to set factor ID
|
||||||
@@ -331,7 +334,7 @@ function FactorQrCode({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'dark:bg-secondary flex flex-col space-y-4 rounded-lg border p-4'
|
'dark:bg-secondary flex flex-col space-y-2 rounded-lg border p-4'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
@@ -343,6 +346,10 @@ function FactorQrCode({
|
|||||||
<div className={'flex justify-center'}>
|
<div className={'flex justify-center'}>
|
||||||
<QrImage src={form.getValues('qrCode')} />
|
<QrImage src={form.getValues('qrCode')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className='text-center text-sm'>
|
||||||
|
{form.getValues('totpSecret')}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const personalCodeSchema = z.string().refine(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: 'Invalid personal code',
|
message: 'common:formFieldError.invalidPersonalCode',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ export function AuthLayoutShell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
'sm:py-auto flex flex-col items-center justify-center py-6' +
|
'sm:py-auto flex flex-col items-center justify-center py-6 h-screen' +
|
||||||
' bg-background lg:bg-muted/30 gap-y-10 lg:gap-y-8' +
|
' bg-background lg:bg-muted/30 gap-y-10 lg:gap-y-8'
|
||||||
' animate-in fade-in slide-in-from-top-16 zoom-in-95 duration-1000'
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{Logo ? <Logo /> : null}
|
{Logo ? <Logo /> : null}
|
||||||
|
|||||||
@@ -113,12 +113,16 @@ export const OauthProviders: React.FC<{
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{provider === 'keycloak' ? (
|
||||||
|
<Trans i18nKey={'auth:signInWithKeycloak'} />
|
||||||
|
) : (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={'auth:signInWithProvider'}
|
i18nKey={'auth:signInWithProvider'}
|
||||||
values={{
|
values={{
|
||||||
provider: getProviderName(provider),
|
provider: getProviderName(provider),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</AuthProviderButton>
|
</AuthProviderButton>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -10,9 +10,18 @@ import { useCaptchaToken } from '../captcha/client';
|
|||||||
import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow';
|
import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow';
|
||||||
import { AuthErrorAlert } from './auth-error-alert';
|
import { AuthErrorAlert } from './auth-error-alert';
|
||||||
import { PasswordSignUpForm } from './password-sign-up-form';
|
import { PasswordSignUpForm } from './password-sign-up-form';
|
||||||
|
import { Spinner } from '@kit/ui/makerkit/spinner';
|
||||||
|
|
||||||
interface EmailPasswordSignUpContainerProps {
|
interface EmailPasswordSignUpContainerProps {
|
||||||
displayTermsCheckbox?: boolean;
|
authConfig: {
|
||||||
|
providers: {
|
||||||
|
password: boolean;
|
||||||
|
magicLink: boolean;
|
||||||
|
oAuth: string[];
|
||||||
|
};
|
||||||
|
displayTermsCheckbox: boolean | undefined;
|
||||||
|
isMailerAutoconfirmEnabled: boolean;
|
||||||
|
};
|
||||||
defaultValues?: {
|
defaultValues?: {
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
@@ -21,10 +30,10 @@ interface EmailPasswordSignUpContainerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function EmailPasswordSignUpContainer({
|
export function EmailPasswordSignUpContainer({
|
||||||
|
authConfig,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
onSignUp,
|
onSignUp,
|
||||||
emailRedirectTo,
|
emailRedirectTo,
|
||||||
displayTermsCheckbox,
|
|
||||||
}: EmailPasswordSignUpContainerProps) {
|
}: EmailPasswordSignUpContainerProps) {
|
||||||
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
const { captchaToken, resetCaptchaToken } = useCaptchaToken();
|
||||||
|
|
||||||
@@ -43,7 +52,12 @@ export function EmailPasswordSignUpContainer({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<If condition={showVerifyEmailAlert}>
|
<If condition={showVerifyEmailAlert}>
|
||||||
<SuccessAlert />
|
{authConfig.isMailerAutoconfirmEnabled ? (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : <SuccessAlert />
|
||||||
|
}
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={!showVerifyEmailAlert}>
|
<If condition={!showVerifyEmailAlert}>
|
||||||
@@ -53,7 +67,7 @@ export function EmailPasswordSignUpContainer({
|
|||||||
onSubmit={onSignupRequested}
|
onSubmit={onSignupRequested}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={displayTermsCheckbox}
|
displayTermsCheckbox={authConfig.displayTermsCheckbox}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ import { MagicLinkAuthContainer } from './magic-link-auth-container';
|
|||||||
import { OauthProviders } from './oauth-providers';
|
import { OauthProviders } from './oauth-providers';
|
||||||
import { PasswordSignInContainer } from './password-sign-in-container';
|
import { PasswordSignInContainer } from './password-sign-in-container';
|
||||||
|
|
||||||
|
export type Providers = {
|
||||||
|
password: boolean;
|
||||||
|
magicLink: boolean;
|
||||||
|
oAuth: Provider[];
|
||||||
|
};
|
||||||
|
|
||||||
export function SignInMethodsContainer(props: {
|
export function SignInMethodsContainer(props: {
|
||||||
inviteToken?: string;
|
inviteToken?: string;
|
||||||
|
|
||||||
@@ -25,11 +31,7 @@ export function SignInMethodsContainer(props: {
|
|||||||
updateAccount: string;
|
updateAccount: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
providers: {
|
providers: Providers;
|
||||||
password: boolean;
|
|
||||||
magicLink: boolean;
|
|
||||||
oAuth: Provider[];
|
|
||||||
};
|
|
||||||
}) {
|
}) {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import type { Provider } from '@supabase/supabase-js';
|
import type { Provider } from '@supabase/supabase-js';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { isBrowser } from '@kit/shared/utils';
|
import { isBrowser } from '@kit/shared/utils';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
@@ -21,15 +20,20 @@ export function SignUpMethodsContainer(props: {
|
|||||||
updateAccount: string;
|
updateAccount: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
authConfig: {
|
||||||
providers: {
|
providers: {
|
||||||
password: boolean;
|
password: boolean;
|
||||||
magicLink: boolean;
|
magicLink: boolean;
|
||||||
oAuth: Provider[];
|
oAuth: Provider[];
|
||||||
};
|
};
|
||||||
|
displayTermsCheckbox: boolean | undefined;
|
||||||
|
isMailerAutoconfirmEnabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
displayTermsCheckbox?: boolean;
|
|
||||||
inviteToken?: string;
|
inviteToken?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const redirectUrl = getCallbackUrl(props);
|
const redirectUrl = getCallbackUrl(props);
|
||||||
const defaultValues = getDefaultValues();
|
const defaultValues = getDefaultValues();
|
||||||
|
|
||||||
@@ -39,26 +43,33 @@ export function SignUpMethodsContainer(props: {
|
|||||||
<InviteAlert />
|
<InviteAlert />
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.password}>
|
<If condition={props.authConfig.providers.password}>
|
||||||
<EmailPasswordSignUpContainer
|
<EmailPasswordSignUpContainer
|
||||||
emailRedirectTo={props.paths.callback}
|
emailRedirectTo={props.paths.callback}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
authConfig={props.authConfig}
|
||||||
//onSignUp={() => redirect(redirectUrl)}
|
onSignUp={() => {
|
||||||
|
if (!props.authConfig.isMailerAutoconfirmEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
router.replace(props.paths.updateAccount)
|
||||||
|
}, 2_500);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.magicLink}>
|
<If condition={props.authConfig.providers.magicLink}>
|
||||||
<MagicLinkAuthContainer
|
<MagicLinkAuthContainer
|
||||||
inviteToken={props.inviteToken}
|
inviteToken={props.inviteToken}
|
||||||
redirectUrl={redirectUrl}
|
redirectUrl={redirectUrl}
|
||||||
shouldCreateUser={true}
|
shouldCreateUser={true}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
displayTermsCheckbox={props.displayTermsCheckbox}
|
displayTermsCheckbox={props.authConfig.displayTermsCheckbox}
|
||||||
/>
|
/>
|
||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={props.providers.oAuth.length}>
|
<If condition={props.authConfig.providers.oAuth.length}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute inset-0 flex items-center">
|
<div className="absolute inset-0 flex items-center">
|
||||||
<Separator />
|
<Separator />
|
||||||
@@ -72,7 +83,7 @@ export function SignUpMethodsContainer(props: {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OauthProviders
|
<OauthProviders
|
||||||
enabledProviders={props.providers.oAuth}
|
enabledProviders={props.authConfig.providers.oAuth}
|
||||||
inviteToken={props.inviteToken}
|
inviteToken={props.inviteToken}
|
||||||
shouldCreateUser={true}
|
shouldCreateUser={true}
|
||||||
paths={{
|
paths={{
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ export interface AccountSubmitData {
|
|||||||
email: string;
|
email: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
city?: string;
|
city?: string;
|
||||||
weight: number | null;
|
weight?: number | null | undefined;
|
||||||
height: number | null;
|
height?: number | null | undefined;
|
||||||
userConsent: boolean;
|
userConsent: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import "server-only"
|
import "server-only"
|
||||||
|
|
||||||
import { cookies as nextCookies } from "next/headers"
|
import { cookies as nextCookies } from "next/headers"
|
||||||
|
|
||||||
|
const CookieName = {
|
||||||
|
MEDUSA_CUSTOMER_ID: "_medusa_customer_id",
|
||||||
|
MEDUSA_JWT: "_medusa_jwt",
|
||||||
|
MEDUSA_CART_ID: "_medusa_cart_id",
|
||||||
|
MEDUSA_CACHE_ID: "_medusa_cache_id",
|
||||||
|
}
|
||||||
|
|
||||||
export const getAuthHeaders = async (): Promise<
|
export const getAuthHeaders = async (): Promise<
|
||||||
{ authorization: string } | {}
|
{ authorization: string } | {}
|
||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
const token = cookies.get("_medusa_jwt")?.value
|
const token = cookies.get(CookieName.MEDUSA_JWT)?.value
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return {}
|
return {}
|
||||||
@@ -23,7 +31,7 @@ export const getMedusaCustomerId = async (): Promise<
|
|||||||
> => {
|
> => {
|
||||||
try {
|
try {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
const customerId = cookies.get("_medusa_customer_id")?.value
|
const customerId = cookies.get(CookieName.MEDUSA_CUSTOMER_ID)?.value
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
return { customerId: null }
|
return { customerId: null }
|
||||||
@@ -31,14 +39,14 @@ export const getMedusaCustomerId = async (): Promise<
|
|||||||
|
|
||||||
return { customerId }
|
return { customerId }
|
||||||
} catch {
|
} catch {
|
||||||
return { customerId: null}
|
return { customerId: null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCacheTag = async (tag: string): Promise<string> => {
|
export const getCacheTag = async (tag: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
const cacheId = cookies.get("_medusa_cache_id")?.value
|
const cacheId = cookies.get(CookieName.MEDUSA_CACHE_ID)?.value
|
||||||
|
|
||||||
if (!cacheId) {
|
if (!cacheId) {
|
||||||
return ""
|
return ""
|
||||||
@@ -66,51 +74,51 @@ export const getCacheOptions = async (
|
|||||||
return { tags: [`${cacheTag}`] }
|
return { tags: [`${cacheTag}`] }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getCookieSharedOptions = () => ({
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
httpOnly: false,
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
});
|
||||||
|
const getCookieResetOptions = () => ({
|
||||||
|
maxAge: -1,
|
||||||
|
});
|
||||||
|
|
||||||
export const setAuthToken = async (token: string) => {
|
export const setAuthToken = async (token: string) => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
cookies.set("_medusa_jwt", token, {
|
cookies.set(CookieName.MEDUSA_JWT, token, {
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
...getCookieSharedOptions(),
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setMedusaCustomerId = async (customerId: string) => {
|
export const setMedusaCustomerId = async (customerId: string) => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
cookies.set("_medusa_customer_id", customerId, {
|
cookies.set(CookieName.MEDUSA_CUSTOMER_ID, customerId, {
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
...getCookieSharedOptions(),
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeAuthToken = async () => {
|
export const removeAuthToken = async () => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
cookies.set("_medusa_jwt", "", {
|
cookies.set(CookieName.MEDUSA_JWT, "", {
|
||||||
maxAge: -1,
|
...getCookieResetOptions(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCartId = async () => {
|
export const getCartId = async () => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
return cookies.get("_medusa_cart_id")?.value
|
return cookies.get(CookieName.MEDUSA_CART_ID)?.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setCartId = async (cartId: string) => {
|
export const setCartId = async (cartId: string) => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
cookies.set("_medusa_cart_id", cartId, {
|
cookies.set(CookieName.MEDUSA_CART_ID, cartId, {
|
||||||
maxAge: 60 * 60 * 24 * 7,
|
...getCookieSharedOptions(),
|
||||||
httpOnly: true,
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: process.env.NODE_ENV === "production",
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const removeCartId = async () => {
|
export const removeCartId = async () => {
|
||||||
const cookies = await nextCookies()
|
const cookies = await nextCookies()
|
||||||
cookies.set("_medusa_cart_id", "", {
|
cookies.set(CookieName.MEDUSA_CART_ID, "", {
|
||||||
maxAge: -1,
|
...getCookieResetOptions(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,18 +126,22 @@ export async function login(_currentState: unknown, formData: FormData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function medusaLogout(countryCode = 'ee') {
|
export async function medusaLogout(countryCode = 'ee', canRevalidateTags = true) {
|
||||||
await sdk.auth.logout()
|
await sdk.auth.logout()
|
||||||
|
|
||||||
await removeAuthToken()
|
await removeAuthToken()
|
||||||
|
|
||||||
|
if (canRevalidateTags) {
|
||||||
const customerCacheTag = await getCacheTag("customers")
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
revalidateTag(customerCacheTag)
|
revalidateTag(customerCacheTag)
|
||||||
|
}
|
||||||
|
|
||||||
await removeCartId()
|
await removeCartId()
|
||||||
|
|
||||||
|
if (canRevalidateTags) {
|
||||||
const cartCacheTag = await getCacheTag("carts")
|
const cartCacheTag = await getCacheTag("carts")
|
||||||
revalidateTag(cartCacheTag)
|
revalidateTag(cartCacheTag)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function transferCart() {
|
export async function transferCart() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function InfoTooltip({
|
|||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
{icon || <Info className="size-4 cursor-pointer" />}
|
{icon || <Info className="size-4 cursor-pointer" />}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className='sm:max-w-[400px]'>{content}</TooltipContent>
|
<TooltipContent className='max-w-[90vw] sm:max-w-[400px]'>{content}</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
144
packages/shared/src/config/auth-providers.service.ts
Normal file
144
packages/shared/src/config/auth-providers.service.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import type { Provider } from '@supabase/supabase-js';
|
||||||
|
import authConfig from './auth.config';
|
||||||
|
|
||||||
|
type SupabaseExternalProvider = Provider | 'email';
|
||||||
|
interface SupabaseAuthSettings {
|
||||||
|
external: Record<SupabaseExternalProvider, boolean>;
|
||||||
|
disable_signup: boolean;
|
||||||
|
mailer_autoconfirm: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthProvidersService {
|
||||||
|
private supabaseUrl: string;
|
||||||
|
private cache: Map<string, { data: SupabaseAuthSettings; timestamp: number }> = new Map();
|
||||||
|
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
constructor(supabaseUrl: string) {
|
||||||
|
this.supabaseUrl = supabaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAuthSettings(): Promise<SupabaseAuthSettings | null> {
|
||||||
|
try {
|
||||||
|
const cacheKey = 'auth-settings';
|
||||||
|
const cached = this.cache.get(cacheKey);
|
||||||
|
|
||||||
|
if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||||
|
if (!anonKey) {
|
||||||
|
throw new Error('NEXT_PUBLIC_SUPABASE_ANON_KEY is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${this.supabaseUrl}/auth/v1/settings?apikey=${anonKey}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.warn('Failed to fetch auth settings from Supabase:', response.status);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settings: SupabaseAuthSettings = await response.json();
|
||||||
|
|
||||||
|
this.cache.set(cacheKey, { data: settings, timestamp: Date.now() });
|
||||||
|
|
||||||
|
return settings;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Error fetching auth settings from Supabase:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isPasswordEnabled({ settings }: { settings: SupabaseAuthSettings | null }): boolean {
|
||||||
|
if (settings) {
|
||||||
|
return settings.external.email === true && !settings.disable_signup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.env.NEXT_PUBLIC_AUTH_PASSWORD === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
isMailerAutoconfirmEnabled({ settings }: { settings: SupabaseAuthSettings | null }): boolean {
|
||||||
|
return settings?.mailer_autoconfirm === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isMagicLinkEnabled(): boolean {
|
||||||
|
return process.env.NEXT_PUBLIC_AUTH_MAGIC_LINK === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
isOAuthProviderEnabled({
|
||||||
|
provider,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
provider: SupabaseExternalProvider;
|
||||||
|
settings: SupabaseAuthSettings | null;
|
||||||
|
}): boolean {
|
||||||
|
if (settings && settings.external) {
|
||||||
|
return settings.external[provider] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEnabledOAuthProviders({ settings }: { settings: SupabaseAuthSettings | null }): SupabaseExternalProvider[] {
|
||||||
|
const enabledProviders: SupabaseExternalProvider[] = [];
|
||||||
|
|
||||||
|
if (settings && settings.external) {
|
||||||
|
for (const [providerName, isEnabled] of Object.entries(settings.external)) {
|
||||||
|
if (isEnabled && providerName !== 'email') {
|
||||||
|
enabledProviders.push(providerName as SupabaseExternalProvider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return enabledProviders;
|
||||||
|
}
|
||||||
|
|
||||||
|
const potentialProviders: SupabaseExternalProvider[] = ['keycloak'];
|
||||||
|
const enabledFallback: SupabaseExternalProvider[] = [];
|
||||||
|
|
||||||
|
for (const provider of potentialProviders) {
|
||||||
|
if (provider !== 'email' && this.isOAuthProviderEnabled({ provider, settings })) {
|
||||||
|
enabledFallback.push(provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledFallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthConfig() {
|
||||||
|
const settings = await this.fetchAuthSettings();
|
||||||
|
const [passwordEnabled, magicLinkEnabled, oAuthProviders, isMailerAutoconfirmEnabled] = await Promise.all([
|
||||||
|
this.isPasswordEnabled({ settings }),
|
||||||
|
this.isMagicLinkEnabled(),
|
||||||
|
this.getEnabledOAuthProviders({ settings }),
|
||||||
|
this.isMailerAutoconfirmEnabled({ settings }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
providers: {
|
||||||
|
password: passwordEnabled,
|
||||||
|
magicLink: magicLinkEnabled,
|
||||||
|
oAuth: oAuthProviders,
|
||||||
|
},
|
||||||
|
displayTermsCheckbox: authConfig.displayTermsCheckbox,
|
||||||
|
isMailerAutoconfirmEnabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCache(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAuthProvidersService(): AuthProvidersService {
|
||||||
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
|
|
||||||
|
if (!supabaseUrl) {
|
||||||
|
throw new Error('NEXT_PUBLIC_SUPABASE_URL is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AuthProvidersService(supabaseUrl);
|
||||||
|
}
|
||||||
114
packages/shared/src/config/dynamic-auth.config.ts
Normal file
114
packages/shared/src/config/dynamic-auth.config.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import type { Provider } from '@supabase/supabase-js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { createAuthProvidersService } from './auth-providers.service';
|
||||||
|
|
||||||
|
const providers: z.ZodType<Provider> = getProviders();
|
||||||
|
|
||||||
|
const DynamicAuthConfigSchema = z.object({
|
||||||
|
providers: z.object({
|
||||||
|
password: z.boolean().describe('Enable password authentication.'),
|
||||||
|
magicLink: z.boolean().describe('Enable magic link authentication.'),
|
||||||
|
oAuth: providers.array(),
|
||||||
|
}),
|
||||||
|
displayTermsCheckbox: z.boolean().describe('Whether to display the terms checkbox during sign-up.'),
|
||||||
|
isMailerAutoconfirmEnabled: z.boolean().describe('Whether Supabase sends confirmation email automatically.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DynamicAuthConfig = {
|
||||||
|
providers: {
|
||||||
|
password: boolean;
|
||||||
|
magicLink: boolean;
|
||||||
|
oAuth: Provider[];
|
||||||
|
};
|
||||||
|
displayTermsCheckbox: boolean | undefined;
|
||||||
|
isMailerAutoconfirmEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getDynamicAuthConfig() {
|
||||||
|
const authService = createAuthProvidersService();
|
||||||
|
const dynamicProviders = await authService.getAuthConfig();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
providers: dynamicProviders.providers,
|
||||||
|
displayTermsCheckbox: dynamicProviders.displayTermsCheckbox,
|
||||||
|
isMailerAutoconfirmEnabled: dynamicProviders.isMailerAutoconfirmEnabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
return DynamicAuthConfigSchema.parse(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCachedAuthConfig() {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const cached = sessionStorage.getItem('auth-config');
|
||||||
|
if (cached) {
|
||||||
|
try {
|
||||||
|
const { data, timestamp } = JSON.parse(cached);
|
||||||
|
// Cache for 5 minutes
|
||||||
|
if (Date.now() - timestamp < 5 * 60 * 1000) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Invalid auth config cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = await getDynamicAuthConfig();
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem('auth-config', JSON.stringify({
|
||||||
|
data: config,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cache auth config:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServerAuthConfig() {
|
||||||
|
return getDynamicAuthConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isProviderEnabled(provider: 'password' | 'magicLink' | Provider): Promise<boolean> {
|
||||||
|
const authService = createAuthProvidersService();
|
||||||
|
const settings = await authService.fetchAuthSettings();
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'password':
|
||||||
|
return authService.isPasswordEnabled({ settings });
|
||||||
|
case 'magicLink':
|
||||||
|
return authService.isMagicLinkEnabled();
|
||||||
|
default:
|
||||||
|
return authService.isOAuthProviderEnabled({ provider, settings });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getProviders() {
|
||||||
|
return z.enum([
|
||||||
|
'apple',
|
||||||
|
'azure',
|
||||||
|
'bitbucket',
|
||||||
|
'discord',
|
||||||
|
'facebook',
|
||||||
|
'figma',
|
||||||
|
'github',
|
||||||
|
'gitlab',
|
||||||
|
'google',
|
||||||
|
'kakao',
|
||||||
|
'keycloak',
|
||||||
|
'linkedin',
|
||||||
|
'linkedin_oidc',
|
||||||
|
'notion',
|
||||||
|
'slack',
|
||||||
|
'spotify',
|
||||||
|
'twitch',
|
||||||
|
'twitter',
|
||||||
|
'workos',
|
||||||
|
'zoom',
|
||||||
|
'fly',
|
||||||
|
]);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createPath,
|
createPath,
|
||||||
getTeamAccountSidebarConfig,
|
getTeamAccountSidebarConfig,
|
||||||
} from './team-account-navigation.config';
|
} from './team-account-navigation.config';
|
||||||
|
import { DynamicAuthConfig, getCachedAuthConfig, getServerAuthConfig } from './dynamic-auth.config';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
appConfig,
|
appConfig,
|
||||||
@@ -18,4 +19,7 @@ export {
|
|||||||
getTeamAccountSidebarConfig,
|
getTeamAccountSidebarConfig,
|
||||||
pathsConfig,
|
pathsConfig,
|
||||||
personalAccountNavigationConfig,
|
personalAccountNavigationConfig,
|
||||||
|
getCachedAuthConfig,
|
||||||
|
getServerAuthConfig,
|
||||||
|
type DynamicAuthConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
export * from './use-csrf-token';
|
export * from './use-csrf-token';
|
||||||
export * from './use-current-locale-language-names';
|
export * from './use-current-locale-language-names';
|
||||||
|
export * from './use-auth-config';
|
||||||
|
|||||||
76
packages/shared/src/hooks/use-auth-config.ts
Normal file
76
packages/shared/src/hooks/use-auth-config.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { Provider } from '@supabase/supabase-js';
|
||||||
|
import { getCachedAuthConfig } from '../config/dynamic-auth.config';
|
||||||
|
import { authConfig } from '../config';
|
||||||
|
|
||||||
|
interface AuthConfig {
|
||||||
|
providers: {
|
||||||
|
password: boolean;
|
||||||
|
magicLink: boolean;
|
||||||
|
oAuth: Provider[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseAuthConfigResult {
|
||||||
|
config: AuthConfig | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
refetch: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuthConfig(): UseAuthConfigResult {
|
||||||
|
const [config, setConfig] = useState<AuthConfig | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const fetchConfig = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
const authConfig = await getCachedAuthConfig();
|
||||||
|
setConfig(authConfig);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch auth config', err);
|
||||||
|
setError(err instanceof Error ? err : new Error('Failed to fetch auth config'));
|
||||||
|
setConfig(authConfig);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchConfig();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
config,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refetch: fetchConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProviderEnabled(provider: 'password' | 'magicLink' | Provider) {
|
||||||
|
const { config, loading, error } = useAuthConfig();
|
||||||
|
|
||||||
|
const isEnabled = (() => {
|
||||||
|
if (!config) return false;
|
||||||
|
|
||||||
|
switch (provider) {
|
||||||
|
case 'password':
|
||||||
|
return config.providers.password;
|
||||||
|
case 'magicLink':
|
||||||
|
return config.providers.magicLink;
|
||||||
|
default:
|
||||||
|
return config.providers.oAuth.includes(provider);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: isEnabled,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import Isikukood, { Gender } from 'isikukood';
|
import Isikukood from 'isikukood';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the code is running in a browser environment.
|
* Check if the code is running in a browser environment.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function useSignOut() {
|
|||||||
try {
|
try {
|
||||||
try {
|
try {
|
||||||
const { medusaLogout } = await import('../../../features/medusa-storefront/src/lib/data/customer');
|
const { medusaLogout } = await import('../../../features/medusa-storefront/src/lib/data/customer');
|
||||||
await medusaLogout();
|
await medusaLogout(undefined, false);
|
||||||
} catch (medusaError) {
|
} catch (medusaError) {
|
||||||
console.warn('Medusa logout failed or not available:', medusaError);
|
console.warn('Medusa logout failed or not available:', medusaError);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ export function useUser(initialData?: User | null) {
|
|||||||
queryFn,
|
queryFn,
|
||||||
queryKey,
|
queryKey,
|
||||||
initialData,
|
initialData,
|
||||||
refetchInterval: false,
|
refetchInterval: 2_000,
|
||||||
refetchOnMount: false,
|
refetchOnMount: true,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
@@ -52,9 +53,13 @@ export function AppBreadcrumbs(props: {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLast = index === visiblePaths.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
<BreadcrumbItem className={'capitalize lg:text-xs'}>
|
<BreadcrumbItem className={clsx('lg:text-xs', {
|
||||||
|
'font-bold text-black': isLast,
|
||||||
|
})}>
|
||||||
<If
|
<If
|
||||||
condition={index < visiblePaths.length - 1}
|
condition={index < visiblePaths.length - 1}
|
||||||
fallback={label}
|
fallback={label}
|
||||||
@@ -64,6 +69,7 @@ export function AppBreadcrumbs(props: {
|
|||||||
'/' +
|
'/' +
|
||||||
splitPath.slice(0, splitPath.indexOf(path) + 1).join('/')
|
splitPath.slice(0, splitPath.indexOf(path) + 1).join('/')
|
||||||
}
|
}
|
||||||
|
className='text-muted-foreground'
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</BreadcrumbLink>
|
</BreadcrumbLink>
|
||||||
@@ -77,7 +83,7 @@ export function AppBreadcrumbs(props: {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<If condition={index !== visiblePaths.length - 1}>
|
<If condition={!isLast}>
|
||||||
<BreadcrumbSeparator />
|
<BreadcrumbSeparator />
|
||||||
</If>
|
</If>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) {
|
|||||||
>
|
>
|
||||||
{MobileNavigation}
|
{MobileNavigation}
|
||||||
|
|
||||||
<div className={'bg-background flex flex-1 flex-col px-4 pb-8 lg:px-0'}>
|
<div className={'bg-background flex flex-1 flex-col px-2 pb-8 lg:px-0'}>
|
||||||
{Children}
|
{Children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -106,7 +106,7 @@ export function PageBody(
|
|||||||
}>,
|
}>,
|
||||||
) {
|
) {
|
||||||
const className = cn(
|
const className = cn(
|
||||||
'flex w-full flex-1 flex-col space-y-6 lg:px-4',
|
'flex w-full flex-1 flex-col space-y-6',
|
||||||
props.className,
|
props.className,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -119,8 +119,8 @@ export function PageNavigation(props: React.PropsWithChildren) {
|
|||||||
|
|
||||||
export function PageDescription(props: React.PropsWithChildren) {
|
export function PageDescription(props: React.PropsWithChildren) {
|
||||||
return (
|
return (
|
||||||
<div className={'flex h-6 items-center'}>
|
<div className={'flex items-center'}>
|
||||||
<div className={'text-muted-foreground text-sm leading-none font-normal'}>
|
<div className={'text-muted-foreground text-sm font-normal leading-6'}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,7 +158,7 @@ export function PageHeader({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-center justify-between py-5 lg:px-4',
|
'flex items-center justify-between py-5',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -168,7 +168,7 @@ export function PageHeader({
|
|||||||
</If>
|
</If>
|
||||||
|
|
||||||
<If condition={displaySidebarTrigger || description}>
|
<If condition={displaySidebarTrigger || description}>
|
||||||
<div className="flex items-center gap-x-2.5">
|
<div className="flex items-center gap-3">
|
||||||
{displaySidebarTrigger ? (
|
{displaySidebarTrigger ? (
|
||||||
<SidebarTrigger className="text-muted-foreground hover:text-secondary-foreground hidden h-4.5 w-4.5 cursor-pointer lg:inline-flex" />
|
<SidebarTrigger className="text-muted-foreground hover:text-secondary-foreground hidden h-4.5 w-4.5 cursor-pointer lg:inline-flex" />
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const CardHeader: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<div className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
<div className={cn('flex flex-col space-y-1.5 p-4 sm:p-6', className)} {...props} />
|
||||||
);
|
);
|
||||||
CardHeader.displayName = 'CardHeader';
|
CardHeader.displayName = 'CardHeader';
|
||||||
|
|
||||||
@@ -60,14 +60,14 @@ CardDescription.displayName = 'CardDescription';
|
|||||||
const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => <div className={cn('p-6 pt-0', className)} {...props} />;
|
}) => <div className={cn('p-4 sm:p-6 pt-0', className)} {...props} />;
|
||||||
CardContent.displayName = 'CardContent';
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
const CardFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
const CardFooter: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}) => (
|
}) => (
|
||||||
<div className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
<div className={cn('flex items-center p-4 sm:p-6 pt-0', className)} {...props} />
|
||||||
);
|
);
|
||||||
CardFooter.displayName = 'CardFooter';
|
CardFooter.displayName = 'CardFooter';
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,10 @@
|
|||||||
"description": "Please enter your personal details to continue",
|
"description": "Please enter your personal details to continue",
|
||||||
"button": "Continue",
|
"button": "Continue",
|
||||||
"userConsentLabel": "I agree to the use of personal data on the platform",
|
"userConsentLabel": "I agree to the use of personal data on the platform",
|
||||||
"userConsentUrlTitle": "View privacy policy"
|
"userConsentUrlTitle": "View privacy policy",
|
||||||
|
"updateAccountLoading": "Updating account details...",
|
||||||
|
"updateAccountSuccess": "Account details updated",
|
||||||
|
"updateAccountError": "Updating account details error"
|
||||||
},
|
},
|
||||||
"consentModal": {
|
"consentModal": {
|
||||||
"title": "Before we start",
|
"title": "Before we start",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
|
"alreadyHaveAccountStatement": "I already have an account, I want to sign in instead",
|
||||||
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
|
"doNotHaveAccountStatement": "I do not have an account, I want to sign up instead",
|
||||||
"signInWithProvider": "Sign in with {{provider}}",
|
"signInWithProvider": "Sign in with {{provider}}",
|
||||||
|
"signInWithKeycloak": "Smart-ID/Mobile-ID/ID-card",
|
||||||
"signInWithPhoneNumber": "Sign in with Phone Number",
|
"signInWithPhoneNumber": "Sign in with Phone Number",
|
||||||
"signInWithEmail": "Sign in with Email",
|
"signInWithEmail": "Sign in with Email",
|
||||||
"signUpWithEmail": "Sign up with Email",
|
"signUpWithEmail": "Sign up with Email",
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
"description": "View your cart",
|
"description": "View your cart",
|
||||||
"emptyCartMessage": "Your cart is empty",
|
"emptyCartMessage": "Your cart is empty",
|
||||||
"emptyCartMessageDescription": "Add items to your cart to continue.",
|
"emptyCartMessageDescription": "Add items to your cart to continue.",
|
||||||
"subtotal": "Subtotal",
|
|
||||||
"total": "Total",
|
|
||||||
"promotionsTotal": "Promotions total",
|
|
||||||
"table": {
|
"table": {
|
||||||
"item": "Item",
|
"item": "Item",
|
||||||
"quantity": "Quantity",
|
"quantity": "Quantity",
|
||||||
@@ -25,13 +22,19 @@
|
|||||||
"timeoutAction": "Continue"
|
"timeoutAction": "Continue"
|
||||||
},
|
},
|
||||||
"discountCode": {
|
"discountCode": {
|
||||||
"title": "Gift card or promotion code",
|
"title": "Gift card or promo code",
|
||||||
"label": "Add Promotion Code(s)",
|
"label": "Add Promo Code(s)",
|
||||||
"apply": "Apply",
|
"apply": "Apply",
|
||||||
"subtitle": "If you wish, you can add a promotion code",
|
"subtitle": "If you wish, you can add a promo code",
|
||||||
"placeholder": "Enter promotion code",
|
"placeholder": "Enter promo code",
|
||||||
"remove": "Remove promotion code",
|
"remove": "Remove promo code",
|
||||||
"appliedCodes": "Promotion(s) applied:"
|
"appliedCodes": "Promotions(s) applied:",
|
||||||
|
"removeError": "Failed to remove promo code",
|
||||||
|
"removeSuccess": "Promo code removed",
|
||||||
|
"removeLoading": "Removing promo code...",
|
||||||
|
"addError": "Failed to add promo code",
|
||||||
|
"addSuccess": "Promo code added",
|
||||||
|
"addLoading": "Setting promo code..."
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"synlabAnalyses": {
|
"synlabAnalyses": {
|
||||||
@@ -52,7 +55,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"order": {
|
"order": {
|
||||||
"title": "Order"
|
"title": "Order",
|
||||||
|
"promotionsTotal": "Promotions total",
|
||||||
|
"subtotal": "Subtotal",
|
||||||
|
"total": "Total",
|
||||||
|
"giftCard": "Gift card"
|
||||||
},
|
},
|
||||||
"orderConfirmed": {
|
"orderConfirmed": {
|
||||||
"title": "Order confirmed",
|
"title": "Order confirmed",
|
||||||
|
|||||||
@@ -129,7 +129,9 @@
|
|||||||
"selectDate": "Select date"
|
"selectDate": "Select date"
|
||||||
},
|
},
|
||||||
"formFieldError": {
|
"formFieldError": {
|
||||||
"invalidPhoneNumber": "Please enter a valid Estonian phone number (must include country code +372)"
|
"invalidPhoneNumber": "Please enter a valid Estonian phone number (must include country code +372)",
|
||||||
|
"invalidPersonalCode": "Please enter a valid Estonian personal code",
|
||||||
|
"stringNonEmpty": "This field is required"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Your MedReport account balance",
|
"balance": "Your MedReport account balance",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Select analysis",
|
"title": "Select analysis",
|
||||||
"description": "All analysis results will appear within 1-3 days after the blood test.",
|
"description": "All analysis results will appear within 1-3 days after the blood test.",
|
||||||
"analysisNotAvailable": "Analysis is not available currently",
|
|
||||||
"analysisAddedToCart": "Analysis added to cart",
|
"analysisAddedToCart": "Analysis added to cart",
|
||||||
"analysisAddToCartError": "Adding analysis to cart failed"
|
"analysisAddToCartError": "Adding analysis to cart failed"
|
||||||
}
|
}
|
||||||
4
public/locales/en/order-health-analysis.json
Normal file
4
public/locales/en/order-health-analysis.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Order health analysis",
|
||||||
|
"description": "Select a suitable date and book your appointment time."
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Orders",
|
"title": "Orders",
|
||||||
"description": "View your orders",
|
"description": "View your orders",
|
||||||
|
"noOrders": "No orders found",
|
||||||
"table": {
|
"table": {
|
||||||
"analysisPackage": "Analysis package",
|
"analysisPackage": "Analysis package",
|
||||||
"otherOrders": "Order",
|
"otherOrders": "Order",
|
||||||
|
|||||||
@@ -130,7 +130,10 @@
|
|||||||
"description": "Jätkamiseks palun sisestage enda isikuandmed",
|
"description": "Jätkamiseks palun sisestage enda isikuandmed",
|
||||||
"button": "Jätka",
|
"button": "Jätka",
|
||||||
"userConsentLabel": "Nõustun isikuandmete kasutamisega platvormil",
|
"userConsentLabel": "Nõustun isikuandmete kasutamisega platvormil",
|
||||||
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid"
|
"userConsentUrlTitle": "Vaata isikuandmete töötlemise põhimõtteid",
|
||||||
|
"updateAccountLoading": "Konto andmed uuendatakse...",
|
||||||
|
"updateAccountSuccess": "Konto andmed uuendatud",
|
||||||
|
"updateAccountError": "Konto andmete uuendamine ebaõnnestus"
|
||||||
},
|
},
|
||||||
"consentModal": {
|
"consentModal": {
|
||||||
"title": "Enne alustamist",
|
"title": "Enne alustamist",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"signUpHeading": "Loo konto",
|
"signUpHeading": "Loo konto",
|
||||||
"signUp": "Loo konto",
|
"signUp": "Loo konto",
|
||||||
"signUpSubheading": "Täida allolev vorm, et luua konto.",
|
"signUpSubheading": "Täida allolev vorm, et luua konto.",
|
||||||
"signInHeading": "Logi oma kontole sisse",
|
"signInHeading": "Logi sisse",
|
||||||
"signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
|
"signInSubheading": "Tere tulemast tagasi! Palun sisesta oma andmed",
|
||||||
"signIn": "Logi sisse",
|
"signIn": "Logi sisse",
|
||||||
"getStarted": "Alusta",
|
"getStarted": "Alusta",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
|
"alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida",
|
||||||
"doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
|
"doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda",
|
||||||
"signInWithProvider": "Logi sisse teenusega {{provider}}",
|
"signInWithProvider": "Logi sisse teenusega {{provider}}",
|
||||||
|
"signInWithKeycloak": "Smart-ID/Mobiil-ID/ID-kaart",
|
||||||
"signInWithPhoneNumber": "Logi sisse telefoninumbriga",
|
"signInWithPhoneNumber": "Logi sisse telefoninumbriga",
|
||||||
"signInWithEmail": "Logi sisse e-posti aadressiga",
|
"signInWithEmail": "Logi sisse e-posti aadressiga",
|
||||||
"signUpWithEmail": "Registreeru e-posti aadressiga",
|
"signUpWithEmail": "Registreeru e-posti aadressiga",
|
||||||
@@ -68,7 +69,7 @@
|
|||||||
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
|
"acceptTermsAndConditions": "Ma nõustun <TermsOfServiceLink /> ja <PrivacyPolicyLink />",
|
||||||
"termsOfService": "Kasutustingimused",
|
"termsOfService": "Kasutustingimused",
|
||||||
"privacyPolicy": "Privaatsuspoliitika",
|
"privacyPolicy": "Privaatsuspoliitika",
|
||||||
"orContinueWith": "Või jätka koos",
|
"orContinueWith": "Või",
|
||||||
"redirecting": "Oled sees! Palun oota...",
|
"redirecting": "Oled sees! Palun oota...",
|
||||||
"errors": {
|
"errors": {
|
||||||
"Invalid login credentials": "Sisestatud andmed on valed",
|
"Invalid login credentials": "Sisestatud andmed on valed",
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
"description": "Vaata oma ostukorvi",
|
"description": "Vaata oma ostukorvi",
|
||||||
"emptyCartMessage": "Sinu ostukorv on tühi",
|
"emptyCartMessage": "Sinu ostukorv on tühi",
|
||||||
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
|
"emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.",
|
||||||
"subtotal": "Vahesumma",
|
|
||||||
"promotionsTotal": "Soodustuse summa",
|
|
||||||
"total": "Summa",
|
|
||||||
"table": {
|
"table": {
|
||||||
"item": "Toode",
|
"item": "Toode",
|
||||||
"quantity": "Kogus",
|
"quantity": "Kogus",
|
||||||
@@ -34,8 +31,10 @@
|
|||||||
"appliedCodes": "Rakendatud sooduskoodid:",
|
"appliedCodes": "Rakendatud sooduskoodid:",
|
||||||
"removeError": "Sooduskoodi eemaldamine ebaõnnestus",
|
"removeError": "Sooduskoodi eemaldamine ebaõnnestus",
|
||||||
"removeSuccess": "Sooduskood eemaldatud",
|
"removeSuccess": "Sooduskood eemaldatud",
|
||||||
|
"removeLoading": "Sooduskoodi eemaldamine",
|
||||||
"addError": "Sooduskoodi rakendamine ebaõnnestus",
|
"addError": "Sooduskoodi rakendamine ebaõnnestus",
|
||||||
"addSuccess": "Sooduskood rakendatud"
|
"addSuccess": "Sooduskood rakendatud",
|
||||||
|
"addLoading": "Rakendan sooduskoodi..."
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"synlabAnalyses": {
|
"synlabAnalyses": {
|
||||||
@@ -56,7 +55,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"order": {
|
"order": {
|
||||||
"title": "Tellimus"
|
"title": "Tellimus",
|
||||||
|
"promotionsTotal": "Soodustuse summa",
|
||||||
|
"subtotal": "Vahesumma",
|
||||||
|
"total": "Summa",
|
||||||
|
"giftCard": "Kinkekaart"
|
||||||
},
|
},
|
||||||
"orderConfirmed": {
|
"orderConfirmed": {
|
||||||
"title": "Tellimus on edukalt esitatud",
|
"title": "Tellimus on edukalt esitatud",
|
||||||
|
|||||||
@@ -129,7 +129,9 @@
|
|||||||
"selectDate": "Vali kuupäev"
|
"selectDate": "Vali kuupäev"
|
||||||
},
|
},
|
||||||
"formFieldError": {
|
"formFieldError": {
|
||||||
"invalidPhoneNumber": "Palun sisesta Eesti telefoninumber (peab sisaldama riigikoodi +372)"
|
"invalidPhoneNumber": "Palun sisesta Eesti telefoninumber (peab sisaldama riigikoodi +372)",
|
||||||
|
"invalidPersonalCode": "Palun sisesta Eesti isikukood",
|
||||||
|
"stringNonEmpty": "See väli on kohustuslik"
|
||||||
},
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Sinu MedReporti konto saldo",
|
"balance": "Sinu MedReporti konto saldo",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Vali analüüs",
|
"title": "Vali analüüs",
|
||||||
"description": "Kõikide analüüside tulemused ilmuvad 1–3 tööpäeva jooksul peale vere andmist.",
|
"description": "Kõikide analüüside tulemused ilmuvad 1–3 tööpäeva jooksul peale vere andmist.",
|
||||||
"analysisNotAvailable": "Analüüsi tellimine ei ole hetkel saadaval",
|
|
||||||
"analysisAddedToCart": "Analüüs lisatud ostukorvi",
|
"analysisAddedToCart": "Analüüs lisatud ostukorvi",
|
||||||
"analysisAddToCartError": "Analüüsi lisamine ostukorvi ebaõnnestus"
|
"analysisAddToCartError": "Analüüsi lisamine ostukorvi ebaõnnestus"
|
||||||
}
|
}
|
||||||
4
public/locales/et/order-health-analysis.json
Normal file
4
public/locales/et/order-health-analysis.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Telli terviseuuring",
|
||||||
|
"description": "Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg."
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Tellimused",
|
"title": "Tellimused",
|
||||||
"description": "Vaata oma tellimusi",
|
"description": "Vaata oma tellimusi",
|
||||||
|
"noOrders": "Tellimusi ei leitud",
|
||||||
"table": {
|
"table": {
|
||||||
"analysisPackage": "Analüüsi pakett",
|
"analysisPackage": "Analüüsi pakett",
|
||||||
"otherOrders": "Tellimus",
|
"otherOrders": "Tellimus",
|
||||||
|
|||||||
@@ -130,7 +130,10 @@
|
|||||||
"description": "Пожалуйста, введите личные данные для продолжения",
|
"description": "Пожалуйста, введите личные данные для продолжения",
|
||||||
"button": "Продолжить",
|
"button": "Продолжить",
|
||||||
"userConsentLabel": "Я согласен на использование персональных данных на платформе",
|
"userConsentLabel": "Я согласен на использование персональных данных на платформе",
|
||||||
"userConsentUrlTitle": "Посмотреть политику конфиденциальности"
|
"userConsentUrlTitle": "Посмотреть политику конфиденциальности",
|
||||||
|
"updateAccountLoading": "Обновление данных аккаунта...",
|
||||||
|
"updateAccountSuccess": "Данные аккаунта обновлены",
|
||||||
|
"updateAccountError": "Не удалось обновить данные аккаунта"
|
||||||
},
|
},
|
||||||
"consentModal": {
|
"consentModal": {
|
||||||
"title": "Перед началом",
|
"title": "Перед началом",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"alreadyHaveAccountStatement": "У меня уже есть аккаунт, я хочу войти",
|
"alreadyHaveAccountStatement": "У меня уже есть аккаунт, я хочу войти",
|
||||||
"doNotHaveAccountStatement": "У меня нет аккаунта, я хочу зарегистрироваться",
|
"doNotHaveAccountStatement": "У меня нет аккаунта, я хочу зарегистрироваться",
|
||||||
"signInWithProvider": "Войти через {{provider}}",
|
"signInWithProvider": "Войти через {{provider}}",
|
||||||
|
"signInWithKeycloak": "Smart-ID/Mobiil-ID/ID-kaart",
|
||||||
"signInWithPhoneNumber": "Войти по номеру телефона",
|
"signInWithPhoneNumber": "Войти по номеру телефона",
|
||||||
"signInWithEmail": "Войти по Email",
|
"signInWithEmail": "Войти по Email",
|
||||||
"signUpWithEmail": "Зарегистрироваться по Email",
|
"signUpWithEmail": "Зарегистрироваться по Email",
|
||||||
|
|||||||
@@ -3,8 +3,6 @@
|
|||||||
"description": "Просмотрите свою корзину",
|
"description": "Просмотрите свою корзину",
|
||||||
"emptyCartMessage": "Ваша корзина пуста",
|
"emptyCartMessage": "Ваша корзина пуста",
|
||||||
"emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.",
|
"emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.",
|
||||||
"subtotal": "Промежуточный итог",
|
|
||||||
"total": "Сумма",
|
|
||||||
"table": {
|
"table": {
|
||||||
"item": "Товар",
|
"item": "Товар",
|
||||||
"quantity": "Количество",
|
"quantity": "Количество",
|
||||||
@@ -33,8 +31,10 @@
|
|||||||
"appliedCodes": "Примененные промокоды:",
|
"appliedCodes": "Примененные промокоды:",
|
||||||
"removeError": "Не удалось удалить промокод",
|
"removeError": "Не удалось удалить промокод",
|
||||||
"removeSuccess": "Промокод удален",
|
"removeSuccess": "Промокод удален",
|
||||||
|
"removeLoading": "Удаление промокода...",
|
||||||
"addError": "Не удалось применить промокод",
|
"addError": "Не удалось применить промокод",
|
||||||
"addSuccess": "Промокод применен"
|
"addSuccess": "Промокод применен",
|
||||||
|
"addLoading": "Применение промокода..."
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
"synlabAnalyses": {
|
"synlabAnalyses": {
|
||||||
@@ -55,7 +55,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"order": {
|
"order": {
|
||||||
"title": "Заказ"
|
"title": "Заказ",
|
||||||
|
"promotionsTotal": "Скидка",
|
||||||
|
"subtotal": "Промежуточный итог",
|
||||||
|
"total": "Сумма",
|
||||||
|
"giftCard": "Подарочная карта"
|
||||||
},
|
},
|
||||||
"orderConfirmed": {
|
"orderConfirmed": {
|
||||||
"title": "Заказ успешно оформлен",
|
"title": "Заказ успешно оформлен",
|
||||||
|
|||||||
@@ -128,6 +128,11 @@
|
|||||||
"amount": "Сумма",
|
"amount": "Сумма",
|
||||||
"selectDate": "Выберите дату"
|
"selectDate": "Выберите дату"
|
||||||
},
|
},
|
||||||
|
"formFieldError": {
|
||||||
|
"invalidPhoneNumber": "Пожалуйста, введите действительный номер телефона (должен включать код страны +372)",
|
||||||
|
"invalidPersonalCode": "Пожалуйста, введите действительный персональный код",
|
||||||
|
"stringNonEmpty": "Это поле обязательно"
|
||||||
|
},
|
||||||
"wallet": {
|
"wallet": {
|
||||||
"balance": "Баланс вашего счета MedReport",
|
"balance": "Баланс вашего счета MedReport",
|
||||||
"expiredAt": "Действительно до {{expiredAt}}"
|
"expiredAt": "Действительно до {{expiredAt}}"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Выберите анализ",
|
"title": "Выберите анализ",
|
||||||
"description": "Результаты всех анализов будут доступны в течение 1–3 рабочих дней после сдачи крови.",
|
"description": "Результаты всех анализов будут доступны в течение 1–3 рабочих дней после сдачи крови.",
|
||||||
"analysisNotAvailable": "Заказ анализа в данный момент недоступен",
|
|
||||||
"analysisAddedToCart": "Анализ добавлен в корзину",
|
"analysisAddedToCart": "Анализ добавлен в корзину",
|
||||||
"analysisAddToCartError": "Не удалось добавить анализ в корзину"
|
"analysisAddToCartError": "Не удалось добавить анализ в корзину"
|
||||||
}
|
}
|
||||||
4
public/locales/ru/order-health-analysis.json
Normal file
4
public/locales/ru/order-health-analysis.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"title": "Заказать анализ здоровья",
|
||||||
|
"description": "Выберите подходящую дату и забронируйте время для вашего приёма."
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"title": "Заказы",
|
"title": "Заказы",
|
||||||
"description": "Просмотрите ваши заказы",
|
"description": "Просмотрите ваши заказы",
|
||||||
|
"noOrders": "Заказы не найдены",
|
||||||
"table": {
|
"table": {
|
||||||
"analysisPackage": "Пакет анализов",
|
"analysisPackage": "Пакет анализов",
|
||||||
"otherOrders": "Заказ",
|
"otherOrders": "Заказ",
|
||||||
|
|||||||
Reference in New Issue
Block a user