feat: implement upgrade to premium flow
This commit adds the ability for tenants to upgrade their package directly from the mobile billing page. It includes: - New API function createTenantPaddleCheckout in api.ts - Upgrade handler and UI in BillingPage.tsx - Updated navigation in EventAnalyticsPage.tsx to link to the packages section of the billing page
This commit is contained in:
@@ -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 }> {
|
export async function createTenantBillingPortalSession(): Promise<{ url: string }> {
|
||||||
const response = await authorizedFetch('/api/v1/tenant/billing/portal', {
|
const response = await authorizedFetch('/api/v1/tenant/billing/portal', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
getTenantPaddleTransactions,
|
getTenantPaddleTransactions,
|
||||||
TenantPackageSummary,
|
TenantPackageSummary,
|
||||||
PaddleTransactionSummary,
|
PaddleTransactionSummary,
|
||||||
|
createTenantPaddleCheckout,
|
||||||
} from '../api';
|
} from '../api';
|
||||||
import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api';
|
import { TenantAddonHistoryEntry, getTenantAddonHistory } from '../api';
|
||||||
import { getApiErrorMessage } from '../lib/apiError';
|
import { getApiErrorMessage } from '../lib/apiError';
|
||||||
@@ -40,6 +41,7 @@ export default function MobileBillingPage() {
|
|||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const [portalBusy, setPortalBusy] = React.useState(false);
|
const [portalBusy, setPortalBusy] = React.useState(false);
|
||||||
|
const [upgradeBusy, setUpgradeBusy] = React.useState<number | null>(null);
|
||||||
const packagesRef = React.useRef<HTMLDivElement | null>(null);
|
const packagesRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const invoicesRef = React.useRef<HTMLDivElement | null>(null);
|
const invoicesRef = React.useRef<HTMLDivElement | null>(null);
|
||||||
const supportEmail = 'support@fotospiel.de';
|
const supportEmail = 'support@fotospiel.de';
|
||||||
@@ -95,6 +97,26 @@ export default function MobileBillingPage() {
|
|||||||
}
|
}
|
||||||
}, [portalBusy, t]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
void load();
|
void load();
|
||||||
}, [load]);
|
}, [load]);
|
||||||
@@ -161,7 +183,12 @@ export default function MobileBillingPage() {
|
|||||||
{packages
|
{packages
|
||||||
.filter((pkg) => !activePackage || pkg.id !== activePackage.id)
|
.filter((pkg) => !activePackage || pkg.id !== activePackage.id)
|
||||||
.map((pkg) => (
|
.map((pkg) => (
|
||||||
<PackageCard key={pkg.id} pkg={pkg} />
|
<PackageCard
|
||||||
|
key={pkg.id}
|
||||||
|
pkg={pkg}
|
||||||
|
onUpgrade={() => handleUpgrade(pkg)}
|
||||||
|
upgradeBusy={upgradeBusy === pkg.package_id}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</YStack>
|
</YStack>
|
||||||
)}
|
)}
|
||||||
@@ -265,12 +292,16 @@ function PackageCard({
|
|||||||
isActive = false,
|
isActive = false,
|
||||||
onOpenPortal,
|
onOpenPortal,
|
||||||
portalBusy,
|
portalBusy,
|
||||||
|
onUpgrade,
|
||||||
|
upgradeBusy,
|
||||||
}: {
|
}: {
|
||||||
pkg: TenantPackageSummary;
|
pkg: TenantPackageSummary;
|
||||||
label?: string;
|
label?: string;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
onOpenPortal?: () => void;
|
onOpenPortal?: () => void;
|
||||||
portalBusy?: boolean;
|
portalBusy?: boolean;
|
||||||
|
onUpgrade?: () => void;
|
||||||
|
upgradeBusy?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation('management');
|
const { t } = useTranslation('management');
|
||||||
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
|
const { border, primary, accentSoft, textStrong, muted } = useAdminTheme();
|
||||||
@@ -383,6 +414,14 @@ function PackageCard({
|
|||||||
tone={isDanger ? 'danger' : 'primary'}
|
tone={isDanger ? 'danger' : 'primary'}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{!isActive && onUpgrade ? (
|
||||||
|
<CTAButton
|
||||||
|
label={upgradeBusy ? t('billing.actions.upgradeBusy', 'Einen Moment...') : t('billing.actions.upgrade', 'Buy / Upgrade')}
|
||||||
|
onPress={onUpgrade}
|
||||||
|
disabled={upgradeBusy}
|
||||||
|
tone="primary"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function MobileEventAnalyticsPage() {
|
|||||||
</YStack>
|
</YStack>
|
||||||
<CTAButton
|
<CTAButton
|
||||||
label={t('analytics.upgradeAction', 'Upgrade to Premium')}
|
label={t('analytics.upgradeAction', 'Upgrade to Premium')}
|
||||||
onPress={() => navigate(adminPath('/mobile/billing'))}
|
onPress={() => navigate(adminPath('/mobile/billing#packages'))}
|
||||||
/>
|
/>
|
||||||
</MobileCard>
|
</MobileCard>
|
||||||
</MobileShell>
|
</MobileShell>
|
||||||
|
|||||||
Reference in New Issue
Block a user