Fix PayPal billing flow and mobile admin UX
This commit is contained in:
@@ -108,6 +108,34 @@ export function resolveCheckoutLocale(rawLocale?: string | null): string {
|
||||
return short || 'en';
|
||||
}
|
||||
|
||||
const PAYPAL_LOCALE_FALLBACKS: Record<string, string> = {
|
||||
de: 'DE',
|
||||
en: 'US',
|
||||
};
|
||||
|
||||
function resolvePayPalLocale(rawLocale?: string | null): string | null {
|
||||
if (!rawLocale) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = rawLocale.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = trimmed.replace('_', '-');
|
||||
const parts = normalized.split('-').filter(Boolean);
|
||||
|
||||
if (parts.length >= 2) {
|
||||
return `${parts[0].toLowerCase()}_${parts[1].toUpperCase()}`;
|
||||
}
|
||||
|
||||
const language = parts[0].toLowerCase();
|
||||
const region = PAYPAL_LOCALE_FALLBACKS[language];
|
||||
|
||||
return region ? `${language}_${region}` : null;
|
||||
}
|
||||
|
||||
type PayPalSdkOptions = {
|
||||
clientId: string;
|
||||
currency: string;
|
||||
@@ -134,8 +162,9 @@ async function loadPayPalSdk(options: PayPalSdkOptions): Promise<typeof window.p
|
||||
components: 'buttons',
|
||||
});
|
||||
|
||||
if (options.locale) {
|
||||
params.set('locale', options.locale);
|
||||
const paypalLocale = resolvePayPalLocale(options.locale);
|
||||
if (paypalLocale) {
|
||||
params.set('locale', paypalLocale);
|
||||
}
|
||||
|
||||
const src = `${PAYPAL_SDK_BASE}?${params.toString()}`;
|
||||
@@ -173,6 +202,9 @@ export const PaymentStep: React.FC = () => {
|
||||
setPaymentCompleted,
|
||||
checkoutSessionId,
|
||||
setCheckoutSessionId,
|
||||
checkoutActionUrl,
|
||||
setCheckoutActionUrl,
|
||||
clearCheckoutActionUrl,
|
||||
paypalConfig,
|
||||
} = useCheckoutWizard();
|
||||
const [status, setStatus] = useState<PaymentStatus>('idle');
|
||||
@@ -292,18 +324,18 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
}, [RateLimitHelper, selectedPackage, t, trackEvent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (couponCode && selectedPackage) {
|
||||
applyCoupon(couponCode);
|
||||
}
|
||||
}, [applyCoupon, couponCode, selectedPackage]);
|
||||
|
||||
useEffect(() => {
|
||||
setCouponPreview(null);
|
||||
setCouponNotice(null);
|
||||
setCouponError(null);
|
||||
}, [selectedPackage?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPackage) {
|
||||
clearCheckoutActionUrl();
|
||||
}
|
||||
}, [clearCheckoutActionUrl, selectedPackage?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
@@ -343,6 +375,7 @@ export const PaymentStep: React.FC = () => {
|
||||
|
||||
setConsentError(null);
|
||||
setFreeActivationBusy(true);
|
||||
clearCheckoutActionUrl();
|
||||
|
||||
try {
|
||||
await refreshCheckoutCsrfToken();
|
||||
@@ -469,6 +502,7 @@ export const PaymentStep: React.FC = () => {
|
||||
setMessage(t('checkout.payment_step.paypal_preparing'));
|
||||
setPaymentCompleted(false);
|
||||
setCheckoutSessionId(null);
|
||||
setCheckoutActionUrl(null);
|
||||
|
||||
await refreshCheckoutCsrfToken();
|
||||
|
||||
@@ -498,6 +532,11 @@ export const PaymentStep: React.FC = () => {
|
||||
checkoutSessionRef.current = payload.checkout_session_id;
|
||||
}
|
||||
|
||||
const approveUrl = typeof payload?.approve_url === 'string' ? payload.approve_url : null;
|
||||
if (approveUrl) {
|
||||
setCheckoutActionUrl(approveUrl);
|
||||
}
|
||||
|
||||
const orderId = payload?.order_id;
|
||||
if (!orderId) {
|
||||
throw new Error('PayPal order ID missing.');
|
||||
@@ -524,6 +563,11 @@ export const PaymentStep: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload?.status === 'requires_customer_action') {
|
||||
nextStep();
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus('error');
|
||||
setMessage(t('checkout.payment_step.paypal_error'));
|
||||
} catch (error) {
|
||||
@@ -554,7 +598,7 @@ export const PaymentStep: React.FC = () => {
|
||||
paypalButtonsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [authUser, checkoutLocale, goToStep, isAuthenticated, isFree, paypalConfig?.client_id, paypalConfig?.currency, paypalConfig?.intent, paypalConfig?.locale, selectedPackage, setCheckoutSessionId, setPaymentCompleted, t, handlePayPalCapture, nextStep]);
|
||||
}, [authUser, checkoutLocale, goToStep, isAuthenticated, isFree, paypalConfig?.client_id, paypalConfig?.currency, paypalConfig?.intent, paypalConfig?.locale, selectedPackage, setCheckoutActionUrl, setCheckoutSessionId, setPaymentCompleted, t, handlePayPalCapture, nextStep]);
|
||||
|
||||
useEffect(() => {
|
||||
if (paypalActionsRef.current) {
|
||||
@@ -593,6 +637,12 @@ export const PaymentStep: React.FC = () => {
|
||||
}
|
||||
}, [couponPreview, trackEvent]);
|
||||
|
||||
const handleOpenPayPal = useCallback(() => {
|
||||
if (checkoutActionUrl && typeof window !== 'undefined') {
|
||||
window.open(checkoutActionUrl, '_blank', 'noopener');
|
||||
}
|
||||
}, [checkoutActionUrl]);
|
||||
|
||||
const openWithdrawalModal = useCallback(async () => {
|
||||
setShowWithdrawalModal(true);
|
||||
|
||||
@@ -771,6 +821,21 @@ export const PaymentStep: React.FC = () => {
|
||||
<p className="text-xs text-white/70 text-center">
|
||||
{t('checkout.payment_step.guided_cta_hint')}
|
||||
</p>
|
||||
{checkoutActionUrl && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full border-white/50 bg-transparent text-white hover:bg-white/15 hover:text-white"
|
||||
onClick={handleOpenPayPal}
|
||||
>
|
||||
{t('checkout.payment_step.resume_paypal')}
|
||||
</Button>
|
||||
<p className="text-xs text-white/70 text-center">
|
||||
{t('checkout.payment_step.resume_hint')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user