From 95452de88b6656df589e70866ab7076e580fbf5c Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:31:23 +0300 Subject: [PATCH 01/19] prefer using providers conf from supabase instead of env --- .../site-header-account-section.tsx | 37 +++-- .../sign-in/components/PasswordOption.tsx | 8 +- app/auth/sign-in/page.tsx | 18 ++- app/auth/sign-up/page.tsx | 6 +- app/home/(user)/_components/dashboard.tsx | 11 +- lib/utils.ts | 2 +- .../components/sign-in-methods-container.tsx | 12 +- .../components/sign-up-methods-container.tsx | 2 - .../src/config/auth-providers.service.ts | 137 ++++++++++++++++++ .../shared/src/config/dynamic-auth.config.ts | 102 +++++++++++++ packages/shared/src/config/index.ts | 3 + packages/shared/src/hooks/index.ts | 1 + packages/shared/src/hooks/use-auth-config.ts | 76 ++++++++++ 13 files changed, 382 insertions(+), 33 deletions(-) create mode 100644 packages/shared/src/config/auth-providers.service.ts create mode 100644 packages/shared/src/config/dynamic-auth.config.ts create mode 100644 packages/shared/src/hooks/use-auth-config.ts 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..52d77e9 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('/'); } @@ -56,9 +58,9 @@ async function SignUpPage({ searchParams }: Props) {
diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx index d2f8007..f8b2e79 100644 --- a/app/home/(user)/_components/dashboard.tsx +++ b/app/home/(user)/_components/dashboard.tsx @@ -146,14 +146,21 @@ export default function Dashboard({ }) { const height = account.accountParams?.height || 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 }); return ( <>
{cards({ - gender: gender.label, + gender: gender?.label, age, height, weight, 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/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..8759e0c 100644 --- a/packages/features/auth/src/components/sign-up-methods-container.tsx +++ b/packages/features/auth/src/components/sign-up-methods-container.tsx @@ -1,7 +1,5 @@ 'use client'; -import { redirect } from 'next/navigation'; - import type { Provider } from '@supabase/supabase-js'; import { isBrowser } from '@kit/shared/utils'; 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..5cdb599 --- /dev/null +++ b/packages/shared/src/config/auth-providers.service.ts @@ -0,0 +1,137 @@ +import type { Provider } from '@supabase/supabase-js'; +import authConfig from './auth.config'; + +type SupabaseExternalProvider = Provider | 'email'; +interface SupabaseAuthSettings { + external: Record; + disable_signup: 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'; + } + + 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] = await Promise.all([ + this.isPasswordEnabled({ settings }), + this.isMagicLinkEnabled(), + this.getEnabledOAuthProviders({ settings }), + ]); + + return { + providers: { + password: passwordEnabled, + magicLink: magicLinkEnabled, + oAuth: oAuthProviders, + }, + displayTermsCheckbox: authConfig.displayTermsCheckbox, + }; + } + + 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..428be33 --- /dev/null +++ b/packages/shared/src/config/dynamic-auth.config.ts @@ -0,0 +1,102 @@ +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.'), +}); + +export async function getDynamicAuthConfig() { + const authService = createAuthProvidersService(); + const dynamicProviders = await authService.getAuthConfig(); + + const config = { + providers: dynamicProviders.providers, + displayTermsCheckbox: dynamicProviders.displayTermsCheckbox, + }; + + 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..5669259 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 { getCachedAuthConfig, getServerAuthConfig } from './dynamic-auth.config'; export { appConfig, @@ -18,4 +19,6 @@ export { getTeamAccountSidebarConfig, pathsConfig, personalAccountNavigationConfig, + getCachedAuthConfig, + getServerAuthConfig, }; 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, + }; +} From 7bc89f7c22f1c3e648a318a6653b37244d814c41 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:31:32 +0300 Subject: [PATCH 02/19] no need to show "select-package" page after onboading if it has nothing --- app/select-package/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) 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 (
From be33b892f5dd40089cf1b2f87bbf1200570ba193 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:31:39 +0300 Subject: [PATCH 03/19] only show checks for analysis elements explicitly in current package --- app/home/(user)/_components/compare-packages-modal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/home/(user)/_components/compare-packages-modal.tsx b/app/home/(user)/_components/compare-packages-modal.tsx index 90ed81c..eadfd88 100644 --- a/app/home/(user)/_components/compare-packages-modal.tsx +++ b/app/home/(user)/_components/compare-packages-modal.tsx @@ -136,10 +136,10 @@ const ComparePackagesModal = async ({ {isIncludedInStandard && } - {(isIncludedInStandard || isIncludedInStandardPlus) && } + {isIncludedInStandardPlus && } - {(isIncludedInStandard || isIncludedInStandardPlus || isIncludedInPremium) && } + {isIncludedInPremium && } ); From 0aea6b80d418d7d6440bf8e14b219d69e662c29b Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:31:49 +0300 Subject: [PATCH 04/19] sent time in medipost xml can be different from order creation time --- lib/services/medipostTest.service.ts | 2 +- lib/services/medipostXML.service.ts | 2 +- lib/templates/medipost-order.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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 `; From fa9895637d88bc45051df748cd987f091b6721a0 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:32:25 +0300 Subject: [PATCH 05/19] add env to turn off automatic medipost sending on montonio callback --- .env | 3 +++ .env.development | 16 ++++++++++++++++ .../cart/montonio-callback/actions.ts | 9 ++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) 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/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 89c8dc3..6ee8e5d 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 ({ @@ -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) { From cb11244d79a95fad8b66ffb905e0adf2674c0c0e Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:32:41 +0300 Subject: [PATCH 06/19] improve order analyses cards --- .../_components/order-analyses-cards.tsx | 78 +++++++++---------- app/home/(user)/_lib/server/load-analyses.ts | 10 +-- public/locales/en/order-analysis.json | 1 - public/locales/et/order-analysis.json | 1 - public/locales/ru/order-analysis.json | 1 - 5 files changed, 42 insertions(+), 49 deletions(-) diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx index 5cb4d31..2e1064d 100644 --- a/app/home/(user)/_components/order-analyses-cards.tsx +++ b/app/home/(user)/_components/order-analyses-cards.tsx @@ -21,7 +21,6 @@ import { formatCurrency } from '@/packages/shared/src/utils'; export type OrderAnalysisCard = Pick< StoreProduct, 'title' | 'description' | 'subtitle' > & { - isAvailable: boolean; variant: { id: string }; price: number | null; }; @@ -64,7 +63,6 @@ export default function OrderAnalysesCards({ variant, description, subtitle, - isAvailable, price, }) => { const formattedPrice = typeof price === 'number' @@ -77,7 +75,7 @@ export default function OrderAnalysesCards({ return ( @@ -86,46 +84,44 @@ export default function OrderAnalysesCards({ >
- {isAvailable && ( -
- -
- )} +
+ +
- -
- {title} - {description && ( - <> - {' '} - - {formattedPrice} - {description} -
- } - /> - + +
+
+ {title} + {description && ( + <> + {' '} + + {formattedPrice} + {description} +
+ } + /> + + )} + + {subtitle && ( + + {subtitle} + )} - - {isAvailable && subtitle && ( - - {subtitle} - - )} - {!isAvailable && ( - - - - )} +
+
+ {formattedPrice} +
); diff --git a/app/home/(user)/_lib/server/load-analyses.ts b/app/home/(user)/_lib/server/load-analyses.ts index 1cc954c..cd16e61 100644 --- a/app/home/(user)/_lib/server/load-analyses.ts +++ b/app/home/(user)/_lib/server/load-analyses.ts @@ -41,7 +41,7 @@ async function analysesLoader() { const categoryProducts = category ? await listProducts({ countryCode, - queryParams: { limit: 100, category_id: category.id }, + queryParams: { limit: 100, category_id: category.id, order: 'title' }, }) : null; @@ -51,8 +51,10 @@ async function analysesLoader() { return { analyses: - categoryProducts?.response.products.map( - ({ title, description, subtitle, variants, status, metadata }) => { + categoryProducts?.response.products + .filter(({ status, metadata }) => status === 'published' && !!metadata?.analysisIdOriginal) + .map( + ({ title, description, subtitle, variants }) => { const variant = variants![0]!; return { title, @@ -61,8 +63,6 @@ async function analysesLoader() { variant: { id: variant.id, }, - isAvailable: - status === 'published' && !!metadata?.analysisIdOriginal, price: variant.calculated_price?.calculated_amount ?? null, }; }, 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/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/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 From 229b3d7c270ad221f76cfe7d69f15306b0470bf3 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:32:48 +0300 Subject: [PATCH 07/19] fix warnings on cart page refresh --- app/home/(user)/(dashboard)/cart/page.tsx | 7 ++- .../medusa-storefront/src/lib/data/cookies.ts | 56 +++++++++++-------- 2 files changed, 37 insertions(+), 26 deletions(-) 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/packages/features/medusa-storefront/src/lib/data/cookies.ts b/packages/features/medusa-storefront/src/lib/data/cookies.ts index 7694904..ede7537 100644 --- a/packages/features/medusa-storefront/src/lib/data/cookies.ts +++ b/packages/features/medusa-storefront/src/lib/data/cookies.ts @@ -1,12 +1,20 @@ import "server-only" + 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< { authorization: string } | {} > => { 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(), }) } From 2aad0329f3c0fa44917bcc3aa8d67aa48dbc669d Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:32:55 +0300 Subject: [PATCH 08/19] update cart discount for prod build, add loading toast --- .../_components/cart/discount-code-actions.ts | 24 ++++++++ .../(user)/_components/cart/discount-code.tsx | 59 ++++++++----------- public/locales/en/cart.json | 18 ++++-- public/locales/et/cart.json | 6 +- public/locales/ru/cart.json | 5 +- 5 files changed, 70 insertions(+), 42 deletions(-) create mode 100644 app/home/(user)/_components/cart/discount-code-actions.ts diff --git a/app/home/(user)/_components/cart/discount-code-actions.ts b/app/home/(user)/_components/cart/discount-code-actions.ts new file mode 100644 index 0000000..869ec78 --- /dev/null +++ b/app/home/(user)/_components/cart/discount-code-actions.ts @@ -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' }; + } +} diff --git a/app/home/(user)/_components/cart/discount-code.tsx b/app/home/(user)/_components/cart/discount-code.tsx index eea8ef4..9df9073 100644 --- a/app/home/(user)/_components/cart/discount-code.tsx +++ b/app/home/(user)/_components/cart/discount-code.tsx @@ -2,9 +2,8 @@ import { Badge, Text } from "@medusajs/ui" 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 { StoreCart, StorePromotion } from "@medusajs/types" import Trash from "@modules/common/icons/trash" @@ -16,6 +15,7 @@ import { useTranslation } from "react-i18next"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { addPromotionCodeAction, removePromotionCodeAction } from "./discount-code-actions"; const DiscountCodeSchema = z.object({ code: z.string().min(1), @@ -31,42 +31,35 @@ export default function DiscountCode({ cart }: { const { promotions = [] } = cart; const removePromotionCode = async (code: string) => { - const validPromotions = promotions.filter( - (promotion) => promotion.code !== code, - ) + const appliedCodes = promotions + .filter((p) => p.code !== undefined) + .map((p) => p.code!) - await applyPromotions( - validPromotions.filter((p) => p.code === undefined).map((p) => p.code!), - { - onSuccess: () => { - toast.success(t('cart:discountCode.removeSuccess')); - }, - onError: () => { - toast.error(t('cart:discountCode.removeError')); - }, - } - ) + const loading = toast.loading(t('cart:discountCode.removeLoading')); + + const result = await removePromotionCodeAction(code, appliedCodes) + + toast.dismiss(loading); + if (result.success) { + toast.success(t('cart:discountCode.removeSuccess')); + } else { + toast.error(t('cart:discountCode.removeError')); + } } const addPromotionCode = async (code: string) => { - const codes = promotions - .filter((p) => p.code === undefined) - .map((p) => p.code!) - codes.push(code.toString()) - - await applyPromotions(codes, { - onSuccess: () => { - toast.success(t('cart:discountCode.addSuccess')); - }, - onError: () => { - toast.error(t('cart:discountCode.addError')); - }, - }); - - form.reset() + const loading = toast.loading(t('cart:discountCode.addLoading')); + const result = await addPromotionCodeAction(code) + + toast.dismiss(loading); + if (result.success) { + toast.success(t('cart:discountCode.addSuccess')); + form.reset() + } else { + toast.error(t('cart:discountCode.addError')); + } } - const [message, formAction] = useActionState(submitPromotionForm, null) const form = useForm>({ defaultValues: { @@ -135,7 +128,7 @@ export default function DiscountCode({ cart }: { "percentage" ? `${promotion.application_method.value}%` : convertToLocale({ - amount: promotion.application_method.value, + amount: Number(promotion.application_method.value), currency_code: promotion.application_method .currency_code, diff --git a/public/locales/en/cart.json b/public/locales/en/cart.json index 0c9af9a..a78a8cb 100644 --- a/public/locales/en/cart.json +++ b/public/locales/en/cart.json @@ -25,13 +25,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": { diff --git a/public/locales/et/cart.json b/public/locales/et/cart.json index fe87f7b..e5e7376 100644 --- a/public/locales/et/cart.json +++ b/public/locales/et/cart.json @@ -4,8 +4,8 @@ "emptyCartMessage": "Sinu ostukorv on tühi", "emptyCartMessageDescription": "Lisa tooteid ostukorvi, et jätkata.", "subtotal": "Vahesumma", - "promotionsTotal": "Soodustuse summa", "total": "Summa", + "promotionsTotal": "Soodustuse summa", "table": { "item": "Toode", "quantity": "Kogus", @@ -34,8 +34,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": { diff --git a/public/locales/ru/cart.json b/public/locales/ru/cart.json index 7f3c536..bac0c63 100644 --- a/public/locales/ru/cart.json +++ b/public/locales/ru/cart.json @@ -5,6 +5,7 @@ "emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.", "subtotal": "Промежуточный итог", "total": "Сумма", + "promotionsTotal": "Скидка", "table": { "item": "Товар", "quantity": "Количество", @@ -33,8 +34,10 @@ "appliedCodes": "Примененные промокоды:", "removeError": "Не удалось удалить промокод", "removeSuccess": "Промокод удален", + "removeLoading": "Удаление промокода...", "addError": "Не удалось применить промокод", - "addSuccess": "Промокод применен" + "addSuccess": "Промокод применен", + "addLoading": "Применение промокода..." }, "items": { "synlabAnalyses": { From 312027b9edb35dddccbb2668b44eac788929405a Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 00:12:57 +0300 Subject: [PATCH 09/19] avoid too many duplicate `requireUserInServerComponent` requests for each page+layout --- .../update-account/_lib/server/update-account.ts | 1 - app/auth/update-account/page.tsx | 7 +------ .../(dashboard)/analysis-results/[id]/page.tsx | 13 +++++++++---- .../(dashboard)/cart/montonio-callback/actions.ts | 2 +- app/home/(user)/(dashboard)/order-analysis/page.tsx | 2 +- app/home/(user)/(dashboard)/page.tsx | 2 +- app/home/(user)/_components/orders/actions.ts | 2 +- .../(user)/_lib/server/load-analysis-packages.ts | 2 +- app/home/(user)/_lib/server/load-user-account.ts | 9 ++++++--- app/home/(user)/settings/page.tsx | 2 +- app/home/(user)/settings/preferences/page.tsx | 7 +------ app/home/layout.tsx | 4 +--- lib/services/medusaCart.service.ts | 12 ++++-------- 13 files changed, 28 insertions(+), 37 deletions(-) diff --git a/app/auth/update-account/_lib/server/update-account.ts b/app/auth/update-account/_lib/server/update-account.ts index ff9a80d..ebead4c 100644 --- a/app/auth/update-account/_lib/server/update-account.ts +++ b/app/auth/update-account/_lib/server/update-account.ts @@ -10,7 +10,6 @@ import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { pathsConfig } from '@kit/shared/config'; - import { UpdateAccountSchema } from '../schemas/update-account.schema'; export const onUpdateAccount = enhanceAction( diff --git a/app/auth/update-account/page.tsx b/app/auth/update-account/page.tsx index a40b13e..6c1030f 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,12 +14,8 @@ 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'; if (!user) { diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx index 9ce91e8..7422d32 100644 --- a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx +++ b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx @@ -22,13 +22,14 @@ 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) { + if (!account?.id) { return null; } @@ -37,6 +38,10 @@ export default async function AnalysisResultsPage({ action: PageViewAction.VIEW_ANALYSIS_RESULTS, }); + if (!analysisResponse) { + return null; + } + return ( <> diff --git a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts index 6ee8e5d..f705399 100644 --- a/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts +++ b/app/home/(user)/(dashboard)/cart/montonio-callback/actions.ts @@ -168,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"); } 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)/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/orders/actions.ts b/app/home/(user)/_components/orders/actions.ts index d201507..07bdfbc 100644 --- a/app/home/(user)/_components/orders/actions.ts +++ b/app/home/(user)/_components/orders/actions.ts @@ -4,7 +4,7 @@ import { createPageViewLog, PageViewAction } from "~/lib/services/audit/pageView import { loadCurrentUserAccount } from "../../_lib/server/load-user-account"; export async function logAnalysisResultsNavigateAction(analysisOrderId: string) { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } diff --git a/app/home/(user)/_lib/server/load-analysis-packages.ts b/app/home/(user)/_lib/server/load-analysis-packages.ts index 597b95f..c4c1a12 100644 --- a/app/home/(user)/_lib/server/load-analysis-packages.ts +++ b/app/home/(user)/_lib/server/load-analysis-packages.ts @@ -140,7 +140,7 @@ async function analysisPackagesWithVariantLoader({ } async function analysisPackagesLoader() { - const account = await loadCurrentUserAccount(); + const { account } = await loadCurrentUserAccount(); if (!account) { throw new Error('Account not found'); } diff --git a/app/home/(user)/_lib/server/load-user-account.ts b/app/home/(user)/_lib/server/load-user-account.ts index 1b324ea..a16108a 100644 --- a/app/home/(user)/_lib/server/load-user-account.ts +++ b/app/home/(user)/_lib/server/load-user-account.ts @@ -16,9 +16,12 @@ export const loadUserAccount = cache(accountLoader); export async function loadCurrentUserAccount() { const user = await requireUserInServerComponent(); - return user?.id - ? await loadUserAccount(user.id) - : null; + const userId = user?.id; + if (!userId) { + return { account: null, user: null }; + } + const account = await loadUserAccount(userId); + return { account, user }; } async function accountLoader(userId: string) { 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/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'); } From e3cdba6a7c5f5904ec7a684c3e94b8e367c10f47 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:33:56 +0300 Subject: [PATCH 10/19] show less update-account fields on email login --- .../_components/update-account-form.tsx | 168 +++++++++--------- .../_lib/schemas/update-account.schema.ts | 42 ++++- .../_lib/server/update-account.ts | 4 +- app/auth/update-account/page.tsx | 3 +- 4 files changed, 127 insertions(+), 90 deletions(-) diff --git a/app/auth/update-account/_components/update-account-form.tsx b/app/auth/update-account/_components/update-account-form.tsx index bdda351..0741489 100644 --- a/app/auth/update-account/_components/update-account-form.tsx +++ b/app/auth/update-account/_components/update-account-form.tsx @@ -19,19 +19,21 @@ 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'; -type UpdateAccountFormValues = z.infer; +type UpdateAccountFormValues = z.infer; export function UpdateAccountForm({ defaultValues, + isEmailUser, }: { defaultValues: UpdateAccountFormValues, + isEmailUser: boolean, }) { const form = useForm({ - resolver: zodResolver(UpdateAccountSchema), + resolver: zodResolver(UpdateAccountSchemaClient), mode: 'onChange', defaultValues, }); @@ -44,18 +46,18 @@ export function UpdateAccountForm({ 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 }), + firstName: hasFirstName ? firstName : values.firstName, + lastName: hasLastName ? lastName : values.lastName, + personalCode: hasPersonalCode ? personalCode : values.personalCode, + email: hasEmail ? email : values.email, + phone: values.phone, + weight: (hasWeight ? weight : values.weight) as number, + height: (hasHeight ? height : values.height) as number, + userConsent: values.userConsent ?? userConsent, + city: values.city, }); return ( @@ -66,14 +68,14 @@ export function UpdateAccountForm({ > ( - + @@ -82,14 +84,14 @@ export function UpdateAccountForm({ ( - + @@ -98,7 +100,7 @@ export function UpdateAccountForm({ ( @@ -143,72 +145,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', }), @@ -59,4 +67,26 @@ 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, + height: updateAccountSchema.height, + userConsent: updateAccountSchema.userConsent, +}); +export const UpdateAccountSchemaClient = z.object({ + firstName: updateAccountSchema.firstName, + lastName: updateAccountSchema.lastName, + personalCode: updateAccountSchema.personalCode, + email: updateAccountSchema.email, + phone: updateAccountSchema.phone, + city: updateAccountSchema.city, + weight: updateAccountSchema.weight.gt(-1).gte(0).nullable(), + height: updateAccountSchema.height.gt(-1).gte(0).nullable(), + 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 ebead4c..7e70139 100644 --- a/app/auth/update-account/_lib/server/update-account.ts +++ b/app/auth/update-account/_lib/server/update-account.ts @@ -10,7 +10,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) => { @@ -47,6 +47,6 @@ export const onUpdateAccount = enhanceAction( } }, { - schema: UpdateAccountSchema, + schema: UpdateAccountSchemaServer, }, ); diff --git a/app/auth/update-account/page.tsx b/app/auth/update-account/page.tsx index 6c1030f..031120c 100644 --- a/app/auth/update-account/page.tsx +++ b/app/auth/update-account/page.tsx @@ -17,6 +17,7 @@ async function UpdateAccount() { const { account, user } = await loadCurrentUserAccount(); const isKeycloakUser = user?.app_metadata?.provider === 'keycloak'; + const isEmailUser = user?.app_metadata?.provider === 'email'; if (!user) { redirect(pathsConfig.auth.signIn); @@ -50,7 +51,7 @@ async function UpdateAccount() {

- +
From fa0bbe64fb6a390d3a5a86389c635e5b37c49d72 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:34:05 +0300 Subject: [PATCH 11/19] update account form for email login --- .../_components/update-account-form.tsx | 63 +++++++++++++------ .../_lib/schemas/update-account.schema.ts | 26 +++++--- .../_lib/server/update-account.ts | 9 +-- .../_components/account-settings-form.tsx | 14 +++-- .../settings/_lib/account-settings.schema.ts | 4 +- .../server/schema/create-company.schema.ts | 2 +- packages/shared/src/utils.ts | 2 +- public/locales/en/account.json | 5 +- public/locales/en/common.json | 4 +- public/locales/et/account.json | 5 +- public/locales/et/common.json | 4 +- public/locales/ru/account.json | 5 +- public/locales/ru/common.json | 5 ++ 13 files changed, 102 insertions(+), 46 deletions(-) diff --git a/app/auth/update-account/_components/update-account-form.tsx b/app/auth/update-account/_components/update-account-form.tsx index 0741489..a657755 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'; @@ -21,9 +24,10 @@ import { Trans } from '@kit/ui/trans'; 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, @@ -32,33 +36,56 @@ export function UpdateAccountForm({ defaultValues: UpdateAccountFormValues, isEmailUser: boolean, }) { + const router = useRouter(); + const { t } = useTranslation('account'); + const form = useForm({ - resolver: zodResolver(UpdateAccountSchemaClient), + 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 onUpdateAccountOptions = async (values: UpdateAccountFormValues) => - onUpdateAccount({ - firstName: hasFirstName ? firstName : values.firstName, - lastName: hasLastName ? lastName : values.lastName, - personalCode: hasPersonalCode ? personalCode : values.personalCode, - email: hasEmail ? email : values.email, - phone: values.phone, - weight: (hasWeight ? weight : values.weight) as number, - height: (hasHeight ? height : values.height) as number, - userConsent: values.userConsent ?? userConsent, - city: values.city, - }); + const onUpdateAccountOptions = async (values: UpdateAccountFormValues) => { + const loading = toast.loading(t('updateAccount.updateAccountLoading')); + try { + const response = await onUpdateAccount({ + firstName: hasFirstName ? firstName : values.firstName, + lastName: hasLastName ? lastName : values.lastName, + personalCode: hasPersonalCode ? personalCode : values.personalCode, + email: hasEmail ? email : values.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 (
diff --git a/app/auth/update-account/_lib/schemas/update-account.schema.ts b/app/auth/update-account/_lib/schemas/update-account.schema.ts index ad59e59..bba388c 100644 --- a/app/auth/update-account/_lib/schemas/update-account.schema.ts +++ b/app/auth/update-account/_lib/schemas/update-account.schema.ts @@ -12,7 +12,9 @@ const updateAccountSchema = { .string({ error: 'Last name is required', }) - .nonempty(), + .nonempty({ + error: 'common:formFieldError.stringNonEmpty', + }), personalCode: z.string().refine( (val) => { try { @@ -30,7 +32,7 @@ const updateAccountSchema = { }), phone: z .string({ - error: 'Phone number is required', + error: 'error:invalidPhone', }) .nonempty() .refine( @@ -75,18 +77,26 @@ export const UpdateAccountSchemaServer = z.object({ email: updateAccountSchema.email, phone: updateAccountSchema.phone, city: updateAccountSchema.city, - weight: updateAccountSchema.weight, - height: updateAccountSchema.height, + weight: updateAccountSchema.weight.nullable(), + height: updateAccountSchema.height.nullable(), userConsent: updateAccountSchema.userConsent, }); -export const UpdateAccountSchemaClient = z.object({ +export const UpdateAccountSchemaClient = ({ isEmailUser }: { isEmailUser: boolean }) => z.object({ firstName: updateAccountSchema.firstName, lastName: updateAccountSchema.lastName, personalCode: updateAccountSchema.personalCode, email: updateAccountSchema.email, phone: updateAccountSchema.phone, - city: updateAccountSchema.city, - weight: updateAccountSchema.weight.gt(-1).gte(0).nullable(), - height: updateAccountSchema.height.gt(-1).gte(0).nullable(), + ...(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 7e70139..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'; @@ -39,11 +37,8 @@ export const onUpdateAccount = enhanceAction( const hasUnseenMembershipConfirmation = await api.hasUnseenMembershipConfirmation(); - - if (hasUnseenMembershipConfirmation) { - redirect(pathsConfig.auth.membershipConfirmation); - } else { - redirect(pathsConfig.app.selectPackage); + return { + hasUnseenMembershipConfirmation, } }, { diff --git a/app/home/(user)/settings/_components/account-settings-form.tsx b/app/home/(user)/settings/_components/account-settings-form.tsx index 95db6cd..8513798 100644 --- a/app/home/(user)/settings/_components/account-settings-form.tsx +++ b/app/home/(user)/settings/_components/account-settings-form.tsx @@ -7,7 +7,6 @@ import { Trans } from 'react-i18next'; import { AccountWithParams } from '@kit/accounts/api'; import { useRevalidatePersonalAccountDataQuery } from '@kit/accounts/hooks/use-personal-account-data'; import { Button } from '@kit/ui/button'; -import { Card, CardTitle } from '@kit/ui/card'; import { Form, FormControl, @@ -25,7 +24,6 @@ import { SelectValue, } from '@kit/ui/select'; import { toast } from '@kit/ui/sonner'; -import { Switch } from '@kit/ui/switch'; import { AccountSettings, @@ -131,7 +129,11 @@ export default function AccountSettingsForm({ - + @@ -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/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/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/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/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/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/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/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/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}}" From b8a8eab87c7f5df025523209dccc3a028192a042 Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:34:18 +0300 Subject: [PATCH 12/19] update order details view, translations --- .../analysis-results/[id]/page.tsx | 14 +++++++++++-- app/home/(user)/_components/cart/index.tsx | 6 +++--- .../_components/order-analyses-cards.tsx | 2 +- .../(user)/_components/order/cart-totals.tsx | 12 +++++------ .../_components/order/order-details.tsx | 20 +++++++++++++------ packages/features/auth/src/server/api.ts | 4 ++-- packages/ui/src/makerkit/page.tsx | 6 +++--- public/locales/en/cart.json | 9 +++++---- public/locales/et/cart.json | 9 +++++---- public/locales/ru/cart.json | 9 +++++---- 10 files changed, 56 insertions(+), 35 deletions(-) diff --git a/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx b/app/home/(user)/(dashboard)/analysis-results/[id]/page.tsx index 7422d32..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'; @@ -30,7 +31,7 @@ export default async function AnalysisResultsPage({ ]); if (!account?.id) { - return null; + return redirect("/"); } await createPageViewLog({ @@ -39,7 +40,16 @@ export default async function AnalysisResultsPage({ }); if (!analysisResponse) { - return null; + return ( + <> + } + description={} + /> + + + + ); } return ( diff --git a/app/home/(user)/_components/cart/index.tsx b/app/home/(user)/_components/cart/index.tsx index 4ab083e..afd4639 100644 --- a/app/home/(user)/_components/cart/index.tsx +++ b/app/home/(user)/_components/cart/index.tsx @@ -81,7 +81,7 @@ export default function Cart({

- +

@@ -97,7 +97,7 @@ export default function Cart({

- +

@@ -113,7 +113,7 @@ export default function Cart({

- +

diff --git a/app/home/(user)/_components/order-analyses-cards.tsx b/app/home/(user)/_components/order-analyses-cards.tsx index 2e1064d..bd7f8a7 100644 --- a/app/home/(user)/_components/order-analyses-cards.tsx +++ b/app/home/(user)/_components/order-analyses-cards.tsx @@ -57,7 +57,7 @@ export default function OrderAnalysesCards({ } return ( -
+
{analyses.map(({ title, variant, diff --git a/app/home/(user)/_components/order/cart-totals.tsx b/app/home/(user)/_components/order/cart-totals.tsx index 2df2237..fb7b030 100644 --- a/app/home/(user)/_components/order/cart-totals.tsx +++ b/app/home/(user)/_components/order/cart-totals.tsx @@ -24,7 +24,7 @@ export default function CartTotals({ medusaOrder }: {
- + {formatCurrency({ value: subtotal ?? 0, currencyCode: currency_code, locale: language })} @@ -32,7 +32,7 @@ export default function CartTotals({ medusaOrder }: {
{!!discount_total && (
- +
)} -
+ {/*
{formatCurrency({ value: tax_total ?? 0, currencyCode: currency_code, locale: language })} -
+
*/} {!!gift_card_total && (
- +
- + - - :{" "} +
+ + :{" "} + + + {order.medusa_order_id} + +
+ +
+ + :{" "} + {formatDate(order.created_at, 'dd.MM.yyyy HH:mm')} - - - : {order.medusa_order_id} - +
) } diff --git a/packages/features/auth/src/server/api.ts b/packages/features/auth/src/server/api.ts index 223e82c..8462006 100644 --- a/packages/features/auth/src/server/api.ts +++ b/packages/features/auth/src/server/api.ts @@ -9,8 +9,8 @@ export interface AccountSubmitData { email: string; phone?: string; city?: string; - weight: number | null; - height: number | null; + weight?: number | null | undefined; + height?: number | null | undefined; userConsent: boolean; } diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 7a4ee42..6ca7573 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -119,8 +119,8 @@ export function PageNavigation(props: React.PropsWithChildren) { export function PageDescription(props: React.PropsWithChildren) { return ( -
-
+
+
{props.children}
@@ -168,7 +168,7 @@ export function PageHeader({ -
+
{displaySidebarTrigger ? ( ) : null} diff --git a/public/locales/en/cart.json b/public/locales/en/cart.json index a78a8cb..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", @@ -58,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/et/cart.json b/public/locales/et/cart.json index e5e7376..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", - "total": "Summa", - "promotionsTotal": "Soodustuse summa", "table": { "item": "Toode", "quantity": "Kogus", @@ -58,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/ru/cart.json b/public/locales/ru/cart.json index bac0c63..289ff31 100644 --- a/public/locales/ru/cart.json +++ b/public/locales/ru/cart.json @@ -3,9 +3,6 @@ "description": "Просмотрите свою корзину", "emptyCartMessage": "Ваша корзина пуста", "emptyCartMessageDescription": "Добавьте товары в корзину, чтобы продолжить.", - "subtotal": "Промежуточный итог", - "total": "Сумма", - "promotionsTotal": "Скидка", "table": { "item": "Товар", "quantity": "Количество", @@ -58,7 +55,11 @@ } }, "order": { - "title": "Заказ" + "title": "Заказ", + "promotionsTotal": "Скидка", + "subtotal": "Промежуточный итог", + "total": "Сумма", + "giftCard": "Подарочная карта" }, "orderConfirmed": { "title": "Заказ успешно оформлен", From 8b3e58e833564afea237e60c1b4a8b640cf19aff Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:34:27 +0300 Subject: [PATCH 13/19] improve signup container --- app/auth/sign-up/page.tsx | 3 +- .../_components/update-account-form.tsx | 8 ++-- .../auth/src/components/auth-layout.tsx | 5 +-- .../auth/src/components/oauth-providers.tsx | 16 +++++--- .../components/password-sign-up-container.tsx | 22 +++++++++-- .../components/sign-up-methods-container.tsx | 37 +++++++++++++------ .../shared/src/components/ui/info-tooltip.tsx | 2 +- .../src/config/auth-providers.service.ts | 9 ++++- .../shared/src/config/dynamic-auth.config.ts | 12 ++++++ packages/shared/src/config/index.ts | 3 +- packages/ui/src/makerkit/page.tsx | 2 +- packages/ui/src/shadcn/card.tsx | 6 +-- public/locales/en/auth.json | 1 + public/locales/et/auth.json | 5 ++- public/locales/ru/auth.json | 1 + 15 files changed, 92 insertions(+), 40 deletions(-) diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 52d77e9..cdee3a5 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -57,10 +57,9 @@ 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 a657755..b807745 100644 --- a/app/auth/update-account/_components/update-account-form.tsx +++ b/app/auth/update-account/_components/update-account-form.tsx @@ -59,10 +59,10 @@ export function UpdateAccountForm({ const loading = toast.loading(t('updateAccount.updateAccountLoading')); try { const response = await onUpdateAccount({ - firstName: hasFirstName ? firstName : values.firstName, - lastName: hasLastName ? lastName : values.lastName, - personalCode: hasPersonalCode ? personalCode : values.personalCode, - email: hasEmail ? email : values.email, + 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, 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-up-methods-container.tsx b/packages/features/auth/src/components/sign-up-methods-container.tsx index 8759e0c..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,6 +1,7 @@ 'use client'; 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'; @@ -19,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(); @@ -37,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); + }} /> - + - +
@@ -70,7 +83,7 @@ export function SignUpMethodsContainer(props: {
{icon || } - {content} + {content} ); diff --git a/packages/shared/src/config/auth-providers.service.ts b/packages/shared/src/config/auth-providers.service.ts index 5cdb599..179e7da 100644 --- a/packages/shared/src/config/auth-providers.service.ts +++ b/packages/shared/src/config/auth-providers.service.ts @@ -5,6 +5,7 @@ type SupabaseExternalProvider = Provider | 'email'; interface SupabaseAuthSettings { external: Record; disable_signup: boolean; + mailer_autoconfirm: boolean; } export class AuthProvidersService { @@ -61,6 +62,10 @@ export class AuthProvidersService { 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'; } @@ -105,10 +110,11 @@ export class AuthProvidersService { async getAuthConfig() { const settings = await this.fetchAuthSettings(); - const [passwordEnabled, magicLinkEnabled, oAuthProviders] = await Promise.all([ + const [passwordEnabled, magicLinkEnabled, oAuthProviders, isMailerAutoconfirmEnabled] = await Promise.all([ this.isPasswordEnabled({ settings }), this.isMagicLinkEnabled(), this.getEnabledOAuthProviders({ settings }), + this.isMailerAutoconfirmEnabled({ settings }), ]); return { @@ -118,6 +124,7 @@ export class AuthProvidersService { oAuth: oAuthProviders, }, displayTermsCheckbox: authConfig.displayTermsCheckbox, + isMailerAutoconfirmEnabled, }; } diff --git a/packages/shared/src/config/dynamic-auth.config.ts b/packages/shared/src/config/dynamic-auth.config.ts index 428be33..571516a 100644 --- a/packages/shared/src/config/dynamic-auth.config.ts +++ b/packages/shared/src/config/dynamic-auth.config.ts @@ -11,8 +11,19 @@ const DynamicAuthConfigSchema = z.object({ 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(); @@ -20,6 +31,7 @@ export async function getDynamicAuthConfig() { const config = { providers: dynamicProviders.providers, displayTermsCheckbox: dynamicProviders.displayTermsCheckbox, + isMailerAutoconfirmEnabled: dynamicProviders.isMailerAutoconfirmEnabled, }; return DynamicAuthConfigSchema.parse(config); diff --git a/packages/shared/src/config/index.ts b/packages/shared/src/config/index.ts index 5669259..d1737bb 100644 --- a/packages/shared/src/config/index.ts +++ b/packages/shared/src/config/index.ts @@ -8,7 +8,7 @@ import { createPath, getTeamAccountSidebarConfig, } from './team-account-navigation.config'; -import { getCachedAuthConfig, getServerAuthConfig } from './dynamic-auth.config'; +import { DynamicAuthConfig, getCachedAuthConfig, getServerAuthConfig } from './dynamic-auth.config'; export { appConfig, @@ -21,4 +21,5 @@ export { personalAccountNavigationConfig, getCachedAuthConfig, getServerAuthConfig, + type DynamicAuthConfig, }; diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 6ca7573..b4ee923 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}
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/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/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/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", From 76433684e7ae43306c7284852bb1e39f073f502c Mon Sep 17 00:00:00 2001 From: Karli Date: Wed, 10 Sep 2025 06:34:34 +0300 Subject: [PATCH 14/19] improve cart mobile styles --- .../_components/cart/analysis-location.tsx | 13 ++++---- .../(user)/_components/cart/cart-item.tsx | 10 +++---- .../(user)/_components/cart/cart-items.tsx | 10 +++---- .../(user)/_components/cart/discount-code.tsx | 24 +++++++-------- app/home/(user)/_components/cart/index.tsx | 30 +++++++++---------- 5 files changed, 43 insertions(+), 44 deletions(-) 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" >