Show billing activation banner
This commit is contained in:
@@ -27,6 +27,9 @@ import {
|
||||
getPackageFeatureLabel,
|
||||
getPackageLimitEntries,
|
||||
} from './lib/packageSummary';
|
||||
import { PendingCheckout, PENDING_CHECKOUT_TTL_MS, shouldClearPendingCheckout } from './lib/billingCheckout';
|
||||
|
||||
const CHECKOUT_STORAGE_KEY = 'admin.billing.checkout.pending.v1';
|
||||
|
||||
export default function MobileBillingPage() {
|
||||
const { t } = useTranslation('management');
|
||||
@@ -40,6 +43,29 @@ export default function MobileBillingPage() {
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [portalBusy, setPortalBusy] = React.useState(false);
|
||||
const [pendingCheckout, setPendingCheckout] = React.useState<PendingCheckout | null>(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(CHECKOUT_STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as PendingCheckout;
|
||||
if (typeof parsed?.startedAt !== 'number') {
|
||||
return null;
|
||||
}
|
||||
const packageId =
|
||||
typeof parsed.packageId === 'number' && Number.isFinite(parsed.packageId)
|
||||
? parsed.packageId
|
||||
: null;
|
||||
const isExpired = Date.now() - parsed.startedAt > PENDING_CHECKOUT_TTL_MS;
|
||||
return isExpired ? null : { packageId, startedAt: parsed.startedAt };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const packagesRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const invoicesRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const supportEmail = 'support@fotospiel.de';
|
||||
@@ -95,6 +121,22 @@ export default function MobileBillingPage() {
|
||||
}
|
||||
}, [portalBusy, t]);
|
||||
|
||||
const persistPendingCheckout = React.useCallback((next: PendingCheckout | null) => {
|
||||
setPendingCheckout(next);
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!next) {
|
||||
window.sessionStorage.removeItem(CHECKOUT_STORAGE_KEY);
|
||||
} else {
|
||||
window.sessionStorage.setItem(CHECKOUT_STORAGE_KEY, JSON.stringify(next));
|
||||
}
|
||||
} catch {
|
||||
// Ignore storage errors.
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
@@ -115,17 +157,26 @@ export default function MobileBillingPage() {
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const checkout = params.get('checkout');
|
||||
const packageId = params.get('package_id');
|
||||
if (!checkout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkout === 'success') {
|
||||
const packageIdNumber = packageId ? Number(packageId) : null;
|
||||
const pendingEntry = {
|
||||
packageId: Number.isFinite(packageIdNumber) ? packageIdNumber : null,
|
||||
startedAt: Date.now(),
|
||||
};
|
||||
persistPendingCheckout(pendingEntry);
|
||||
toast.success(t('billing.checkoutSuccess', 'Checkout completed. Your package will activate shortly.'));
|
||||
} else if (checkout === 'cancel') {
|
||||
persistPendingCheckout(null);
|
||||
toast(t('billing.checkoutCancelled', 'Checkout was cancelled.'));
|
||||
}
|
||||
|
||||
params.delete('checkout');
|
||||
params.delete('package_id');
|
||||
navigate(
|
||||
{
|
||||
pathname: location.pathname,
|
||||
@@ -134,7 +185,17 @@ export default function MobileBillingPage() {
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
}, [location.hash, location.pathname, location.search, navigate, t]);
|
||||
}, [location.hash, location.pathname, location.search, navigate, persistPendingCheckout, t]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!pendingCheckout) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldClearPendingCheckout(pendingCheckout, activePackage?.package_id ?? null)) {
|
||||
persistPendingCheckout(null);
|
||||
}
|
||||
}, [activePackage?.package_id, pendingCheckout, persistPendingCheckout]);
|
||||
|
||||
return (
|
||||
<MobileShell
|
||||
@@ -155,6 +216,35 @@ export default function MobileBillingPage() {
|
||||
<CTAButton label={t('billing.actions.refresh', 'Refresh')} tone="ghost" onPress={load} />
|
||||
</MobileCard>
|
||||
) : null}
|
||||
{pendingCheckout ? (
|
||||
<MobileCard borderColor={accentSoft} backgroundColor={accentSoft} space="$2">
|
||||
<XStack alignItems="center" justifyContent="space-between">
|
||||
<YStack space="$0.5" flex={1}>
|
||||
<Text fontSize="$sm" fontWeight="800" color={textStrong}>
|
||||
{t('billing.checkoutPendingTitle', 'Activating your package')}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t(
|
||||
'billing.checkoutPendingBody',
|
||||
'This can take a few minutes. We will update this screen once the package is active.'
|
||||
)}
|
||||
</Text>
|
||||
</YStack>
|
||||
<PillBadge tone="warning">
|
||||
{t('billing.checkoutPendingBadge', 'Pending')}
|
||||
</PillBadge>
|
||||
</XStack>
|
||||
<XStack space="$2">
|
||||
<CTAButton label={t('billing.checkoutPendingRefresh', 'Refresh')} onPress={load} fullWidth={false} />
|
||||
<CTAButton
|
||||
label={t('billing.checkoutPendingDismiss', 'Dismiss')}
|
||||
tone="ghost"
|
||||
onPress={() => persistPendingCheckout(null)}
|
||||
fullWidth={false}
|
||||
/>
|
||||
</XStack>
|
||||
</MobileCard>
|
||||
) : null}
|
||||
|
||||
<MobileCard space="$2" ref={packagesRef as any}>
|
||||
<XStack alignItems="center" space="$2">
|
||||
|
||||
Reference in New Issue
Block a user