From cceed361b7dc4a799ccffeab48aa0cf01b3eb4af Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Mon, 12 Jan 2026 13:35:43 +0100 Subject: [PATCH] feat: add checkout action banner --- .../Controllers/Api/PackageController.php | 3 ++ resources/js/admin/api.ts | 4 +- .../js/admin/i18n/locales/de/management.json | 4 ++ .../js/admin/i18n/locales/en/management.json | 4 ++ resources/js/admin/mobile/BillingPage.tsx | 41 ++++++++++++++++++- .../TenantCheckoutSessionStatusTest.php | 6 ++- 6 files changed, 58 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Api/PackageController.php b/app/Http/Controllers/Api/PackageController.php index 60b3acb..ecbcfb2 100644 --- a/app/Http/Controllers/Api/PackageController.php +++ b/app/Http/Controllers/Api/PackageController.php @@ -239,10 +239,13 @@ class PackageController extends Controller } } + $checkoutUrl = data_get($session->provider_metadata ?? [], 'paddle_checkout_url'); + return response()->json([ 'status' => $session->status, 'completed_at' => optional($session->completed_at)->toIso8601String(), 'reason' => $reason, + 'checkout_url' => is_string($checkoutUrl) ? $checkoutUrl : null, ]); } diff --git a/resources/js/admin/api.ts b/resources/js/admin/api.ts index 3cc5138..f827c10 100644 --- a/resources/js/admin/api.ts +++ b/resources/js/admin/api.ts @@ -2476,9 +2476,9 @@ export async function createTenantPaddleCheckout( export async function getTenantPackageCheckoutStatus( checkoutSessionId: string, -): Promise<{ status: string; completed_at?: string | null; reason?: string | null }> { +): Promise<{ status: string; completed_at?: string | null; reason?: string | null; checkout_url?: string | null }> { const response = await authorizedFetch(`/api/v1/tenant/packages/checkout-session/${checkoutSessionId}/status`); - return await jsonOrThrow<{ status: string; completed_at?: string | null; reason?: string | null }>( + return await jsonOrThrow<{ status: string; completed_at?: string | null; reason?: string | null; checkout_url?: string | null }>( response, 'Failed to load checkout status' ); diff --git a/resources/js/admin/i18n/locales/de/management.json b/resources/js/admin/i18n/locales/de/management.json index 72607fb..7399551 100644 --- a/resources/js/admin/i18n/locales/de/management.json +++ b/resources/js/admin/i18n/locales/de/management.json @@ -47,6 +47,10 @@ "checkoutFailedBadge": "Fehlgeschlagen", "checkoutFailedRetry": "Erneut versuchen", "checkoutFailedDismiss": "Ausblenden", + "checkoutActionTitle": "Aktion erforderlich", + "checkoutActionBody": "Schließe die Zahlung ab, um das Paket zu aktivieren.", + "checkoutActionBadge": "Aktion nötig", + "checkoutActionButton": "Checkout fortsetzen", "checkoutFailureReasons": { "paddle_failed": "Die Zahlung wurde abgelehnt.", "paddle_cancelled": "Der Checkout wurde abgebrochen." diff --git a/resources/js/admin/i18n/locales/en/management.json b/resources/js/admin/i18n/locales/en/management.json index d2a3648..ada41d1 100644 --- a/resources/js/admin/i18n/locales/en/management.json +++ b/resources/js/admin/i18n/locales/en/management.json @@ -47,6 +47,10 @@ "checkoutFailedBadge": "Failed", "checkoutFailedRetry": "Try again", "checkoutFailedDismiss": "Dismiss", + "checkoutActionTitle": "Action required", + "checkoutActionBody": "Complete your payment to activate the package.", + "checkoutActionBadge": "Action needed", + "checkoutActionButton": "Continue checkout", "checkoutFailureReasons": { "paddle_failed": "The payment was declined.", "paddle_cancelled": "The checkout was cancelled." diff --git a/resources/js/admin/mobile/BillingPage.tsx b/resources/js/admin/mobile/BillingPage.tsx index 420fff3..dbfcd25 100644 --- a/resources/js/admin/mobile/BillingPage.tsx +++ b/resources/js/admin/mobile/BillingPage.tsx @@ -52,6 +52,7 @@ export default function MobileBillingPage() { const [pendingCheckout, setPendingCheckout] = React.useState(() => loadPendingCheckout()); const [checkoutStatus, setCheckoutStatus] = React.useState(null); const [checkoutStatusReason, setCheckoutStatusReason] = React.useState(null); + const [checkoutActionUrl, setCheckoutActionUrl] = React.useState(null); const lastCheckoutStatusRef = React.useRef(null); const packagesRef = React.useRef(null); const invoicesRef = React.useRef(null); @@ -179,6 +180,7 @@ export default function MobileBillingPage() { if (!pendingCheckout?.checkoutSessionId) { setCheckoutStatus(null); setCheckoutStatusReason(null); + setCheckoutActionUrl(null); lastCheckoutStatusRef.current = null; return; } @@ -194,6 +196,7 @@ export default function MobileBillingPage() { } setCheckoutStatus(result.status); setCheckoutStatusReason(result.reason ?? null); + setCheckoutActionUrl(typeof result.checkout_url === 'string' ? result.checkout_url : null); const lastStatus = lastCheckoutStatusRef.current; lastCheckoutStatusRef.current = result.status; @@ -290,7 +293,43 @@ export default function MobileBillingPage() { ) : null} - {pendingCheckout && checkoutStatus !== 'failed' && checkoutStatus !== 'cancelled' ? ( + {pendingCheckout && checkoutStatus === 'requires_customer_action' ? ( + + + + + {t('billing.checkoutActionTitle', 'Action required')} + + + {t('billing.checkoutActionBody', 'Complete your payment to activate the package.')} + + + + {t('billing.checkoutActionBadge', 'Action needed')} + + + + { + if (checkoutActionUrl && typeof window !== 'undefined') { + window.open(checkoutActionUrl, '_blank', 'noopener'); + return; + } + navigate(adminPath('/mobile/billing/shop')); + }} + fullWidth={false} + /> + persistPendingCheckout(null)} + fullWidth={false} + /> + + + ) : null} + {pendingCheckout && checkoutStatus !== 'failed' && checkoutStatus !== 'cancelled' && checkoutStatus !== 'requires_customer_action' ? ( diff --git a/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php b/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php index f8bc9bd..77e38de 100644 --- a/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php +++ b/tests/Feature/Tenant/TenantCheckoutSessionStatusTest.php @@ -21,6 +21,9 @@ class TenantCheckoutSessionStatusTest extends TenantTestCase 'package_id' => $package->id, 'status' => CheckoutSession::STATUS_FAILED, 'provider' => CheckoutSession::PROVIDER_PADDLE, + 'provider_metadata' => [ + 'paddle_checkout_url' => 'https://checkout.paddle.test/checkout/123', + ], 'status_history' => [ [ 'status' => CheckoutSession::STATUS_FAILED, @@ -37,6 +40,7 @@ class TenantCheckoutSessionStatusTest extends TenantTestCase $response->assertOk() ->assertJsonPath('status', CheckoutSession::STATUS_FAILED) - ->assertJsonPath('reason', 'paddle_failed'); + ->assertJsonPath('reason', 'paddle_failed') + ->assertJsonPath('checkout_url', 'https://checkout.paddle.test/checkout/123'); } }