Skip to content

Stripe billing portal: card updates without the cancellation dance #198

Description

@isaacbowen

Motivating case: a subscriber wrote in needing to change their billing card (support: HS 3403436283). The settings page offers cancel-renewal and cancel-immediately, but no way to update a payment method — so the card-change path today is a cancellation dance, and "End access now? This cannot be undone." reads as a threat to their history even though it isn't one. From the reply that resolved it: "I think you just inspired a new Yours feature — this kind of thing should be easier."

The feature

A "Manage billing" button on settings, backed by Stripe's customer billing portal:

# session-derived, request-scoped, nothing stored — same identity law as everything else
portal = Stripe::BillingPortal::Session.create(
  customer: current_resonance.stripe_customer_id,
  return_url: settings_url,
)
redirect_to portal.url, allow_other_host: true, status: :see_other

This composes cleanly with the topology: the server can only mint a portal session while the person is signed in (their google_id arrives with the request and unlocks stripe_customer_id), so the portal is reachable by exactly the one person it's about — Stripe's representation of our representation of this user, behind their own key.

What it solves, by subscription state

  • Active, renewing: update card → future renewals ride the new card. The whole support class disappears.
  • Active, cancel_at_period_end (the motivating case's exact state): the portal natively offers "Renew subscription" for scheduled cancellations — add new card + un-schedule the cancellation = same subscription continues uninterrupted. No cancellation, no access gap, no scary buttons.
  • Fully canceled: Stripe cannot reactivate; the app's existing subscribe-fresh flow remains the path (and already works).

Config lens (dashboard toggles — the portal should be the card-drawer, not a second front door)

  • ON: payment method update, invoice history (harmless, maybe nice)
  • OFF: plan/tier switching — Yours' tier-change ritual (cancel renewal → optionally release → resubscribe at chosen tier) is deliberate product grammar and should stay the one door
  • OFF: portal cancellation — the app's own cancel flow has its own carefully-worded affect; two cancel doors with different words would split the voice

Verify in test mode before shipping

  1. Checkout-created subscriptions pin default_payment_method to the card used at checkout — confirm the portal's update-card flow updates the subscription's payment method (or offers to), not just the customer default, or renewals keep riding the old card.
  2. Whether the "Renew subscription" affordance for cancel_at_period_end subs appears independent of the portal-cancellation feature toggle.

Dovetail

The analytics/funnel work (GTM/Meta measurement) already brings Stripe webhooks + checkout-session touches onto the bench; this rides the same neighborhood and could share a PR cycle.

🤖 Written with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions