diff --git a/.env b/.env index e58a457..8367ad9 100644 --- a/.env +++ b/.env @@ -68,3 +68,6 @@ NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY= # Configure Medusa password secret for Keycloak users MEDUSA_PASSWORD_SECRET=ODEwMGNiMmUtOGMxYS0xMWYwLWJlZDYtYTM3YzYyMWY0NGEzCg== + +# False by default +MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK=false diff --git a/.env.development b/.env.development index c92a206..2cc0b56 100644 --- a/.env.development +++ b/.env.development @@ -26,6 +26,22 @@ EMAIL_PORT=1025 # or 465 for SSL EMAIL_TLS=false 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_BACKEND_URL=http://localhost:9000 MEDUSA_BACKEND_PUBLIC_URL=http://localhost:9000 diff --git a/app/(marketing)/_components/site-header-account-section.tsx b/app/(marketing)/_components/site-header-account-section.tsx index ff7ad03..dd62722 100644 --- a/app/(marketing)/_components/site-header-account-section.tsx +++ b/app/(marketing)/_components/site-header-account-section.tsx @@ -13,7 +13,8 @@ import { Button } from '@kit/ui/button'; import { If } from '@kit/ui/if'; 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(() => import('@kit/ui/mode-toggle').then((mod) => ({ @@ -57,6 +58,8 @@ export function SiteHeaderAccountSection({ } function AuthButtons() { + const { config } = useAuthConfig(); + return (
@@ -65,21 +68,25 @@ function AuthButtons() {
-
- + {config && ( +
+ {(config.providers.password || config.providers.oAuth.length > 0) && ( + + )} - {authConfig.providers.password && ( - - )} -
+ {config.providers.password && ( + + )} +
+ )}
); } diff --git a/app/auth/sign-in/components/PasswordOption.tsx b/app/auth/sign-in/components/PasswordOption.tsx index 5ef56a4..4f0b59d 100644 --- a/app/auth/sign-in/components/PasswordOption.tsx +++ b/app/auth/sign-in/components/PasswordOption.tsx @@ -1,7 +1,7 @@ import Link from 'next/link'; -import { SignInMethodsContainer } from '@kit/auth/sign-in'; -import { authConfig, pathsConfig } from '@kit/shared/config'; +import { Providers, SignInMethodsContainer } from '@kit/auth/sign-in'; +import { pathsConfig } from '@kit/shared/config'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; @@ -9,9 +9,11 @@ import { Trans } from '@kit/ui/trans'; export default function PasswordOption({ inviteToken, returnPath, + providers, }: { inviteToken?: string; returnPath?: string; + providers: Providers; }) { const signUpPath = pathsConfig.auth.signUp + @@ -39,7 +41,7 @@ export default function PasswordOption({
diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index a82d4a7..4799f7d 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -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 { withI18n } from '~/lib/i18n/with-i18n'; @@ -24,11 +24,23 @@ async function SignInPage({ searchParams }: SignInPageProps) { const { invite_token: inviteToken, next: returnPath = pathsConfig.app.home } = await searchParams; + const authConfig = await getServerAuthConfig(); + if (authConfig.providers.password) { - return ; + return ( + + ); } - return ; + if (authConfig.providers.oAuth.includes('keycloak')) { + return ; + } + + return null; } export default withI18n(SignInPage); diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 0394078..cdee3a5 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -2,7 +2,7 @@ import Link from 'next/link'; import { redirect } from 'next/navigation'; 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 { Heading } from '@kit/ui/heading'; import { Trans } from '@kit/ui/trans'; @@ -38,6 +38,8 @@ async function SignUpPage({ searchParams }: Props) { pathsConfig.auth.signIn + (inviteToken ? `?invite_token=${inviteToken}` : ''); + const authConfig = await getServerAuthConfig(); + if (!authConfig.providers.password) { return redirect('/'); } @@ -55,8 +57,7 @@ async function SignUpPage({ searchParams }: Props) {
diff --git a/app/auth/update-account/_components/update-account-form.tsx b/app/auth/update-account/_components/update-account-form.tsx index bdda351..b807745 100644 --- a/app/auth/update-account/_components/update-account-form.tsx +++ b/app/auth/update-account/_components/update-account-form.tsx @@ -1,6 +1,9 @@ 'use client'; 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 { zodResolver } from '@hookform/resolvers/zod'; @@ -19,44 +22,70 @@ import { import { Input } from '@kit/ui/input'; 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 { z } from 'zod'; +import { toast } from '@kit/ui/sonner'; +import { pathsConfig } from '@/packages/shared/src/config'; -type UpdateAccountFormValues = z.infer; +type UpdateAccountFormValues = z.infer>; export function UpdateAccountForm({ defaultValues, + isEmailUser, }: { defaultValues: UpdateAccountFormValues, + isEmailUser: boolean, }) { + const router = useRouter(); + const { t } = useTranslation('account'); + const form = useForm({ - resolver: zodResolver(UpdateAccountSchema), + resolver: zodResolver(UpdateAccountSchemaClient({ isEmailUser })), mode: 'onChange', 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 hasLastName = !!lastName; const hasPersonalCode = !!personalCode; const hasEmail = !!email; - const hasWeight = !!weight; - const hasHeight = !!height; - const hasUserConsent = !!userConsent; - const onUpdateAccountOptions = async (values: UpdateAccountFormValues) => - onUpdateAccount({ - ...values, - ...(hasFirstName && { firstName }), - ...(hasLastName && { lastName }), - ...(hasPersonalCode && { personalCode }), - ...(hasEmail && { email }), - ...(hasWeight && { weight: values.weight ?? weight }), - ...(hasHeight && { height: values.height ?? height }), - ...(hasUserConsent && { userConsent: values.userConsent ?? userConsent }), - }); + const onUpdateAccountOptions = async (values: UpdateAccountFormValues) => { + const loading = toast.loading(t('updateAccount.updateAccountLoading')); + try { + const response = await onUpdateAccount({ + firstName: values.firstName || firstName, + lastName: values.lastName || lastName, + personalCode: values.personalCode || personalCode, + email: values.email || email, + phone: values.phone, + 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 (
@@ -66,14 +95,14 @@ export function UpdateAccountForm({ > ( - + @@ -82,14 +111,14 @@ export function UpdateAccountForm({ ( - + @@ -98,7 +127,7 @@ export function UpdateAccountForm({ ( @@ -143,72 +172,76 @@ export function UpdateAccountForm({ )} /> - ( - - - - - - - - - - )} - /> + {!isEmailUser && ( + <> + ( + + + + + + + + + + )} + /> -
- ( - - - - - - - field.onChange( - e.target.value === '' ? null : Number(e.target.value), - ) - } - /> - - - - )} - /> +
+ ( + + + + + + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> + + + + )} + /> - ( - - - - - - - field.onChange( - e.target.value === '' ? null : Number(e.target.value), - ) - } - /> - - - - )} - /> -
+ ( + + + + + + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> + + + + )} + /> +
+ + )} { + try { + return new Isikukood(val).validate(); + } catch { + return false; + } + }, + { + message: 'common:formFieldError.invalidPersonalCode', + }, + ), email: z.string().email({ message: 'Email is required', }), phone: z .string({ - error: 'Phone number is required', + error: 'error:invalidPhone', }) .nonempty() .refine( @@ -59,4 +69,34 @@ export const UpdateAccountSchema = z.object({ userConsent: z.boolean().refine((val) => val === 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, }); diff --git a/app/auth/update-account/_lib/server/update-account.ts b/app/auth/update-account/_lib/server/update-account.ts index ff9a80d..07e415b 100644 --- a/app/auth/update-account/_lib/server/update-account.ts +++ b/app/auth/update-account/_lib/server/update-account.ts @@ -1,7 +1,5 @@ 'use server'; -import { redirect } from 'next/navigation'; - import { updateCustomer } from '@lib/data/customer'; 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 { UpdateAccountSchema } from '../schemas/update-account.schema'; +import { UpdateAccountSchemaServer } from '../schemas/update-account.schema'; export const onUpdateAccount = enhanceAction( async (params: AccountSubmitData) => { @@ -40,14 +37,11 @@ export const onUpdateAccount = enhanceAction( const hasUnseenMembershipConfirmation = await api.hasUnseenMembershipConfirmation(); - - if (hasUnseenMembershipConfirmation) { - redirect(pathsConfig.auth.membershipConfirmation); - } else { - redirect(pathsConfig.app.selectPackage); + return { + hasUnseenMembershipConfirmation, } }, { - schema: UpdateAccountSchema, + schema: UpdateAccountSchemaServer, }, ); diff --git a/app/auth/update-account/page.tsx b/app/auth/update-account/page.tsx index a40b13e..031120c 100644 --- a/app/auth/update-account/page.tsx +++ b/app/auth/update-account/page.tsx @@ -1,7 +1,6 @@ import { redirect } from 'next/navigation'; 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 { 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'; async function UpdateAccount() { - const client = getSupabaseServerClient(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); - const { - data: { user }, - } = await client.auth.getUser(); const isKeycloakUser = user?.app_metadata?.provider === 'keycloak'; + const isEmailUser = user?.app_metadata?.provider === 'email'; if (!user) { redirect(pathsConfig.auth.signIn); @@ -55,7 +51,7 @@ async function UpdateAccount() {

- +
diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx index 9ce91e8..c69d0b6 100644 --- a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +import { redirect } from 'next/navigation'; import { ButtonTooltip } from '@kit/shared/components/ui/button-tooltip'; import { pathsConfig } from '@kit/shared/config'; @@ -22,14 +23,15 @@ export default async function AnalysisResultsPage({ id: string; }>; }) { - const account = await loadCurrentUserAccount(); - 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) { - return null; + if (!account?.id) { + return redirect("/"); } await createPageViewLog({ @@ -37,6 +39,19 @@ export default async function AnalysisResultsPage({ action: PageViewAction.VIEW_ANALYSIS_RESULTS, }); + if (!analysisResponse) { + return ( + <> + } + description={} + /> + + + + ); + } + return ( <> diff --git a/app/home/(user)/(dashboard)/booking/page.tsx b/app/home/(user)/(dashboard)/booking/page.tsx index 680edfb..94e2b2f 100644 --- a/app/home/(user)/(dashboard)/booking/page.tsx +++ b/app/home/(user)/(dashboard)/booking/page.tsx @@ -28,9 +28,13 @@ function BookingPage() { return ( <> -

+ } + description={} + /> +

-

+

); } diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 89c8dc3..f705399 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -30,10 +30,15 @@ const env = () => z error: 'NEXT_PUBLIC_SITE_URL is required', }) .min(1), + isEnabledDispatchOnMontonioCallback: z + .boolean({ + error: 'MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK is required', + }), }) .parse({ emailSender: process.env.EMAIL_SENDER, siteUrl: process.env.NEXT_PUBLIC_SITE_URL!, + isEnabledDispatchOnMontonioCallback: process.env.MEDIPOST_ENABLE_DISPATCH_ON_MONTONIO_CALLBACK === 'true', }); const sendEmail = async ({ @@ -163,7 +168,7 @@ async function sendAnalysisPackageOrderEmail({ } export async function processMontonioCallback(orderToken: string) { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); if (!account) { 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); } - await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); + if (env().isEnabledDispatchOnMontonioCallback) { + await sendOrderToMedipost({ medusaOrderId, orderedAnalysisElements }); + } return { success: true, orderId }; } catch (error) { diff --git a/app/home/(user)/(dashboard)/cart/page.tsx b/app/home/(user)/(dashboard)/cart/page.tsx index 250412b..1bb2675 100644 --- a/app/home/(user)/(dashboard)/cart/page.tsx +++ b/app/home/(user)/(dashboard)/cart/page.tsx @@ -8,6 +8,7 @@ import Cart from '../../_components/cart'; import { listProductTypes } from '@lib/data/products'; import CartTimer from '../../_components/cart/cart-timer'; import { Trans } from '@kit/ui/trans'; +import { withI18n } from '~/lib/i18n/with-i18n'; export async function generateMetadata() { 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) => { - console.error(error); + console.error("Failed to retrieve cart", error); return notFound(); }); @@ -50,3 +51,5 @@ export default async function CartPage() { ); } + +export default withI18n(CartPage); diff --git a/app/home/(user)/(dashboard)/order-analysis/page.tsx b/app/home/(user)/(dashboard)/order-analysis/page.tsx index 1736dc9..1269bde 100644 --- a/app/home/(user)/(dashboard)/order-analysis/page.tsx +++ b/app/home/(user)/(dashboard)/order-analysis/page.tsx @@ -18,7 +18,7 @@ export const generateMetadata = async () => { }; async function OrderAnalysisPage() { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } diff --git a/app/home/(user)/(dashboard)/order-health-analysis/page.tsx b/app/home/(user)/(dashboard)/order-health-analysis/page.tsx index a4a4478..432ca39 100644 --- a/app/home/(user)/(dashboard)/order-health-analysis/page.tsx +++ b/app/home/(user)/(dashboard)/order-health-analysis/page.tsx @@ -21,6 +21,9 @@ async function OrderHealthAnalysisPage() { description={} /> +

+ +

); diff --git a/app/home/(user)/(dashboard)/order/page.tsx b/app/home/(user)/(dashboard)/order/page.tsx index 5bb1ae7..48faae3 100644 --- a/app/home/(user)/(dashboard)/order/page.tsx +++ b/app/home/(user)/(dashboard)/order/page.tsx @@ -61,6 +61,11 @@ async function OrdersPage() { ) })} + {analysisOrders.length === 0 && ( +
+ +
+ )} ); diff --git a/app/home/(user)/(dashboard)/page.tsx b/app/home/(user)/(dashboard)/page.tsx index dc52fc1..ca9fc22 100644 --- a/app/home/(user)/(dashboard)/page.tsx +++ b/app/home/(user)/(dashboard)/page.tsx @@ -26,7 +26,7 @@ export const generateMetadata = async () => { async function UserHomePage() { const client = getSupabaseServerClient(); - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); const api = createAccountsApi(client); const bmiThresholds = await api.fetchBmiThresholds(); diff --git a/app/home/(user)/_components/cart/analysis-location.tsx b/app/home/(user)/_components/cart/analysis-location.tsx index 9e1ab16..99355dd 100644 --- a/app/home/(user)/_components/cart/analysis-location.tsx +++ b/app/home/(user)/_components/cart/analysis-location.tsx @@ -55,11 +55,15 @@ export default function AnalysisLocation({ cart, synlabAnalyses }: { cart: Store } return ( -
+
+

+ +

+ onSubmit(data))} - className="w-full mb-2 flex gap-x-2" + className="w-full mb-2 flex gap-x-2 flex-1" > + @@ -150,7 +152,11 @@ export default function AccountSettingsForm({ - + diff --git a/app/home/(user)/settings/_lib/account-settings.schema.ts b/app/home/(user)/settings/_lib/account-settings.schema.ts index a6944a4..8c3cece 100644 --- a/app/home/(user)/settings/_lib/account-settings.schema.ts +++ b/app/home/(user)/settings/_lib/account-settings.schema.ts @@ -12,8 +12,8 @@ export const accountSettingsSchema = z.object({ email: z.email({ error: 'error:invalidEmail' }).nullable(), phone: z.e164({ error: 'error:invalidPhone' }), accountParams: z.object({ - height: z.coerce.number({ error: 'error:invalidNumber' }), - weight: z.coerce.number({ error: 'error:invalidNumber' }), + height: z.coerce.number({ error: 'error:invalidNumber' }).gt(0), + weight: z.coerce.number({ error: 'error:invalidNumber' }).gt(0), isSmoker: z.boolean().optional().nullable(), }), }); diff --git a/app/home/(user)/settings/page.tsx b/app/home/(user)/settings/page.tsx index c1e81e1..bf65423 100644 --- a/app/home/(user)/settings/page.tsx +++ b/app/home/(user)/settings/page.tsx @@ -17,7 +17,7 @@ export const generateMetadata = async () => { }; async function PersonalAccountSettingsPage() { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); return (
diff --git a/app/home/(user)/settings/preferences/page.tsx b/app/home/(user)/settings/preferences/page.tsx index ec55fd6..4c0faeb 100644 --- a/app/home/(user)/settings/preferences/page.tsx +++ b/app/home/(user)/settings/preferences/page.tsx @@ -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 AccountPreferencesForm from '../_components/account-preferences-form'; import SettingsSectionHeader from '../_components/settings-section-header'; export default async function PreferencesPage() { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); return (
@@ -16,7 +12,6 @@ export default async function PreferencesPage() { titleKey="account:preferencesTabLabel" descriptionKey="account:preferencesTabDescription" /> -
diff --git a/app/home/layout.tsx b/app/home/layout.tsx index c483aec..088a68c 100644 --- a/app/home/layout.tsx +++ b/app/home/layout.tsx @@ -1,4 +1,3 @@ -import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component'; import { createAccountsApi } from '@/packages/features/accounts/src/server/api'; import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; @@ -12,8 +11,7 @@ export default async function HomeLayout({ }) { const client = getSupabaseServerClient(); - const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); const api = createAccountsApi(client); const hasAccountTeamMembership = await api.hasAccountTeamMembership( diff --git a/app/select-package/page.tsx b/app/select-package/page.tsx index b45f213..69bb098 100644 --- a/app/select-package/page.tsx +++ b/app/select-package/page.tsx @@ -14,6 +14,7 @@ import { withI18n } from '~/lib/i18n/with-i18n'; import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal'; import { loadAnalysisPackages } from '../home/(user)/_lib/server/load-analysis-packages'; +import { redirect } from 'next/navigation'; export const generateMetadata = async () => { const { t } = await createI18nServerInstance(); @@ -27,6 +28,10 @@ async function SelectPackagePage() { const { analysisPackageElements, analysisPackages, countryCode } = await loadAnalysisPackages(); + if (analysisPackageElements.length === 0) { + return redirect(pathsConfig.app.home); + } + return (
diff --git a/lib/i18n/i18n.settings.ts b/lib/i18n/i18n.settings.ts index b970b7a..7bb07a8 100644 --- a/lib/i18n/i18n.settings.ts +++ b/lib/i18n/i18n.settings.ts @@ -37,6 +37,7 @@ export const defaultI18nNamespaces = [ 'booking', 'order-analysis-package', 'order-analysis', + 'order-health-analysis', 'cart', 'orders', 'analysis-results', diff --git a/lib/services/medipostTest.service.ts b/lib/services/medipostTest.service.ts index f4393f7..4459d0c 100644 --- a/lib/services/medipostTest.service.ts +++ b/lib/services/medipostTest.service.ts @@ -100,7 +100,7 @@ export async function composeOrderTestResponseXML({ return ` - ${getPais(USER, RECIPIENT, orderCreatedAt, orderId, "AL")} + ${getPais(USER, RECIPIENT, orderId, "AL")} ${orderId} ${getClientInstitution({ index: 1 })} diff --git a/lib/services/medipostXML.service.ts b/lib/services/medipostXML.service.ts index 3b55506..2cac69b 100644 --- a/lib/services/medipostXML.service.ts +++ b/lib/services/medipostXML.service.ts @@ -184,7 +184,7 @@ export async function composeOrderXML({ return ` - ${getPais(USER, RECIPIENT, orderCreatedAt, orderId)} + ${getPais(USER, RECIPIENT, orderId)} ${orderId} ${getClientInstitution()} diff --git a/lib/services/medusaCart.service.ts b/lib/services/medusaCart.service.ts index a416e04..c33ed0d 100644 --- a/lib/services/medusaCart.service.ts +++ b/lib/services/medusaCart.service.ts @@ -38,8 +38,7 @@ export async function handleAddToCart({ countryCode: string; }) { const supabase = getSupabaseServerClient(); - const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } @@ -70,8 +69,7 @@ export async function handleDeleteCartItem({ lineId }: { lineId: string }) { const supabase = getSupabaseServerClient(); const cartId = await getCartId(); - const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } @@ -96,8 +94,7 @@ export async function handleNavigateToPayment({ paymentSessionId: string; }) { const supabase = getSupabaseServerClient(); - const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } @@ -137,8 +134,7 @@ export async function handleLineItemTimeout({ lineItem: StoreCartLineItem; }) { const supabase = getSupabaseServerClient(); - const user = await requireUserInServerComponent(); - const account = await loadCurrentUserAccount(); + const { account, user } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } diff --git a/lib/templates/medipost-order.ts b/lib/templates/medipost-order.ts index 19e5b79..e226774 100644 --- a/lib/templates/medipost-order.ts +++ b/lib/templates/medipost-order.ts @@ -19,7 +19,7 @@ export const getPais = ( ${packageName} ${sender} ${recipient} - ${format(createdAt, DATE_TIME_FORMAT)} + ${format(new Date(), DATE_TIME_FORMAT)} ${orderId} info@medreport.ee `; diff --git a/lib/utils.ts b/lib/utils.ts index d8fa393..233042a 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -126,7 +126,7 @@ export default class PersonalCode { if (age >= 60) { return '60'; } - throw new Error('Age range not supported'); + throw new Error('Age range not supported, age=' + age); })(); const gender = (() => { const gender = parsed.getGender(); diff --git a/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx index 6c302ff..b485d95 100644 --- a/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx +++ b/packages/features/accounts/src/components/personal-account-settings/mfa/multi-factor-auth-setup-dialog.tsx @@ -265,11 +265,13 @@ function FactorQrCode({ z.object({ factorName: z.string().min(1), qrCode: z.string().min(1), + totpSecret: z.string().min(1), }), ), defaultValues: { factorName: '', qrCode: '', + totpSecret: '', }, }); @@ -319,6 +321,7 @@ function FactorQrCode({ if (data.type === 'totp') { form.setValue('factorName', name); form.setValue('qrCode', data.totp.qr_code); + form.setValue('totpSecret', data.totp.secret); } // dispatch event to set factor ID @@ -331,7 +334,7 @@ function FactorQrCode({ return (

@@ -343,6 +346,10 @@ function FactorQrCode({

+ +

+ {form.getValues('totpSecret')} +

); } diff --git a/packages/features/admin/src/lib/server/schema/create-company.schema.ts b/packages/features/admin/src/lib/server/schema/create-company.schema.ts index 42ef6cb..c2d33cf 100644 --- a/packages/features/admin/src/lib/server/schema/create-company.schema.ts +++ b/packages/features/admin/src/lib/server/schema/create-company.schema.ts @@ -10,7 +10,7 @@ const personalCodeSchema = z.string().refine( } }, { - message: 'Invalid personal code', + message: 'common:formFieldError.invalidPersonalCode', }, ); diff --git a/packages/features/auth/src/components/auth-layout.tsx b/packages/features/auth/src/components/auth-layout.tsx index 2d83e61..003da16 100644 --- a/packages/features/auth/src/components/auth-layout.tsx +++ b/packages/features/auth/src/components/auth-layout.tsx @@ -7,9 +7,8 @@ export function AuthLayoutShell({ return (
{Logo ? : null} diff --git a/packages/features/auth/src/components/oauth-providers.tsx b/packages/features/auth/src/components/oauth-providers.tsx index 11dcc94..2bb7830 100644 --- a/packages/features/auth/src/components/oauth-providers.tsx +++ b/packages/features/auth/src/components/oauth-providers.tsx @@ -113,12 +113,16 @@ export const OauthProviders: React.FC<{ ); }} > - + {provider === 'keycloak' ? ( + + ) : ( + + )} ); })} diff --git a/packages/features/auth/src/components/password-sign-up-container.tsx b/packages/features/auth/src/components/password-sign-up-container.tsx index 5cbe21a..631c7a5 100644 --- a/packages/features/auth/src/components/password-sign-up-container.tsx +++ b/packages/features/auth/src/components/password-sign-up-container.tsx @@ -10,9 +10,18 @@ import { useCaptchaToken } from '../captcha/client'; import { usePasswordSignUpFlow } from '../hooks/use-sign-up-flow'; import { AuthErrorAlert } from './auth-error-alert'; import { PasswordSignUpForm } from './password-sign-up-form'; +import { Spinner } from '@kit/ui/makerkit/spinner'; interface EmailPasswordSignUpContainerProps { - displayTermsCheckbox?: boolean; + authConfig: { + providers: { + password: boolean; + magicLink: boolean; + oAuth: string[]; + }; + displayTermsCheckbox: boolean | undefined; + isMailerAutoconfirmEnabled: boolean; + }; defaultValues?: { email: string; }; @@ -21,10 +30,10 @@ interface EmailPasswordSignUpContainerProps { } export function EmailPasswordSignUpContainer({ + authConfig, defaultValues, onSignUp, emailRedirectTo, - displayTermsCheckbox, }: EmailPasswordSignUpContainerProps) { const { captchaToken, resetCaptchaToken } = useCaptchaToken(); @@ -43,7 +52,12 @@ export function EmailPasswordSignUpContainer({ return ( <> - + {authConfig.isMailerAutoconfirmEnabled ? ( +
+ +
+ ) : + }
@@ -53,7 +67,7 @@ export function EmailPasswordSignUpContainer({ onSubmit={onSignupRequested} loading={loading} defaultValues={defaultValues} - displayTermsCheckbox={displayTermsCheckbox} + displayTermsCheckbox={authConfig.displayTermsCheckbox} /> diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index 344c040..6b1e8c7 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -15,6 +15,12 @@ import { MagicLinkAuthContainer } from './magic-link-auth-container'; import { OauthProviders } from './oauth-providers'; import { PasswordSignInContainer } from './password-sign-in-container'; +export type Providers = { + password: boolean; + magicLink: boolean; + oAuth: Provider[]; +}; + export function SignInMethodsContainer(props: { inviteToken?: string; @@ -25,11 +31,7 @@ export function SignInMethodsContainer(props: { updateAccount: string; }; - providers: { - password: boolean; - magicLink: boolean; - oAuth: Provider[]; - }; + providers: Providers; }) { const client = useSupabase(); const router = useRouter(); diff --git a/packages/features/auth/src/components/sign-up-methods-container.tsx b/packages/features/auth/src/components/sign-up-methods-container.tsx index aadbfb5..00782bd 100644 --- a/packages/features/auth/src/components/sign-up-methods-container.tsx +++ b/packages/features/auth/src/components/sign-up-methods-container.tsx @@ -1,8 +1,7 @@ 'use client'; -import { redirect } from 'next/navigation'; - import type { Provider } from '@supabase/supabase-js'; +import { useRouter } from 'next/navigation'; import { isBrowser } from '@kit/shared/utils'; import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; @@ -21,15 +20,20 @@ export function SignUpMethodsContainer(props: { updateAccount: string; }; - providers: { - password: boolean; - magicLink: boolean; - oAuth: Provider[]; + authConfig: { + providers: { + password: boolean; + magicLink: boolean; + oAuth: Provider[]; + }; + displayTermsCheckbox: boolean | undefined; + isMailerAutoconfirmEnabled: boolean; }; - displayTermsCheckbox?: boolean; inviteToken?: string; }) { + const router = useRouter(); + const redirectUrl = getCallbackUrl(props); const defaultValues = getDefaultValues(); @@ -39,26 +43,33 @@ export function SignUpMethodsContainer(props: { - + redirect(redirectUrl)} + authConfig={props.authConfig} + onSignUp={() => { + if (!props.authConfig.isMailerAutoconfirmEnabled) { + return; + } + setTimeout(() => { + router.replace(props.paths.updateAccount) + }, 2_500); + }} /> - + - +
@@ -72,7 +83,7 @@ export function SignUpMethodsContainer(props: {
=> { try { const cookies = await nextCookies() - const token = cookies.get("_medusa_jwt")?.value + const token = cookies.get(CookieName.MEDUSA_JWT)?.value if (!token) { return {} @@ -23,7 +31,7 @@ export const getMedusaCustomerId = async (): Promise< > => { try { const cookies = await nextCookies() - const customerId = cookies.get("_medusa_customer_id")?.value + const customerId = cookies.get(CookieName.MEDUSA_CUSTOMER_ID)?.value if (!customerId) { return { customerId: null } @@ -31,14 +39,14 @@ export const getMedusaCustomerId = async (): Promise< return { customerId } } catch { - return { customerId: null} + return { customerId: null } } } export const getCacheTag = async (tag: string): Promise => { try { const cookies = await nextCookies() - const cacheId = cookies.get("_medusa_cache_id")?.value + const cacheId = cookies.get(CookieName.MEDUSA_CACHE_ID)?.value if (!cacheId) { return "" @@ -66,51 +74,51 @@ export const getCacheOptions = async ( 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) => { const cookies = await nextCookies() - cookies.set("_medusa_jwt", token, { - maxAge: 60 * 60 * 24 * 7, - httpOnly: true, - sameSite: "strict", - secure: process.env.NODE_ENV === "production", + cookies.set(CookieName.MEDUSA_JWT, token, { + ...getCookieSharedOptions(), }) } export const setMedusaCustomerId = async (customerId: string) => { const cookies = await nextCookies() - cookies.set("_medusa_customer_id", customerId, { - maxAge: 60 * 60 * 24 * 7, - httpOnly: true, - sameSite: "strict", - secure: process.env.NODE_ENV === "production", + cookies.set(CookieName.MEDUSA_CUSTOMER_ID, customerId, { + ...getCookieSharedOptions(), }) } export const removeAuthToken = async () => { const cookies = await nextCookies() - cookies.set("_medusa_jwt", "", { - maxAge: -1, + cookies.set(CookieName.MEDUSA_JWT, "", { + ...getCookieResetOptions(), }) } export const getCartId = async () => { 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) => { const cookies = await nextCookies() - cookies.set("_medusa_cart_id", cartId, { - maxAge: 60 * 60 * 24 * 7, - httpOnly: true, - sameSite: "strict", - secure: process.env.NODE_ENV === "production", + cookies.set(CookieName.MEDUSA_CART_ID, cartId, { + ...getCookieSharedOptions(), }) } export const removeCartId = async () => { const cookies = await nextCookies() - cookies.set("_medusa_cart_id", "", { - maxAge: -1, + cookies.set(CookieName.MEDUSA_CART_ID, "", { + ...getCookieResetOptions(), }) } diff --git a/packages/features/medusa-storefront/src/lib/data/customer.ts b/packages/features/medusa-storefront/src/lib/data/customer.ts index a33a33a..3c05921 100644 --- a/packages/features/medusa-storefront/src/lib/data/customer.ts +++ b/packages/features/medusa-storefront/src/lib/data/customer.ts @@ -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 removeAuthToken() - const customerCacheTag = await getCacheTag("customers") - revalidateTag(customerCacheTag) + if (canRevalidateTags) { + const customerCacheTag = await getCacheTag("customers") + revalidateTag(customerCacheTag) + } await removeCartId() - const cartCacheTag = await getCacheTag("carts") - revalidateTag(cartCacheTag) + if (canRevalidateTags) { + const cartCacheTag = await getCacheTag("carts") + revalidateTag(cartCacheTag) + } } export async function transferCart() { diff --git a/packages/shared/src/components/ui/info-tooltip.tsx b/packages/shared/src/components/ui/info-tooltip.tsx index 10a7ae3..1217c24 100644 --- a/packages/shared/src/components/ui/info-tooltip.tsx +++ b/packages/shared/src/components/ui/info-tooltip.tsx @@ -23,7 +23,7 @@ export function InfoTooltip({ {icon || } - {content} + {content} ); diff --git a/packages/shared/src/config/auth-providers.service.ts b/packages/shared/src/config/auth-providers.service.ts new file mode 100644 index 0000000..179e7da --- /dev/null +++ b/packages/shared/src/config/auth-providers.service.ts @@ -0,0 +1,144 @@ +import type { Provider } from '@supabase/supabase-js'; +import authConfig from './auth.config'; + +type SupabaseExternalProvider = Provider | 'email'; +interface SupabaseAuthSettings { + external: Record; + disable_signup: boolean; + mailer_autoconfirm: boolean; +} + +export class AuthProvidersService { + private supabaseUrl: string; + private cache: Map = new Map(); + private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + constructor(supabaseUrl: string) { + this.supabaseUrl = supabaseUrl; + } + + async fetchAuthSettings(): Promise { + 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); +} diff --git a/packages/shared/src/config/dynamic-auth.config.ts b/packages/shared/src/config/dynamic-auth.config.ts new file mode 100644 index 0000000..571516a --- /dev/null +++ b/packages/shared/src/config/dynamic-auth.config.ts @@ -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 = 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 { + 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', + ]); +} diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index 516ecc7..d1737bb 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -8,6 +8,7 @@ import { createPath, getTeamAccountSidebarConfig, } from './team-account-navigation.config'; +import { DynamicAuthConfig, getCachedAuthConfig, getServerAuthConfig } from './dynamic-auth.config'; export { appConfig, @@ -18,4 +19,7 @@ export { getTeamAccountSidebarConfig, pathsConfig, personalAccountNavigationConfig, + getCachedAuthConfig, + getServerAuthConfig, + type DynamicAuthConfig, }; diff --git a/packages/shared/src/hooks/index.ts b/packages/shared/src/hooks/index.ts index 95e4bfd..b551263 100644 --- a/packages/shared/src/hooks/index.ts +++ b/packages/shared/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './use-csrf-token'; export * from './use-current-locale-language-names'; +export * from './use-auth-config'; diff --git a/packages/shared/src/hooks/use-auth-config.ts b/packages/shared/src/hooks/use-auth-config.ts new file mode 100644 index 0000000..5282554 --- /dev/null +++ b/packages/shared/src/hooks/use-auth-config.ts @@ -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; +} + +export function useAuthConfig(): UseAuthConfigResult { + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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, + }; +} diff --git a/packages/shared/src/utils.ts b/packages/shared/src/utils.ts index 971a03e..877cbca 100644 --- a/packages/shared/src/utils.ts +++ b/packages/shared/src/utils.ts @@ -1,5 +1,5 @@ import { format } from 'date-fns'; -import Isikukood, { Gender } from 'isikukood'; +import Isikukood from 'isikukood'; /** * Check if the code is running in a browser environment. diff --git a/packages/supabase/src/hooks/use-sign-out.ts b/packages/supabase/src/hooks/use-sign-out.ts index c354cee..7a64bd3 100644 --- a/packages/supabase/src/hooks/use-sign-out.ts +++ b/packages/supabase/src/hooks/use-sign-out.ts @@ -10,7 +10,7 @@ export function useSignOut() { try { try { const { medusaLogout } = await import('../../../features/medusa-storefront/src/lib/data/customer'); - await medusaLogout(); + await medusaLogout(undefined, false); } catch (medusaError) { console.warn('Medusa logout failed or not available:', medusaError); } diff --git a/packages/supabase/src/hooks/use-user.ts b/packages/supabase/src/hooks/use-user.ts index 0986775..9a9cdd9 100644 --- a/packages/supabase/src/hooks/use-user.ts +++ b/packages/supabase/src/hooks/use-user.ts @@ -28,8 +28,8 @@ export function useUser(initialData?: User | null) { queryFn, queryKey, initialData, - refetchInterval: false, - refetchOnMount: false, - refetchOnWindowFocus: false, + refetchInterval: 2_000, + refetchOnMount: true, + refetchOnWindowFocus: true, }); } diff --git a/packages/ui/src/makerkit/app-breadcrumbs.tsx b/packages/ui/src/makerkit/app-breadcrumbs.tsx index 31296b2..dc38b90 100644 --- a/packages/ui/src/makerkit/app-breadcrumbs.tsx +++ b/packages/ui/src/makerkit/app-breadcrumbs.tsx @@ -1,6 +1,7 @@ 'use client'; import { Fragment } from 'react'; +import clsx from 'clsx'; import { usePathname } from 'next/navigation'; @@ -52,9 +53,13 @@ export function AppBreadcrumbs(props: { /> ); + const isLast = index === visiblePaths.length - 1; + return ( - + {label} @@ -77,7 +83,7 @@ export function AppBreadcrumbs(props: { )} - + diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 7a4ee42..aaf48ad 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -42,7 +42,7 @@ function PageWithSidebar(props: PageProps) { > {MobileNavigation} -
+
{Children}
@@ -106,7 +106,7 @@ export function PageBody( }>, ) { 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, ); @@ -119,8 +119,8 @@ export function PageNavigation(props: React.PropsWithChildren) { export function PageDescription(props: React.PropsWithChildren) { return ( -
-
+
+
{props.children}
@@ -158,7 +158,7 @@ export function PageHeader({ return (
@@ -168,7 +168,7 @@ export function PageHeader({ -
+
{displaySidebarTrigger ? ( ) : null} diff --git a/packages/ui/src/shadcn/card.tsx b/packages/ui/src/shadcn/card.tsx index fec5a6b..9841c3e 100644 --- a/packages/ui/src/shadcn/card.tsx +++ b/packages/ui/src/shadcn/card.tsx @@ -34,7 +34,7 @@ const CardHeader: React.FC> = ({ className, ...props }) => ( -
+
); CardHeader.displayName = 'CardHeader'; @@ -60,14 +60,14 @@ CardDescription.displayName = 'CardDescription'; const CardContent: React.FC> = ({ className, ...props -}) =>
; +}) =>
; CardContent.displayName = 'CardContent'; const CardFooter: React.FC> = ({ className, ...props }) => ( -
+
); CardFooter.displayName = 'CardFooter'; diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 8e7020c..2872ede 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -130,7 +130,10 @@ "description": "Please enter your personal details to continue", "button": "Continue", "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": { "title": "Before we start", diff --git a/public/locales/en/auth.json b/public/locales/en/auth.json index 7db0925..5c89064 100644 --- a/public/locales/en/auth.json +++ b/public/locales/en/auth.json @@ -22,6 +22,7 @@ "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", "signInWithProvider": "Sign in with {{provider}}", + "signInWithKeycloak": "Smart-ID/Mobile-ID/ID-card", "signInWithPhoneNumber": "Sign in with Phone Number", "signInWithEmail": "Sign in with Email", "signUpWithEmail": "Sign up with Email", diff --git a/public/locales/en/cart.json b/public/locales/en/cart.json index 0c9af9a..223d3f1 100644 --- a/public/locales/en/cart.json +++ b/public/locales/en/cart.json @@ -3,9 +3,6 @@ "description": "View your cart", "emptyCartMessage": "Your cart is empty", "emptyCartMessageDescription": "Add items to your cart to continue.", - "subtotal": "Subtotal", - "total": "Total", - "promotionsTotal": "Promotions total", "table": { "item": "Item", "quantity": "Quantity", @@ -25,13 +22,19 @@ "timeoutAction": "Continue" }, "discountCode": { - "title": "Gift card or promotion code", - "label": "Add Promotion Code(s)", + "title": "Gift card or promo code", + "label": "Add Promo Code(s)", "apply": "Apply", - "subtitle": "If you wish, you can add a promotion code", - "placeholder": "Enter promotion code", - "remove": "Remove promotion code", - "appliedCodes": "Promotion(s) applied:" + "subtitle": "If you wish, you can add a promo code", + "placeholder": "Enter promo code", + "remove": "Remove promo code", + "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": { "synlabAnalyses": { @@ -52,7 +55,11 @@ } }, "order": { - "title": "Order" + "title": "Order", + "promotionsTotal": "Promotions total", + "subtotal": "Subtotal", + "total": "Total", + "giftCard": "Gift card" }, "orderConfirmed": { "title": "Order confirmed", diff --git a/public/locales/en/common.json b/public/locales/en/common.json index b26211f..cf41acd 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -129,7 +129,9 @@ "selectDate": "Select date" }, "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": { "balance": "Your MedReport account balance", diff --git a/public/locales/en/order-analysis.json b/public/locales/en/order-analysis.json index 2031316..11d1145 100644 --- a/public/locales/en/order-analysis.json +++ b/public/locales/en/order-analysis.json @@ -1,7 +1,6 @@ { "title": "Select analysis", "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", "analysisAddToCartError": "Adding analysis to cart failed" } \ No newline at end of file diff --git a/public/locales/en/order-health-analysis.json b/public/locales/en/order-health-analysis.json new file mode 100644 index 0000000..60633ac --- /dev/null +++ b/public/locales/en/order-health-analysis.json @@ -0,0 +1,4 @@ +{ + "title": "Order health analysis", + "description": "Select a suitable date and book your appointment time." +} \ No newline at end of file diff --git a/public/locales/en/orders.json b/public/locales/en/orders.json index f846b0a..7aa958c 100644 --- a/public/locales/en/orders.json +++ b/public/locales/en/orders.json @@ -1,6 +1,7 @@ { "title": "Orders", "description": "View your orders", + "noOrders": "No orders found", "table": { "analysisPackage": "Analysis package", "otherOrders": "Order", diff --git a/public/locales/et/account.json b/public/locales/et/account.json index e3e824c..86b36e3 100644 --- a/public/locales/et/account.json +++ b/public/locales/et/account.json @@ -130,7 +130,10 @@ "description": "Jätkamiseks palun sisestage enda isikuandmed", "button": "Jätka", "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": { "title": "Enne alustamist", diff --git a/public/locales/et/auth.json b/public/locales/et/auth.json index d9ebf9b..4919b25 100644 --- a/public/locales/et/auth.json +++ b/public/locales/et/auth.json @@ -2,7 +2,7 @@ "signUpHeading": "Loo konto", "signUp": "Loo 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", "signIn": "Logi sisse", "getStarted": "Alusta", @@ -22,6 +22,7 @@ "alreadyHaveAccountStatement": "Mul on juba konto, ma tahan sisse logida", "doNotHaveAccountStatement": "Mul pole kontot, ma tahan registreeruda", "signInWithProvider": "Logi sisse teenusega {{provider}}", + "signInWithKeycloak": "Smart-ID/Mobiil-ID/ID-kaart", "signInWithPhoneNumber": "Logi sisse telefoninumbriga", "signInWithEmail": "Logi sisse e-posti aadressiga", "signUpWithEmail": "Registreeru e-posti aadressiga", @@ -68,7 +69,7 @@ "acceptTermsAndConditions": "Ma nõustun ja ", "termsOfService": "Kasutustingimused", "privacyPolicy": "Privaatsuspoliitika", - "orContinueWith": "Või jätka koos", + "orContinueWith": "Või", "redirecting": "Oled sees! Palun oota...", "errors": { "Invalid login credentials": "Sisestatud andmed on valed", diff --git a/public/locales/et/cart.json b/public/locales/et/cart.json index fe87f7b..36b69d0 100644 --- a/public/locales/et/cart.json +++ b/public/locales/et/cart.json @@ -3,9 +3,6 @@ "description": "Vaata oma ostukorvi", "emptyCartMessage": "Sinu ostukorv on tühi", "emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.", - "subtotal": "Vahesumma", - "promotionsTotal": "Soodustuse summa", - "total": "Summa", "table": { "item": "Toode", "quantity": "Kogus", @@ -34,8 +31,10 @@ "appliedCodes": "Rakendatud sooduskoodid:", "removeError": "Sooduskoodi eemaldamine ebaõnnestus", "removeSuccess": "Sooduskood eemaldatud", + "removeLoading": "Sooduskoodi eemaldamine", "addError": "Sooduskoodi rakendamine ebaõnnestus", - "addSuccess": "Sooduskood rakendatud" + "addSuccess": "Sooduskood rakendatud", + "addLoading": "Rakendan sooduskoodi..." }, "items": { "synlabAnalyses": { @@ -56,7 +55,11 @@ } }, "order": { - "title": "Tellimus" + "title": "Tellimus", + "promotionsTotal": "Soodustuse summa", + "subtotal": "Vahesumma", + "total": "Summa", + "giftCard": "Kinkekaart" }, "orderConfirmed": { "title": "Tellimus on edukalt esitatud", diff --git a/public/locales/et/common.json b/public/locales/et/common.json index 792cc3a..485c009 100644 --- a/public/locales/et/common.json +++ b/public/locales/et/common.json @@ -129,7 +129,9 @@ "selectDate": "Vali kuupäev" }, "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": { "balance": "Sinu MedReporti konto saldo", diff --git a/public/locales/et/order-analysis.json b/public/locales/et/order-analysis.json index 9c7b750..8ff008d 100644 --- a/public/locales/et/order-analysis.json +++ b/public/locales/et/order-analysis.json @@ -1,7 +1,6 @@ { "title": "Vali analüüs", "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", "analysisAddToCartError": "Analüüsi lisamine ostukorvi ebaõnnestus" } \ No newline at end of file diff --git a/public/locales/et/order-health-analysis.json b/public/locales/et/order-health-analysis.json new file mode 100644 index 0000000..f267e09 --- /dev/null +++ b/public/locales/et/order-health-analysis.json @@ -0,0 +1,4 @@ +{ + "title": "Telli terviseuuring", + "description": "Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg." +} \ No newline at end of file diff --git a/public/locales/et/orders.json b/public/locales/et/orders.json index 4811a2e..822c6b1 100644 --- a/public/locales/et/orders.json +++ b/public/locales/et/orders.json @@ -1,6 +1,7 @@ { "title": "Tellimused", "description": "Vaata oma tellimusi", + "noOrders": "Tellimusi ei leitud", "table": { "analysisPackage": "Analüüsi pakett", "otherOrders": "Tellimus", diff --git a/public/locales/ru/account.json b/public/locales/ru/account.json index 9bfae35..bb3785b 100644 --- a/public/locales/ru/account.json +++ b/public/locales/ru/account.json @@ -130,7 +130,10 @@ "description": "Пожалуйста, введите личные данные для продолжения", "button": "Продолжить", "userConsentLabel": "Я согласен на использование персональных данных на платформе", - "userConsentUrlTitle": "Посмотреть политику конфиденциальности" + "userConsentUrlTitle": "Посмотреть политику конфиденциальности", + "updateAccountLoading": "Обновление данных аккаунта...", + "updateAccountSuccess": "Данные аккаунта обновлены", + "updateAccountError": "Не удалось обновить данные аккаунта" }, "consentModal": { "title": "Перед началом", diff --git a/public/locales/ru/auth.json b/public/locales/ru/auth.json index 8634403..5dc5e1d 100644 --- a/public/locales/ru/auth.json +++ b/public/locales/ru/auth.json @@ -22,6 +22,7 @@ "alreadyHaveAccountStatement": "У меня уже есть аккаунт, я хочу войти", "doNotHaveAccountStatement": "У меня нет аккаунта, я хочу зарегистрироваться", "signInWithProvider": "Войти через {{provider}}", + "signInWithKeycloak": "Smart-ID/Mobiil-ID/ID-kaart", "signInWithPhoneNumber": "Войти по номеру телефона", "signInWithEmail": "Войти по Email", "signUpWithEmail": "Зарегистрироваться по Email", diff --git a/public/locales/ru/cart.json b/public/locales/ru/cart.json index 7f3c536..289ff31 100644 --- a/public/locales/ru/cart.json +++ b/public/locales/ru/cart.json @@ -3,8 +3,6 @@ "description": "Просмотрите свою корзину", "emptyCartMessage": "Ваша корзина пуста", "emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.", - "subtotal": "Промежуточный итог", - "total": "Сумма", "table": { "item": "Товар", "quantity": "Количество", @@ -33,8 +31,10 @@ "appliedCodes": "Примененные промокоды:", "removeError": "Не удалось удалить промокод", "removeSuccess": "Промокод удален", + "removeLoading": "Удаление промокода...", "addError": "Не удалось применить промокод", - "addSuccess": "Промокод применен" + "addSuccess": "Промокод применен", + "addLoading": "Применение промокода..." }, "items": { "synlabAnalyses": { @@ -55,7 +55,11 @@ } }, "order": { - "title": "Заказ" + "title": "Заказ", + "promotionsTotal": "Скидка", + "subtotal": "Промежуточный итог", + "total": "Сумма", + "giftCard": "Подарочная карта" }, "orderConfirmed": { "title": "Заказ успешно оформлен", diff --git a/public/locales/ru/common.json b/public/locales/ru/common.json index 545b9e6..28b5d6b 100644 --- a/public/locales/ru/common.json +++ b/public/locales/ru/common.json @@ -128,6 +128,11 @@ "amount": "Сумма", "selectDate": "Выберите дату" }, + "formFieldError": { + "invalidPhoneNumber": "Пожалуйста, введите действительный номер телефона (должен включать код страны +372)", + "invalidPersonalCode": "Пожалуйста, введите действительный персональный код", + "stringNonEmpty": "Это поле обязательно" + }, "wallet": { "balance": "Баланс вашего счета MedReport", "expiredAt": "Действительно до {{expiredAt}}" diff --git a/public/locales/ru/order-analysis.json b/public/locales/ru/order-analysis.json index ea36b5c..c837255 100644 --- a/public/locales/ru/order-analysis.json +++ b/public/locales/ru/order-analysis.json @@ -1,7 +1,6 @@ { "title": "Выберите анализ", "description": "Результаты всех анализов будут доступны в течение 1–3 рабочих дней после сдачи крови.", - "analysisNotAvailable": "Заказ анализа в данный момент недоступен", "analysisAddedToCart": "Анализ добавлен в корзину", "analysisAddToCartError": "Не удалось добавить анализ в корзину" } \ No newline at end of file diff --git a/public/locales/ru/order-health-analysis.json b/public/locales/ru/order-health-analysis.json new file mode 100644 index 0000000..75c65f7 --- /dev/null +++ b/public/locales/ru/order-health-analysis.json @@ -0,0 +1,4 @@ +{ + "title": "Заказать анализ здоровья", + "description": "Выберите подходящую дату и забронируйте время для вашего приёма." +} \ No newline at end of file diff --git a/public/locales/ru/orders.json b/public/locales/ru/orders.json index c42a230..6669aff 100644 --- a/public/locales/ru/orders.json +++ b/public/locales/ru/orders.json @@ -1,6 +1,7 @@ { "title": "Заказы", "description": "Просмотрите ваши заказы", + "noOrders": "Заказы не найдены", "table": { "analysisPackage": "Пакет анализов", "otherOrders": "Заказ",