diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index a68752c..535fad8 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -2454,6 +2454,25 @@ export async function getTenantPaddleTransactions(cursor?: string): Promise<{ }; } +export async function createTenantPaddleCheckout( + packageId: number, + urls?: { success_url?: string; return_url?: string } +): Promise<{ checkout_url: string; id: string; expires_at?: string }> { + const response = await authorizedFetch('/api/v1/tenant/packages/paddle-checkout', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + package_id: packageId, + success_url: urls?.success_url, + return_url: urls?.return_url, + }), + }); + return await jsonOrThrow<{ checkout_url: string; id: string; expires_at?: string }>( + response, + 'Failed to create checkout' + ); +} + export async function createTenantBillingPortalSession(): Promise<{ url: string }> { const response = await authorizedFetch('/api/v1/tenant/billing/portal', { method: 'POST', diff --git a/resources/js/admin/mobile/BillingPage.tsx b/resources/js/admin/mobile/BillingPage.tsx index d9ebf2b..d91b9cb 100644 --- a/resources/js/admin/mobile/BillingPage.tsx +++ b/resources/js/admin/mobile/BillingPage.tsx @@ -14,6 +14,7 @@ import { getTenantPaddleTransactions, TenantPackageSummary, PaddleTransactionSummary, + createTenantPaddleCheckout, } from '../api'; import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api'; import { getApiErrorMessage } from '../lib/apiError'; @@ -40,6 +41,7 @@ export default function MobileBillingPage() { const [loading, setLoading] = React.useState(true); const [error, setError] = React.useState(null); const [portalBusy, setPortalBusy] = React.useState(false); + const [upgradeBusy, setUpgradeBusy] = React.useState(null); const packagesRef = React.useRef(null); const invoicesRef = React.useRef(null); const supportEmail = 'support@fotospiel.de'; @@ -95,6 +97,26 @@ export default function MobileBillingPage() { } }, [portalBusy, t]); + const handleUpgrade = React.useCallback(async (pkg: TenantPackageSummary) => { + if (upgradeBusy) return; + setUpgradeBusy(pkg.package_id); + + try { + const { checkout_url } = await createTenantPaddleCheckout(pkg.package_id, { + success_url: window.location.href, + return_url: window.location.href, + }); + + if (typeof window !== 'undefined') { + window.location.href = checkout_url; + } + } catch (err) { + const message = getApiErrorMessage(err, t('billing.errors.upgrade', 'Konnte Checkout nicht starten.')); + toast.error(message); + setUpgradeBusy(null); + } + }, [upgradeBusy, t]); + React.useEffect(() => { void load(); }, [load]); @@ -161,7 +183,12 @@ export default function MobileBillingPage() { {packages .filter((pkg) => !activePackage || pkg.id !== activePackage.id) .map((pkg) => ( - + handleUpgrade(pkg)} + upgradeBusy={upgradeBusy === pkg.package_id} + /> ))} )} @@ -265,12 +292,16 @@ function PackageCard({ isActive = false, onOpenPortal, portalBusy, + onUpgrade, + upgradeBusy, }: { pkg: TenantPackageSummary; label?: string; isActive?: boolean; onOpenPortal?: () => void; portalBusy?: boolean; + onUpgrade?: () => void; + upgradeBusy?: boolean; }) { const { t } = useTranslation('management'); const { border, primary, accentSoft, textStrong, muted } = useAdminTheme(); @@ -383,6 +414,14 @@ function PackageCard({ tone={isDanger ? 'danger' : 'primary'} /> ) : null} + {!isActive && onUpgrade ? ( + + ) : null} ); } diff --git a/resources/js/admin/mobile/EventAnalyticsPage.tsx b/resources/js/admin/mobile/EventAnalyticsPage.tsx index 3a60bc1..9cf61dd 100644 --- a/resources/js/admin/mobile/EventAnalyticsPage.tsx +++ b/resources/js/admin/mobile/EventAnalyticsPage.tsx @@ -66,7 +66,7 @@ export default function MobileEventAnalyticsPage() { navigate(adminPath('/mobile/billing'))} + onPress={() => navigate(adminPath('/mobile/billing#packages'))} />