140 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|