Skip to content

Commit 3b4308a

Browse files
committed
feat(payments): support custom cancel path for Stripe top-up checkout
Add an optional cancel-path query param to the /payments/topup route, validated with isValidReturnUrl to prevent open redirects. The claw CreditsNudge now passes cancel-path=/claw so users return to the onboarding page instead of /profile when they cancel checkout.
1 parent f3c21b8 commit 3b4308a

3 files changed

Lines changed: 16 additions & 5 deletions

File tree

src/app/(app)/claw/components/CreditsNudge.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export function CreditsNudge({
6262
{/* Hidden form for POST to /payments/topup */}
6363
<form
6464
ref={formRef}
65-
action={`/payments/topup?amount=${selectedAmount}`}
65+
action={`/payments/topup?amount=${selectedAmount}&cancel-path=${encodeURIComponent('/claw')}`}
6666
method="post"
6767
className="hidden"
6868
/>

src/app/payments/topup/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server';
33
import { getUserFromAuth } from '@/lib/user.server';
44
import { getStripeTopUpCheckoutUrl } from '@/lib/stripe';
55
import { MAXIMUM_TOP_UP_AMOUNT, MINIMUM_TOP_UP_AMOUNT } from '@/lib/constants';
6+
import { isValidReturnUrl } from '@/lib/payment-return-url';
67
import { captureException } from '@sentry/nextjs';
78
import { getOrCreateStripeCustomerIdForOrganization } from '@/lib/organizations/organization-billing';
89

@@ -67,12 +68,16 @@ export async function POST(request: NextRequest): Promise<NextResponse<unknown>>
6768
await getOrCreateStripeCustomerIdForOrganization(organizationId)
6869
: currentUser.stripe_customer_id;
6970

71+
const cancelPathRaw = searchParams.get('cancel-path');
72+
const cancelPath = cancelPathRaw && isValidReturnUrl(cancelPathRaw) ? cancelPathRaw : null;
73+
7074
const url = await getStripeTopUpCheckoutUrl(
7175
currentUser.id,
7276
stripeCustomerId,
7377
validationResult.amount as number,
7478
origin,
75-
organizationId
79+
organizationId,
80+
cancelPath
7681
);
7782

7883
if (!url) {

src/lib/stripe.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,9 @@ export async function getStripeTopUpCheckoutUrl(
993993
stripeCustomerId: User['stripe_customer_id'],
994994
amount: number,
995995
origin: string = 'web',
996-
organizationId?: string | null
996+
organizationId?: string | null,
997+
/** Optional internal path to redirect to when the user cancels checkout. */
998+
cancelPath?: string | null
997999
): Promise<string | null> {
9981000
const line_items = amount
9991001
? [
@@ -1016,9 +1018,13 @@ export async function getStripeTopUpCheckoutUrl(
10161018
];
10171019

10181020
const isOrganizationTopUp = Boolean(organizationId);
1019-
let cancelUrl = `${APP_URL}/profile?payment_status=topup_cancelled&origin=${origin}`;
1020-
if (isOrganizationTopUp) {
1021+
let cancelUrl: string;
1022+
if (cancelPath) {
1023+
cancelUrl = `${APP_URL}${cancelPath}`;
1024+
} else if (isOrganizationTopUp) {
10211025
cancelUrl = `${APP_URL}/organizations/${organizationId}?${TOPUP_CANCELED_QUERY_STRING_KEY}=true`;
1026+
} else {
1027+
cancelUrl = `${APP_URL}/profile?payment_status=topup_cancelled&origin=${origin}`;
10221028
}
10231029

10241030
const rewardfulReferral = await getRewardfulReferral();

0 commit comments

Comments
 (0)