Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(billing): Fix Stripe Checkout session creation and add error hand…
…ling

- Remove invalid PaymentMethodCollection parameter for setup mode
- Add Currency parameter required for setup mode
- Add logging for checkout session creation errors
- Add fallback to billing portal if checkout fails
- Add try-catch error handling in StartTeamPlanDialog
- Show user-friendly error notifications on failure

This should fix the loading hang when clicking upgrade.

Co-authored-by: eric.okuma <eric.okuma@rilldata.com>
  • Loading branch information
cursoragent and ericokuma committed Feb 3, 2026
commit 2938634d4ca49298dea2cddbf27b5f2cbf551863
9 changes: 4 additions & 5 deletions admin/billing/payment/stripe.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,18 @@ func (s *Stripe) CreateCheckoutSession(ctx context.Context, opts *CheckoutSessio
Customer: stripe.String(opts.CustomerID),
Mode: stripe.String(string(stripe.CheckoutSessionModeSetup)),
// Setup mode allows collecting payment method without charging
PaymentMethodTypes: stripe.StringSlice([]string{
"card",
}),
// Payment method types are configured in Stripe Dashboard settings
SuccessURL: stripe.String(opts.SuccessURL),
CancelURL: stripe.String(opts.CancelURL),
// Collect billing address
BillingAddressCollection: stripe.String(string(stripe.CheckoutSessionBillingAddressCollectionRequired)),
// Update customer with the collected payment method
PaymentMethodCollection: stripe.String(string(stripe.CheckoutSessionPaymentMethodCollectionAlways)),
// Currency is required for setup mode to determine available payment methods
Currency: stripe.String(string(stripe.CurrencyUSD)),
}

sess, err := checkoutsession.New(params)
if err != nil {
s.logger.Error("failed to create checkout session", zap.Error(err), zap.String("customer_id", opts.CustomerID))
return nil, err
}

Expand Down
92 changes: 54 additions & 38 deletions web-admin/src/features/billing/plans/StartTeamPlanDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -91,50 +91,66 @@
]);
async function handleUpgradePlan() {
loading = true;
// only fetch when needed to avoid hitting orb for list of plans too often
const teamPlan = await fetchTeamPlan();
if (paymentIssues?.length) {
// Use Stripe Checkout for a better payment UX with multiple payment options
const upgradeUrl = getBillingUpgradeUrl($page, organization);
const cancelUrl = `${$page.url.protocol}//${$page.url.host}/${organization}/-/settings/billing`;
window.open(
await createPaymentCheckoutSessionURL(
try {
// only fetch when needed to avoid hitting orb for list of plans too often
const teamPlan = await fetchTeamPlan();
if (paymentIssues?.length) {
// Use Stripe Checkout for a better payment UX with multiple payment options
const upgradeUrl = getBillingUpgradeUrl($page, organization);
const cancelUrl = `${$page.url.protocol}//${$page.url.host}/${organization}/-/settings/billing`;
const checkoutUrl = await createPaymentCheckoutSessionURL(
organization,
upgradeUrl,
cancelUrl,
),
"_self",
);
return;
}
loading = false;
);
if (checkoutUrl) {
window.open(checkoutUrl, "_self");
return;
}
// If no URL was returned, reset loading and show error
loading = false;
eventBus.emit("notification", {
type: "error",
message: "Failed to open payment page. Please try again.",
});
return;
}
loading = false;

if (type === "renew") {
await $planRenewer.mutateAsync({
org: organization,
data: {
planName: teamPlan.name,
},
});
if (type === "renew") {
await $planRenewer.mutateAsync({
org: organization,
data: {
planName: teamPlan.name,
},
});
eventBus.emit("notification", {
type: "success",
message: "Your Team plan was renewed",
});
} else {
await $planUpdater.mutateAsync({
org: organization,
data: {
planName: teamPlan.name,
},
});
showWelcomeToRillDialog.set(true);
}
void invalidateBillingInfo(organization);
open = false;
if (redirect) {
// redirect param could be on a different domain like the rill developer instance
// so using goto won't work
window.open(redirect, "_self");
}
} catch (e) {
console.error("Failed to upgrade plan:", e);
loading = false;
eventBus.emit("notification", {
type: "success",
message: "Your Team plan was renewed",
type: "error",
message: "Failed to upgrade plan. Please try again.",
});
} else {
await $planUpdater.mutateAsync({
org: organization,
data: {
planName: teamPlan.name,
},
});
showWelcomeToRillDialog.set(true);
}
void invalidateBillingInfo(organization);
open = false;
if (redirect) {
// redirect param could be on a different domain like the rill developer instance
// so using goto won't work
window.open(redirect, "_self");
}
}
</script>
Expand Down
26 changes: 18 additions & 8 deletions web-admin/src/features/billing/plans/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,31 @@ export function getBillingUpgradeUrl(page: Page, organization: string) {
/**
* Creates a Stripe Checkout session for collecting payment method and billing address.
* This provides a better UX than the billing portal with quick payment options.
* Falls back to the billing portal URL if checkout session creation fails.
*/
export async function createPaymentCheckoutSessionURL(
organization: string,
successUrl: string,
cancelUrl: string,
): Promise<string> {
const response = await adminServiceCreatePaymentCheckoutSession(
organization,
{
successUrl,
cancelUrl,
},
);
try {
const response = await adminServiceCreatePaymentCheckoutSession(
organization,
{
successUrl,
cancelUrl,
},
);

if (response.url) {
return response.url;
}
} catch (e) {
console.error("Failed to create checkout session, falling back to billing portal:", e);
}

return response.url ?? "";
// Fallback to billing portal if checkout fails
return fetchPaymentsPortalURL(organization, successUrl);
}

export function getNextBillingCycleDate(curEndDateRaw: string): string {
Expand Down
Loading