Fix PayPal billing flow and mobile admin UX
This commit is contained in:
@@ -10,12 +10,12 @@ import { MobileShell, HeaderActionButton } from './components/MobileShell';
|
||||
import { MobileCard, CTAButton, PillBadge } from './components/Primitives';
|
||||
import { ContextHelpLink } from './components/ContextHelpLink';
|
||||
import {
|
||||
createTenantBillingPortalSession,
|
||||
getTenantBillingTransactions,
|
||||
getTenantPackagesOverview,
|
||||
getTenantLemonSqueezyTransactions,
|
||||
getTenantPackageCheckoutStatus,
|
||||
TenantPackageSummary,
|
||||
LemonSqueezyOrderSummary,
|
||||
TenantBillingTransactionSummary,
|
||||
downloadTenantBillingReceipt,
|
||||
} from '../api';
|
||||
import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api';
|
||||
import { getApiErrorMessage } from '../lib/apiError';
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
shouldClearPendingCheckout,
|
||||
storePendingCheckout,
|
||||
} from './lib/billingCheckout';
|
||||
import { triggerDownloadFromBlob } from './invite-layout/export-utils';
|
||||
|
||||
const CHECKOUT_POLL_INTERVAL_MS = 10000;
|
||||
|
||||
@@ -52,11 +53,10 @@ export default function MobileBillingPage() {
|
||||
const { textStrong, text, muted, subtle, danger, border, primary, accentSoft } = useAdminTheme();
|
||||
const [packages, setPackages] = React.useState<TenantPackageSummary[]>([]);
|
||||
const [activePackage, setActivePackage] = React.useState<TenantPackageSummary | null>(null);
|
||||
const [transactions, setTransactions] = React.useState<LemonSqueezyOrderSummary[]>([]);
|
||||
const [transactions, setTransactions] = React.useState<TenantBillingTransactionSummary[]>([]);
|
||||
const [addons, setAddons] = React.useState<TenantAddonHistoryEntry[]>([]);
|
||||
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>(() => loadPendingCheckout());
|
||||
const [checkoutStatus, setCheckoutStatus] = React.useState<string | null>(null);
|
||||
const [checkoutStatusReason, setCheckoutStatusReason] = React.useState<string | null>(null);
|
||||
@@ -78,7 +78,7 @@ export default function MobileBillingPage() {
|
||||
try {
|
||||
const [pkg, trx, addonHistory] = await Promise.all([
|
||||
getTenantPackagesOverview({ force: true }),
|
||||
getTenantLemonSqueezyTransactions().catch(() => ({ data: [] as LemonSqueezyOrderSummary[] })),
|
||||
getTenantBillingTransactions().catch(() => ({ data: [] as TenantBillingTransactionSummary[] })),
|
||||
getTenantAddonHistory().catch(() => ({ data: [] as TenantAddonHistoryEntry[] })),
|
||||
]);
|
||||
setPackages(pkg.packages ?? []);
|
||||
@@ -104,30 +104,32 @@ export default function MobileBillingPage() {
|
||||
}
|
||||
}, [supportEmail]);
|
||||
|
||||
const openPortal = React.useCallback(async () => {
|
||||
if (portalBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPortalBusy(true);
|
||||
try {
|
||||
const { url } = await createTenantBillingPortalSession();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.open(url, '_blank', 'noopener');
|
||||
}
|
||||
} catch (err) {
|
||||
const message = getApiErrorMessage(err, t('billing.errors.portal', 'Konnte das Lemon Squeezy-Portal nicht öffnen.'));
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setPortalBusy(false);
|
||||
}
|
||||
}, [portalBusy, t]);
|
||||
|
||||
const persistPendingCheckout = React.useCallback((next: PendingCheckout | null) => {
|
||||
setPendingCheckout(next);
|
||||
storePendingCheckout(next);
|
||||
}, []);
|
||||
|
||||
const handleReceiptDownload = React.useCallback(
|
||||
async (transaction: TenantBillingTransactionSummary) => {
|
||||
if (!transaction.receipt_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transactionId = transaction.provider_id
|
||||
?? (transaction.id !== null && transaction.id !== undefined ? String(transaction.id) : 'receipt');
|
||||
|
||||
try {
|
||||
const blob = await downloadTenantBillingReceipt(transaction.receipt_url);
|
||||
const filename = `fotospiel-receipt-${transactionId}.pdf`;
|
||||
triggerDownloadFromBlob(blob, filename);
|
||||
toast.success(t('billing.actions.receiptDownloaded', 'Receipt downloaded.'));
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('billing.errors.receipt', 'Receipt download failed.')));
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
@@ -387,11 +389,6 @@ export default function MobileBillingPage() {
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('billing.sections.packages.hint', 'Active package, limits, and history at a glance.')}
|
||||
</Text>
|
||||
<CTAButton
|
||||
label={portalBusy ? t('billing.actions.portalBusy', 'Öffne Portal...') : t('billing.actions.portal', 'Manage in Lemon Squeezy')}
|
||||
onPress={openPortal}
|
||||
disabled={portalBusy}
|
||||
/>
|
||||
{loading ? (
|
||||
<Text fontSize="$sm" color={muted}>
|
||||
{t('common.loading', 'Lädt...')}
|
||||
@@ -439,38 +436,55 @@ export default function MobileBillingPage() {
|
||||
</YStack>
|
||||
) : (
|
||||
<YStack gap="$1.5">
|
||||
{transactions.slice(0, 8).map((trx) => (
|
||||
<XStack key={trx.id} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor={border} paddingVertical="$1.5">
|
||||
<YStack>
|
||||
<Text fontSize="$sm" color={textStrong} fontWeight="700">
|
||||
{trx.status ?? '—'}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{formatDate(trx.created_at)}
|
||||
</Text>
|
||||
{trx.origin ? (
|
||||
<Text fontSize="$xs" color={subtle}>
|
||||
{trx.origin}
|
||||
{transactions.slice(0, 8).map((trx) => {
|
||||
const statusLabel = trx.status
|
||||
? t(`billing.sections.transactions.status.${trx.status}`, trx.status)
|
||||
: '—';
|
||||
const providerLabel = trx.provider
|
||||
? t(`billing.providers.${trx.provider}`, trx.provider)
|
||||
: t('billing.providers.unknown', 'Unknown');
|
||||
const transactionId = trx.provider_id ?? (trx.id !== null && trx.id !== undefined ? String(trx.id) : '—');
|
||||
const packageName = trx.package_name ?? t('billing.sections.transactions.labels.packageFallback', 'Package');
|
||||
|
||||
return (
|
||||
<XStack key={trx.id ?? transactionId} alignItems="center" justifyContent="space-between" borderBottomWidth={1} borderColor={border} paddingVertical="$1.5">
|
||||
<YStack>
|
||||
<Text fontSize="$sm" color={textStrong} fontWeight="700">
|
||||
{packageName}
|
||||
</Text>
|
||||
) : null}
|
||||
</YStack>
|
||||
<YStack alignItems="flex-end">
|
||||
<Text fontSize="$sm" color={textStrong} fontWeight="700">
|
||||
{formatAmount(trx.amount, trx.currency)}
|
||||
</Text>
|
||||
{trx.tax ? (
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('billing.sections.transactions.labels.tax', { value: formatAmount(trx.tax, trx.currency) })}
|
||||
{statusLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
{trx.receipt_url ? (
|
||||
<a href={trx.receipt_url} target="_blank" rel="noreferrer" style={{ fontSize: 12, color: primary }}>
|
||||
{t('billing.sections.transactions.labels.receipt', 'Beleg')}
|
||||
</a>
|
||||
) : null}
|
||||
</YStack>
|
||||
</XStack>
|
||||
))}
|
||||
<Text fontSize="$xs" color={subtle}>
|
||||
{t('billing.sections.transactions.labels.provider', { provider: providerLabel })}
|
||||
</Text>
|
||||
<Text fontSize="$xs" color={subtle}>
|
||||
{t('billing.sections.transactions.labels.transactionId', { id: transactionId })}
|
||||
</Text>
|
||||
</YStack>
|
||||
<YStack alignItems="flex-end">
|
||||
<Text fontSize="$sm" color={textStrong} fontWeight="700">
|
||||
{formatAmount(trx.amount, trx.currency)}
|
||||
</Text>
|
||||
{trx.tax ? (
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{t('billing.sections.transactions.labels.tax', { value: formatAmount(trx.tax, trx.currency) })}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text fontSize="$xs" color={muted}>
|
||||
{formatDate(trx.purchased_at)}
|
||||
</Text>
|
||||
{trx.receipt_url ? (
|
||||
<Pressable onPress={() => void handleReceiptDownload(trx)}>
|
||||
<Text fontSize="$xs" color={primary}>
|
||||
{t('billing.sections.transactions.labels.receipt', 'Beleg')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</YStack>
|
||||
</XStack>
|
||||
);
|
||||
})}
|
||||
</YStack>
|
||||
)}
|
||||
</MobileCard>
|
||||
|
||||
Reference in New Issue
Block a user