perfect-postcode/frontend/src/components/ui/UpgradeModal.tsx
2026-05-05 22:29:28 +01:00

140 lines
5.1 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { CloseIcon } from './icons/CloseIcon';
import { SpinnerIcon } from './icons/SpinnerIcon';
import { apiUrl, logNonAbortError } from '../../lib/api';
interface UpgradeModalProps {
isLoggedIn: boolean;
onLoginClick: () => void;
onRegisterClick: () => void;
onStartCheckout: () => Promise<void>;
onZoomToFreeZone: () => void;
/** When true, the user came in via a share link; relabel "Continue with demo"
* to "Back to shared area" since clicking it returns to the share's coords,
* not the central-London demo. */
isShareReturn?: boolean;
}
export default function UpgradeModal({
isLoggedIn,
onLoginClick,
onRegisterClick,
onStartCheckout,
onZoomToFreeZone,
isShareReturn,
}: UpgradeModalProps) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [pricePence, setPricePence] = useState<number | null>(null);
useEffect(() => {
fetch(apiUrl('pricing'))
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data) setPricePence(data.current_price_pence);
})
.catch((err) => logNonAbortError('Failed to fetch pricing', err));
}, []);
const priceLabel =
pricePence === null
? '...'
: pricePence === 0
? t('upgrade.free')
: `\u00A3${pricePence / 100}`;
const isFree = pricePence === 0;
const handleUpgrade = async () => {
setLoading(true);
setError(null);
try {
await onStartCheckout();
} catch (err) {
setError(err instanceof Error ? err.message : t('upgrade.checkoutFailed'));
} finally {
setLoading(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
<div className="relative w-full max-w-md mx-4 bg-white dark:bg-warm-800 rounded-2xl shadow-2xl border border-warm-200 dark:border-warm-700 overflow-hidden">
{/* Close button */}
<button
onClick={onZoomToFreeZone}
className="absolute top-3 right-3 text-warm-400 hover:text-warm-700 dark:hover:text-warm-300"
>
<CloseIcon className="w-5 h-5" />
</button>
{/* Header */}
<div className="bg-gradient-to-br from-navy-950 to-teal-900 px-6 py-8 text-center">
<h2 className="text-2xl font-bold text-white mb-2">{t('upgrade.title')}</h2>
<p className="text-warm-300 text-sm">
{isShareReturn ? t('upgrade.sharedAreaDescription') : t('upgrade.description')}
</p>
</div>
{/* Body */}
<div className="px-6 py-6">
<div className="flex items-baseline justify-center gap-1 mb-4">
<span className="text-4xl font-extrabold text-navy-950 dark:text-warm-100">
{priceLabel}
</span>
{!isFree && (
<span className="text-warm-500 dark:text-warm-400 text-lg">
{t('pricingPage.lifetime')}
</span>
)}
</div>
<p className="text-center text-sm text-warm-500 dark:text-warm-400 mb-6">
{isFree ? t('upgrade.freeForEarly') : t('upgrade.oneTimePayment')}
</p>
{isLoggedIn ? (
<button
onClick={handleUpgrade}
disabled={loading}
className="w-full px-6 py-3 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-lg shadow-lg shadow-coral-500/25 disabled:opacity-50 disabled:cursor-wait flex items-center justify-center gap-2"
>
{loading && <SpinnerIcon className="w-5 h-5 animate-spin" />}
{loading
? t('upgrade.redirecting')
: isFree
? t('upgrade.claimFreeAccess')
: t('upgrade.upgradeFor', { price: priceLabel })}
</button>
) : (
<div className="flex flex-col gap-3">
<button
onClick={onRegisterClick}
className="w-full px-6 py-3 bg-coral-500 text-white rounded-lg font-semibold hover:bg-coral-600 transition-colors text-lg shadow-lg shadow-coral-500/25"
>
{t('upgrade.registerAndUpgrade')}
</button>
<button
onClick={onLoginClick}
className="w-full px-4 py-2 text-sm text-teal-600 dark:text-teal-400 hover:text-teal-800 dark:hover:text-teal-300"
>
{t('upgrade.alreadyHaveAccount')}
</button>
</div>
)}
{error && (
<p className="mt-3 text-center text-sm text-red-600 dark:text-red-400">{error}</p>
)}
<button
onClick={onZoomToFreeZone}
className="w-full mt-4 text-center text-sm text-warm-400 dark:text-warm-500 hover:text-warm-600 dark:hover:text-warm-400"
>
{isShareReturn ? t('upgrade.backToSharedArea') : t('upgrade.continueWithDemo')}
</button>
</div>
</div>
</div>
);
}