Migrate billing from Paddle to Lemon Squeezy
This commit is contained in:
@@ -19,22 +19,26 @@ import { useAnalytics } from '@/hooks/useAnalytics';
|
||||
|
||||
type PaymentStatus = 'idle' | 'processing' | 'ready' | 'error';
|
||||
|
||||
type LemonSqueezyEvent = {
|
||||
event?: string;
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Paddle?: {
|
||||
Environment?: {
|
||||
set: (environment: string) => void;
|
||||
};
|
||||
Initialize?: (options: { token: string }) => void;
|
||||
Checkout: {
|
||||
open: (options: Record<string, unknown>) => void;
|
||||
createLemonSqueezy?: () => void;
|
||||
LemonSqueezy?: {
|
||||
Setup: (options: { eventHandler?: (event: LemonSqueezyEvent) => void }) => void;
|
||||
Refresh?: () => void;
|
||||
Url: {
|
||||
Open: (url: string) => void;
|
||||
Close?: () => void;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
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'];
|
||||
const LEMON_SCRIPT_URL = 'https://app.lemonsqueezy.com/js/lemon.js';
|
||||
const PRIMARY_CTA_STYLES = 'min-w-[200px] disabled:bg-muted disabled:text-muted-foreground disabled:hover:bg-muted disabled:hover:text-muted-foreground';
|
||||
|
||||
const getCookieValue = (name: string): string | null => {
|
||||
@@ -101,73 +105,50 @@ function buildCheckoutHeaders(): HeadersInit {
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function resolvePaddleLocale(rawLocale?: string | null): string {
|
||||
export function resolveCheckoutLocale(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';
|
||||
return short || 'en';
|
||||
}
|
||||
|
||||
type PaddleEnvironment = 'sandbox' | 'production';
|
||||
let lemonLoaderPromise: Promise<typeof window.LemonSqueezy | null> | null = null;
|
||||
|
||||
let paddleLoaderPromise: Promise<typeof window.Paddle | null> | null = null;
|
||||
|
||||
function configurePaddle(paddle: typeof window.Paddle | undefined | null, environment: PaddleEnvironment): typeof window.Paddle | null {
|
||||
if (!paddle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
paddle.Environment?.set?.(environment);
|
||||
} catch (error) {
|
||||
console.warn('[Paddle] Failed to set environment', error);
|
||||
}
|
||||
|
||||
return paddle;
|
||||
}
|
||||
|
||||
async function loadPaddle(environment: PaddleEnvironment): Promise<typeof window.Paddle | null> {
|
||||
async function loadLemonSqueezy(): Promise<typeof window.LemonSqueezy | null> {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (window.Paddle) {
|
||||
return configurePaddle(window.Paddle, environment);
|
||||
if (window.LemonSqueezy) {
|
||||
return window.LemonSqueezy;
|
||||
}
|
||||
|
||||
if (!paddleLoaderPromise) {
|
||||
paddleLoaderPromise = new Promise<typeof window.Paddle | null>((resolve, reject) => {
|
||||
if (!lemonLoaderPromise) {
|
||||
lemonLoaderPromise = new Promise<typeof window.LemonSqueezy | null>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.src = PADDLE_SCRIPT_URL;
|
||||
script.async = true;
|
||||
script.onload = () => resolve(window.Paddle ?? null);
|
||||
script.src = LEMON_SCRIPT_URL;
|
||||
script.defer = true;
|
||||
script.onload = () => {
|
||||
window.createLemonSqueezy?.();
|
||||
resolve(window.LemonSqueezy ?? null);
|
||||
};
|
||||
script.onerror = (error) => reject(error);
|
||||
document.head.appendChild(script);
|
||||
}).catch((error) => {
|
||||
console.error('Failed to load Paddle.js', error);
|
||||
paddleLoaderPromise = null;
|
||||
console.error('Failed to load Lemon.js', error);
|
||||
lemonLoaderPromise = null;
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
const paddle = await paddleLoaderPromise;
|
||||
|
||||
return configurePaddle(paddle, environment);
|
||||
return lemonLoaderPromise;
|
||||
}
|
||||
|
||||
const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean; isProcessing: boolean; className?: string }> = ({ onCheckout, disabled, isProcessing, className }) => {
|
||||
const LemonSqueezyCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean; isProcessing: boolean; className?: string }> = ({ onCheckout, disabled, isProcessing, className }) => {
|
||||
const { t } = useTranslation('marketing');
|
||||
|
||||
return (
|
||||
@@ -178,7 +159,7 @@ const PaddleCta: React.FC<{ onCheckout: () => Promise<void>; disabled: boolean;
|
||||
onClick={onCheckout}
|
||||
>
|
||||
{isProcessing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('checkout.payment_step.pay_with_paddle')}
|
||||
{t('checkout.payment_step.pay_with_lemonsqueezy')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -189,7 +170,6 @@ export const PaymentStep: React.FC = () => {
|
||||
const {
|
||||
selectedPackage,
|
||||
nextStep,
|
||||
paddleConfig,
|
||||
authUser,
|
||||
isAuthenticated,
|
||||
goToStep,
|
||||
@@ -199,7 +179,6 @@ export const PaymentStep: React.FC = () => {
|
||||
} = useCheckoutWizard();
|
||||
const [status, setStatus] = useState<PaymentStatus>('idle');
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [initialised, setInitialised] = useState(false);
|
||||
const [inlineActive, setInlineActive] = useState(false);
|
||||
const [acceptedTerms, setAcceptedTerms] = useState(false);
|
||||
const [consentError, setConsentError] = useState<string | null>(null);
|
||||
@@ -220,11 +199,10 @@ export const PaymentStep: React.FC = () => {
|
||||
const [couponError, setCouponError] = useState<string | null>(null);
|
||||
const [couponNotice, setCouponNotice] = useState<string | null>(null);
|
||||
const [couponLoading, setCouponLoading] = useState(false);
|
||||
const paddleRef = useRef<typeof window.Paddle | null>(null);
|
||||
const checkoutContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const eventCallbackRef = useRef<(event: Record<string, unknown>) => void>();
|
||||
const lemonRef = useRef<typeof window.LemonSqueezy | null>(null);
|
||||
const eventHandlerRef = useRef<(event: LemonSqueezyEvent) => void>();
|
||||
const lastCheckoutIdRef = useRef<string | null>(null);
|
||||
const hasAutoAppliedCoupon = useRef(false);
|
||||
const checkoutContainerClass = 'paddle-checkout-container';
|
||||
const [showWithdrawalModal, setShowWithdrawalModal] = useState(false);
|
||||
const [withdrawalHtml, setWithdrawalHtml] = useState<string | null>(null);
|
||||
const [withdrawalTitle, setWithdrawalTitle] = useState<string | null>(null);
|
||||
@@ -235,23 +213,23 @@ export const PaymentStep: React.FC = () => {
|
||||
const [isGiftVoucher, setIsGiftVoucher] = useState(false);
|
||||
const [freeActivationBusy, setFreeActivationBusy] = useState(false);
|
||||
const [pendingConfirmation, setPendingConfirmation] = useState<{
|
||||
transactionId: string | null;
|
||||
orderId: string | null;
|
||||
checkoutId: string | null;
|
||||
} | null>(null);
|
||||
|
||||
const paddleLocale = useMemo(() => {
|
||||
const checkoutLocale = useMemo(() => {
|
||||
const sourceLocale = i18n.language || (typeof document !== 'undefined' ? document.documentElement.lang : null);
|
||||
return resolvePaddleLocale(sourceLocale ?? undefined);
|
||||
return resolveCheckoutLocale(sourceLocale ?? undefined);
|
||||
}, [i18n.language]);
|
||||
|
||||
const isFree = useMemo(() => (selectedPackage ? Number(selectedPackage.price) <= 0 : false), [selectedPackage]);
|
||||
|
||||
const confirmCheckoutSession = useCallback(async (payload: { transactionId: string | null; checkoutId: string | null }) => {
|
||||
const confirmCheckoutSession = useCallback(async (payload: { orderId: string | null; checkoutId: string | null }) => {
|
||||
if (!checkoutSessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload.transactionId && !payload.checkoutId) {
|
||||
if (!payload.orderId && !payload.checkoutId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -262,12 +240,12 @@ export const PaymentStep: React.FC = () => {
|
||||
headers: buildCheckoutHeaders(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
transaction_id: payload.transactionId,
|
||||
order_id: payload.orderId,
|
||||
checkout_id: payload.checkoutId,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to confirm Paddle session', error);
|
||||
console.warn('Failed to confirm Lemon Squeezy session', error);
|
||||
}
|
||||
}, [checkoutSessionId]);
|
||||
|
||||
@@ -398,14 +376,14 @@ export const PaymentStep: React.FC = () => {
|
||||
body: JSON.stringify({
|
||||
package_id: selectedPackage.id,
|
||||
accepted_terms: acceptedTerms,
|
||||
locale: paddleLocale,
|
||||
locale: checkoutLocale,
|
||||
}),
|
||||
});
|
||||
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = payload?.errors?.accepted_waiver?.[0] || payload?.message || t('checkout.payment_step.paddle_error');
|
||||
const errorMessage = payload?.errors?.accepted_waiver?.[0] || payload?.message || t('checkout.payment_step.lemonsqueezy_error');
|
||||
setConsentError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
@@ -416,7 +394,7 @@ export const PaymentStep: React.FC = () => {
|
||||
nextStep();
|
||||
} catch (error) {
|
||||
console.error('Failed to activate free package', error);
|
||||
const fallbackMessage = t('checkout.payment_step.paddle_error');
|
||||
const fallbackMessage = t('checkout.payment_step.lemonsqueezy_error');
|
||||
setConsentError(fallbackMessage);
|
||||
toast.error(fallbackMessage);
|
||||
} finally {
|
||||
@@ -424,7 +402,7 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const startPaddleCheckout = async () => {
|
||||
const startLemonSqueezyCheckout = async () => {
|
||||
if (!selectedPackage) {
|
||||
return;
|
||||
}
|
||||
@@ -443,43 +421,29 @@ export const PaymentStep: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedPackage.paddle_price_id) {
|
||||
if (!selectedPackage.lemonsqueezy_variant_id) {
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paddle_not_configured'));
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_not_configured'));
|
||||
return;
|
||||
}
|
||||
|
||||
setPaymentCompleted(false);
|
||||
setStatus('processing');
|
||||
setMessage(t('checkout.payment_step.paddle_preparing'));
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_preparing'));
|
||||
setInlineActive(false);
|
||||
setCheckoutSessionId(null);
|
||||
|
||||
try {
|
||||
await refreshCheckoutCsrfToken();
|
||||
const inlineSupported = initialised && !!paddleConfig?.client_token;
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.info('[Checkout] Paddle inline status', {
|
||||
inlineSupported,
|
||||
initialised,
|
||||
hasClientToken: Boolean(paddleConfig?.client_token),
|
||||
environment: paddleConfig?.environment,
|
||||
paddlePriceId: selectedPackage.paddle_price_id,
|
||||
});
|
||||
}
|
||||
|
||||
const response = await fetch('/paddle/create-checkout', {
|
||||
const response = await fetch('/lemonsqueezy/create-checkout', {
|
||||
method: 'POST',
|
||||
headers: buildCheckoutHeaders(),
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
package_id: selectedPackage.id,
|
||||
locale: paddleLocale,
|
||||
locale: checkoutLocale,
|
||||
coupon_code: couponPreview?.coupon.code ?? undefined,
|
||||
accepted_terms: acceptedTerms,
|
||||
inline: inlineSupported,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -498,14 +462,14 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.info('[Checkout] Hosted checkout response', { status: response.status, rawBody });
|
||||
console.info('[Checkout] Lemon Squeezy checkout response', { status: response.status, rawBody });
|
||||
}
|
||||
|
||||
let data: { checkout_url?: string; message?: string } | null = null;
|
||||
try {
|
||||
data = rawBody && rawBody.trim().startsWith('{') ? JSON.parse(rawBody) : null;
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse Paddle checkout payload as JSON', parseError);
|
||||
console.warn('Failed to parse Lemon Squeezy checkout payload as JSON', parseError);
|
||||
data = null;
|
||||
}
|
||||
|
||||
@@ -528,73 +492,32 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
|
||||
if (!response.ok || !checkoutUrl) {
|
||||
const message = data?.message || rawBody || 'Unable to create Paddle checkout.';
|
||||
if (response.ok && data && (data as { mode?: string }).mode === 'inline') {
|
||||
checkoutUrl = null;
|
||||
} else {
|
||||
throw new Error(message);
|
||||
}
|
||||
const message = data?.message || rawBody || 'Unable to create Lemon Squeezy checkout.';
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
if (data && (data as { mode?: string }).mode === 'inline') {
|
||||
const paddle = paddleRef.current;
|
||||
if (data && typeof (data as { id?: string }).id === 'string') {
|
||||
lastCheckoutIdRef.current = (data as { id?: string }).id ?? null;
|
||||
}
|
||||
|
||||
if (!paddle || !paddle.Checkout || typeof paddle.Checkout.open !== 'function') {
|
||||
throw new Error('Inline Paddle checkout is not available.');
|
||||
}
|
||||
|
||||
const inlinePayload: Record<string, unknown> = {
|
||||
items: (data as { items?: unknown[] }).items ?? [],
|
||||
settings: {
|
||||
displayMode: 'inline',
|
||||
frameTarget: checkoutContainerClass,
|
||||
frameInitialHeight: '550',
|
||||
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
|
||||
theme: 'light',
|
||||
locale: paddleLocale,
|
||||
},
|
||||
};
|
||||
|
||||
if ((data as { custom_data?: Record<string, unknown> }).custom_data) {
|
||||
inlinePayload.customData = (data as { custom_data?: Record<string, unknown> }).custom_data;
|
||||
}
|
||||
|
||||
if ((data as { customer?: Record<string, unknown> }).customer) {
|
||||
inlinePayload.customer = (data as { customer?: Record<string, unknown> }).customer;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.info('[Checkout] Opening inline Paddle checkout', inlinePayload);
|
||||
}
|
||||
|
||||
paddle.Checkout.open(inlinePayload);
|
||||
const lemon = await loadLemonSqueezy();
|
||||
|
||||
if (lemon?.Url?.Open) {
|
||||
lemon.Url.Open(checkoutUrl);
|
||||
setInlineActive(true);
|
||||
setStatus('ready');
|
||||
setMessage(t('checkout.payment_step.paddle_overlay_ready'));
|
||||
if (typeof window !== 'undefined' && checkoutContainerRef.current) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const rect = checkoutContainerRef.current?.getBoundingClientRect();
|
||||
if (!rect) {
|
||||
return;
|
||||
}
|
||||
const offset = 120;
|
||||
const target = Math.max(window.scrollY + rect.top - offset, 0);
|
||||
window.scrollTo({ top: target, behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_overlay_ready'));
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(checkoutUrl, '_blank', 'noopener');
|
||||
setInlineActive(false);
|
||||
setStatus('ready');
|
||||
setMessage(t('checkout.payment_step.paddle_ready'));
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_ready'));
|
||||
} catch (error) {
|
||||
console.error('Failed to start Paddle checkout', error);
|
||||
console.error('Failed to start Lemon Squeezy checkout', error);
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paddle_error'));
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_error'));
|
||||
setInlineActive(false);
|
||||
setPaymentCompleted(false);
|
||||
}
|
||||
@@ -603,85 +526,50 @@ export const PaymentStep: React.FC = () => {
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
const environment = paddleConfig?.environment === 'sandbox' ? 'sandbox' : 'production';
|
||||
const clientToken = paddleConfig?.client_token ?? null;
|
||||
|
||||
eventCallbackRef.current = (event) => {
|
||||
if (!event?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.debug('[Checkout] Paddle event', event);
|
||||
}
|
||||
|
||||
if (event.name === 'checkout.completed') {
|
||||
const transactionId = typeof event?.data?.transaction_id === 'string' ? event.data.transaction_id : null;
|
||||
const checkoutId = typeof event?.data?.id === 'string' ? event.data.id : null;
|
||||
setStatus('processing');
|
||||
setMessage(t('checkout.payment_step.processing_confirmation'));
|
||||
setInlineActive(false);
|
||||
setPaymentCompleted(false);
|
||||
setPendingConfirmation({ transactionId, checkoutId });
|
||||
toast.success(t('checkout.payment_step.toast_success'));
|
||||
setPaymentCompleted(true);
|
||||
nextStep();
|
||||
}
|
||||
|
||||
if (event.name === 'checkout.closed') {
|
||||
setStatus('idle');
|
||||
setMessage('');
|
||||
setInlineActive(false);
|
||||
}
|
||||
|
||||
if (event.name === 'checkout.error') {
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paddle_error'));
|
||||
setInlineActive(false);
|
||||
setPaymentCompleted(false);
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const paddle = await loadPaddle(environment);
|
||||
const lemon = await loadLemonSqueezy();
|
||||
|
||||
if (cancelled || !paddle) {
|
||||
if (cancelled || !lemon) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let inlineReady = false;
|
||||
if (typeof paddle.Initialize === 'function' && clientToken) {
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.info('[Checkout] Initializing Paddle.js', { environment, hasToken: Boolean(clientToken) });
|
||||
eventHandlerRef.current = (event) => {
|
||||
if (!event?.event) {
|
||||
return;
|
||||
}
|
||||
|
||||
paddle.Initialize({
|
||||
token: clientToken,
|
||||
checkout: {
|
||||
settings: {
|
||||
displayMode: 'inline',
|
||||
frameTarget: checkoutContainerClass,
|
||||
frameInitialHeight: '550',
|
||||
frameStyle: 'width: 100%; min-width: 320px; background-color: transparent; border: none;',
|
||||
locale: paddleLocale,
|
||||
},
|
||||
},
|
||||
eventCallback: (event: Record<string, unknown>) => eventCallbackRef.current?.(event),
|
||||
});
|
||||
if (typeof window !== 'undefined') {
|
||||
|
||||
console.debug('[Checkout] Lemon Squeezy event', event);
|
||||
}
|
||||
|
||||
inlineReady = true;
|
||||
}
|
||||
if (event.event === 'Checkout.Success') {
|
||||
const data = event.data as { id?: string; identifier?: string; attributes?: { checkout_id?: string } } | undefined;
|
||||
const orderId = typeof data?.id === 'string' ? data.id : (typeof data?.identifier === 'string' ? data.identifier : null);
|
||||
const checkoutId = typeof data?.attributes?.checkout_id === 'string'
|
||||
? data?.attributes?.checkout_id
|
||||
: lastCheckoutIdRef.current;
|
||||
setStatus('processing');
|
||||
setMessage(t('checkout.payment_step.processing_confirmation'));
|
||||
setInlineActive(false);
|
||||
setPaymentCompleted(false);
|
||||
setPendingConfirmation({ orderId, checkoutId });
|
||||
toast.success(t('checkout.payment_step.toast_success'));
|
||||
setPaymentCompleted(true);
|
||||
nextStep();
|
||||
}
|
||||
};
|
||||
|
||||
paddleRef.current = paddle;
|
||||
setInitialised(inlineReady);
|
||||
lemon.Setup({
|
||||
eventHandler: (event) => eventHandlerRef.current?.(event),
|
||||
});
|
||||
|
||||
lemonRef.current = lemon;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Paddle', error);
|
||||
setInitialised(false);
|
||||
console.error('Failed to initialize Lemon.js', error);
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paddle_error'));
|
||||
setMessage(t('checkout.payment_step.lemonsqueezy_error'));
|
||||
setPaymentCompleted(false);
|
||||
}
|
||||
})();
|
||||
@@ -689,7 +577,7 @@ export const PaymentStep: React.FC = () => {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [nextStep, paddleConfig?.environment, paddleConfig?.client_token, paddleLocale, setPaymentCompleted, t]);
|
||||
}, [nextStep, setPaymentCompleted, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setPaymentCompleted(false);
|
||||
@@ -747,7 +635,7 @@ export const PaymentStep: React.FC = () => {
|
||||
setWithdrawalError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/v1/legal/widerrufsbelehrung?lang=${paddleLocale}`);
|
||||
const response = await fetch(`/api/v1/legal/widerrufsbelehrung?lang=${checkoutLocale}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load withdrawal page (${response.status})`);
|
||||
}
|
||||
@@ -759,7 +647,7 @@ export const PaymentStep: React.FC = () => {
|
||||
} finally {
|
||||
setWithdrawalLoading(false);
|
||||
}
|
||||
}, [paddleLocale, t, withdrawalHtml, withdrawalLoading]);
|
||||
}, [checkoutLocale, t, withdrawalHtml, withdrawalLoading]);
|
||||
|
||||
|
||||
if (!selectedPackage) {
|
||||
@@ -842,16 +730,10 @@ export const PaymentStep: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const PaddleLogo = () => (
|
||||
const LemonSqueezyLogo = () => (
|
||||
<div className="flex items-center gap-3 rounded-full bg-white/15 px-4 py-2 text-white shadow-inner backdrop-blur-sm">
|
||||
<img
|
||||
src="/paddle.logo.svg"
|
||||
alt="Paddle"
|
||||
className="h-6 w-auto brightness-0 invert"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<span className="text-xs font-semibold">{t('checkout.payment_step.paddle_partner')}</span>
|
||||
<span className="text-sm font-semibold tracking-wide">Lemon Squeezy</span>
|
||||
<span className="text-xs font-semibold">{t('checkout.payment_step.lemonsqueezy_partner')}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -863,7 +745,7 @@ export const PaymentStep: React.FC = () => {
|
||||
<div className="overflow-hidden rounded-2xl border bg-gradient-to-br from-[#001835] via-[#002b55] to-[#00407c] p-6 text-white shadow-md">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="space-y-4">
|
||||
<PaddleLogo />
|
||||
<LemonSqueezyLogo />
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-2xl font-semibold">{t('checkout.payment_step.guided_title')}</h3>
|
||||
<p className="text-sm text-white/80">{t('checkout.payment_step.guided_body')}</p>
|
||||
@@ -919,8 +801,8 @@ export const PaymentStep: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
<LemonSqueezyCta
|
||||
onCheckout={startLemonSqueezyCheckout}
|
||||
disabled={status === 'processing' || !acceptedTerms}
|
||||
isProcessing={status === 'processing'}
|
||||
className={cn('bg-white text-[#001835] hover:bg-white/90', PRIMARY_CTA_STYLES)}
|
||||
@@ -1012,10 +894,10 @@ export const PaymentStep: React.FC = () => {
|
||||
{!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')}
|
||||
{t('checkout.payment_step.lemonsqueezy_intro')}
|
||||
</p>
|
||||
<PaddleCta
|
||||
onCheckout={startPaddleCheckout}
|
||||
<LemonSqueezyCta
|
||||
onCheckout={startLemonSqueezyCheckout}
|
||||
disabled={status === 'processing' || !acceptedTerms}
|
||||
isProcessing={status === 'processing'}
|
||||
className={PRIMARY_CTA_STYLES}
|
||||
@@ -1041,10 +923,8 @@ export const PaymentStep: React.FC = () => {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div ref={checkoutContainerRef} className={`${checkoutContainerClass} min-h-[360px]`} />
|
||||
|
||||
<p className={`text-xs text-muted-foreground ${inlineActive ? 'text-center' : 'sm:text-right'}`}>
|
||||
{t('checkout.payment_step.paddle_disclaimer')}
|
||||
{t('checkout.payment_step.lemonsqueezy_disclaimer')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user