Show billing activation banner
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled

This commit is contained in:
Codex Agent
2026-01-12 12:07:37 +01:00
parent 4bcaef53f7
commit b854e3feaa
6 changed files with 154 additions and 1 deletions

View File

@@ -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">