fixed paddle locale
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { resolvePaddleLocale } from '../steps/PaymentStep';
|
||||
|
||||
describe('resolvePaddleLocale', () => {
|
||||
it('returns short locale when given region-specific tag', () => {
|
||||
expect(resolvePaddleLocale('de-DE')).toBe('de');
|
||||
expect(resolvePaddleLocale('en-US')).toBe('en');
|
||||
});
|
||||
|
||||
it('falls back to english when locale unsupported', () => {
|
||||
expect(resolvePaddleLocale('jp')).toBe('en');
|
||||
expect(resolvePaddleLocale('xx-YY')).toBe('en');
|
||||
expect(resolvePaddleLocale(undefined)).toBe('en');
|
||||
});
|
||||
|
||||
it('keeps supported locale codes untouched', () => {
|
||||
expect(resolvePaddleLocale('fr')).toBe('fr');
|
||||
expect(resolvePaddleLocale('es')).toBe('es');
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import { useCheckoutWizard } from "../WizardContext";
|
||||
import type { GoogleProfilePrefill } from '../types';
|
||||
import LoginForm, { AuthUserPayload } from "../../../auth/LoginForm";
|
||||
import RegisterForm, { RegisterSuccessPayload } from "../../../auth/RegisterForm";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import toast from 'react-hot-toast';
|
||||
import { LoaderCircle } from "lucide-react";
|
||||
|
||||
@@ -104,10 +104,20 @@ export const AuthStep: React.FC<AuthStepProps> = ({ privacyHtml, googleProfile,
|
||||
if (isAuthenticated && authUser) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert>
|
||||
<AlertTitle>{t('checkout.auth_step.already_logged_in_title')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('checkout.auth_step.already_logged_in_desc', { email: authUser?.email || '' })}
|
||||
<Alert className="border-primary/40 bg-primary/5">
|
||||
<AlertTitle className="text-xl font-semibold">
|
||||
{t('checkout.auth_step.already_logged_in_title')}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="space-y-2 text-base leading-relaxed">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="checkout.auth_step.already_logged_in_body"
|
||||
components={{ strong: <span className="font-semibold" /> }}
|
||||
values={{ email: authUser?.email || '' }}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('checkout.auth_step.already_logged_in_hint')}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { useCheckoutWizard } from "../WizardContext";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
interface ConfirmationStepProps {
|
||||
onViewProfile?: () => void;
|
||||
@@ -28,13 +28,24 @@ export const ConfirmationStep: React.FC<ConfirmationStepProps> = ({ onViewProfil
|
||||
window.location.href = '/event-admin';
|
||||
}, [onGoToAdmin]);
|
||||
|
||||
const packageName = selectedPackage?.name ?? '';
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Alert>
|
||||
<AlertTitle>{t('checkout.confirmation_step.welcome')}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t('checkout.confirmation_step.package_activated', { name: selectedPackage?.name || '' })}
|
||||
{t('checkout.confirmation_step.email_sent')}
|
||||
<Alert className="border-primary/40 bg-primary/5">
|
||||
<AlertTitle className="text-xl font-semibold">
|
||||
{t('checkout.confirmation_step.welcome')}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="space-y-2 text-base leading-relaxed">
|
||||
<Trans
|
||||
t={t}
|
||||
i18nKey="checkout.confirmation_step.package_summary"
|
||||
components={{ strong: <span className="font-semibold" /> }}
|
||||
values={{ name: packageName }}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('checkout.confirmation_step.email_followup')}
|
||||
</p>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="flex flex-wrap gap-3 justify-end">
|
||||
|
||||
@@ -22,6 +22,26 @@ declare global {
|
||||
}
|
||||
|
||||
const PADDLE_SCRIPT_URL = 'https://cdn.paddle.com/paddle/v2/paddle.js';
|
||||
const PADDLE_SUPPORTED_LOCALES = ['en', 'de', 'fr', 'es', 'it', 'nl', 'pt', 'sv', 'da', 'fi', 'no'];
|
||||
|
||||
export function resolvePaddleLocale(rawLocale?: string | null): string {
|
||||
if (!rawLocale) {
|
||||
return 'en';
|
||||
}
|
||||
|
||||
const normalized = rawLocale.toLowerCase();
|
||||
|
||||
if (PADDLE_SUPPORTED_LOCALES.includes(normalized)) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
const short = normalized.split('-')[0];
|
||||
if (short && PADDLE_SUPPORTED_LOCALES.includes(short)) {
|
||||
return short;
|
||||
}
|
||||
|
||||
return 'en';
|
||||
}
|
||||
|
||||
type PaddleEnvironment = 'sandbox' | 'production';
|
||||
|
||||
@@ -74,7 +94,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
|
||||
const { t } = useTranslation('marketing');
|
||||
|
||||
return (
|
||||
<Button size="lg" className="w-full" disabled={disabled} onClick={onCheckout}>
|
||||
<Button size="lg" className="w-full sm:w-auto" disabled={disabled} onClick={onCheckout}>
|
||||
{isProcessing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('checkout.payment_step.pay_with_paddle')}
|
||||
</Button>
|
||||
@@ -82,7 +102,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
|
||||
};
|
||||
|
||||
export const PaymentStep: React.FC = () => {
|
||||
const { t } = useTranslation('marketing');
|
||||
const { t, i18n } = useTranslation('marketing');
|
||||
const { selectedPackage, nextStep, paddleConfig, authUser, setPaymentCompleted } = useCheckoutWizard();
|
||||
const [status, setStatus] = useState<PaymentStatus>('idle');
|
||||
const [message, setMessage] = useState<string>('');
|
||||
@@ -92,6 +112,11 @@ export const PaymentStep: React.FC = () => {
|
||||
const eventCallbackRef = useRef<(event: any) => void>();
|
||||
const checkoutContainerClass = 'paddle-checkout-container';
|
||||
|
||||
const paddleLocale = useMemo(() => {
|
||||
const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null);
|
||||
return resolvePaddleLocale(sourceLocale ?? undefined);
|
||||
}, [i18n.language]);
|
||||
|
||||
const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]);
|
||||
|
||||
const handleFreeActivation = async () => {
|
||||
@@ -149,10 +174,11 @@ export const PaymentStep: React.FC = () => {
|
||||
frameInitialHeight: '550',
|
||||
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
|
||||
theme: 'light',
|
||||
locale: typeof document !== 'undefined' ? document.documentElement.lang ?? 'de' : 'de',
|
||||
locale: paddleLocale,
|
||||
},
|
||||
customData: {
|
||||
package_id: String(selectedPackage.id),
|
||||
locale: paddleLocale,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -182,6 +208,7 @@ export const PaymentStep: React.FC = () => {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
package_id: selectedPackage.id,
|
||||
locale: paddleLocale,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -283,6 +310,7 @@ export const PaymentStep: React.FC = () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.info('[Checkout] Initializing Paddle.js', { environment, hasToken: Boolean(clientToken) });
|
||||
}
|
||||
|
||||
paddle.Initialize({
|
||||
token: clientToken,
|
||||
checkout: {
|
||||
@@ -291,11 +319,12 @@ export const PaymentStep: React.FC = () => {
|
||||
frameTarget: checkoutContainerClass,
|
||||
frameInitialHeight: '550',
|
||||
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
|
||||
locale: typeof document !== 'undefined' ? document.documentElement.lang ?? 'de' : 'de',
|
||||
locale: paddleLocale,
|
||||
},
|
||||
},
|
||||
eventCallback: (event: any) => eventCallbackRef.current?.(event),
|
||||
});
|
||||
|
||||
inlineReady = true;
|
||||
}
|
||||
|
||||
@@ -313,7 +342,7 @@ export const PaymentStep: React.FC = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [paddleConfig?.environment, paddleConfig?.client_token, setPaymentCompleted, t]);
|
||||
}, [paddleConfig?.environment, paddleConfig?.client_token, paddleLocale, setPaymentCompleted, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setPaymentCompleted(false);
|
||||
@@ -345,42 +374,46 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('checkout.payment_step.paddle_intro')}
|
||||
</p>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border bg-card p-6 shadow-sm">
|
||||
<div className="space-y-4">
|
||||
{!inlineActive && (
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('checkout.payment_step.paddle_intro')}
|
||||
</p>
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
isProcessing={status === 'processing'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status !== 'idle' && (
|
||||
<Alert variant={status === 'error' ? 'destructive' : 'secondary'}>
|
||||
<AlertTitle>
|
||||
{status === 'processing'
|
||||
? t('checkout.payment_step.status_processing_title')
|
||||
: status === 'ready'
|
||||
? t('checkout.payment_step.status_ready_title')
|
||||
: status === 'error'
|
||||
? t('checkout.payment_step.status_error_title')
|
||||
: t('checkout.payment_step.status_info_title')}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex items-center gap-3">
|
||||
<span>{message}</span>
|
||||
{status === 'processing' && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{status !== 'idle' && (
|
||||
<Alert variant={status === 'error' ? 'destructive' : 'secondary'}>
|
||||
<AlertTitle>
|
||||
{status === 'processing'
|
||||
? t('checkout.payment_step.status_processing_title')
|
||||
: status === 'ready'
|
||||
? t('checkout.payment_step.status_ready_title')
|
||||
: status === 'error'
|
||||
? t('checkout.payment_step.status_error_title')
|
||||
: t('checkout.payment_step.status_info_title')}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex items-center gap-3">
|
||||
<span>{message}</span>
|
||||
{status === 'processing' && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="rounded-lg border bg-card p-6 shadow-sm space-y-4">
|
||||
<div className={`${checkoutContainerClass} min-h-[200px]`} />
|
||||
{!inlineActive && (
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
disabled={status === 'processing'}
|
||||
isProcessing={status === 'processing'}
|
||||
/>
|
||||
)}
|
||||
<div className={`${checkoutContainerClass} min-h-[360px]`} />
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t('checkout.payment_step.paddle_disclaimer')}
|
||||
</p>
|
||||
<p className={`text-xs text-muted-foreground ${inlineActive ? 'text-center' : 'sm:text-right'}`}>
|
||||
{t('checkout.payment_step.paddle_disclaimer')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user