The account hub
When billing.enabled is on, the hub mounts under config('account.prefix') (default account/billing)
behind your web + auth middleware:
| Route | Screen |
|---|---|
/ | Overview + tier summary |
/subscription | State, next-invoice preview, cancel / resume, portal link |
/plan | Subscribe or in-app swap with a proration preview, and the coupon field |
/payment-methods | List, set-default, remove, add |
/invoices | History + ownership-checked PDF download |
/usage | Metered dimensions |
/usage/history | Past periods + add-on top-up timeline |
/recovery | Dunning recovery surface |
/danger | Immediate cancel |
/portal | Redirect to the provider's hosted billing portal |
/checkout/return | Where a customer lands coming back from the provider |
See Subscriptions for the subscribe/swap flow behind /plan.
What Subscribe does depends on the driver, and the screen does not change. Under a driver whose provider runs the cycle it opens that provider's hosted checkout. Under a driver this package bills itself it writes an intent and sends the customer to establish a mandate through a first payment; the subscription becomes real when that mandate settles, and until then the screen shows an activating state and polls.
/checkout/return follows the same split. Under a hosted checkout it reconciles the subscription from the
provider, which is a courtesy for a customer who beats the webhook home. Under a driver this package bills
itself there is nothing at the provider to read back, so it reconciles nothing and simply hands the customer
to the subscription screen. The webhook is the durable path either way.
The coupon field sits on /plan, and the status under it means one thing: would the driver you
configured actually apply this code? Not "does the code exist" — see
Subscriptions for which catalog answers under which
driver, and why a code can be valid in your configuration and still be reported as invalid.
Canceling can take two clicks if you want it to. account.cancel_requires_confirmation arms the
cancellation on /subscription and carries it out on the second click, with a way back in between. It is off
by default and the default does not move: canceling here is reversible, and for the person doing it less
friction is the better behavior.
Add the shell banner to your layout — it renders nothing for a healthy account:
<x-billing::banner />
Every screen renders inside a publishable app shell (layouts/account.blade.php): a grouped sidebar
navigation with the active item marked, a skip link to the main content, a typed document title, and a
POST-logout form shown only when your app registers a logout route. It needs no UI-kit dependency; publish
billing-views to replace it with your own design system's shell.
If an external merchant of record owns billing (an app-store subscription, an external portal), set
billing.link_out (env BILLING_LINK_OUT) to that portal's URL: the plan screen then links out to it and
suppresses the in-app checkout it is not the merchant of record for. On a native runtime, set
billing.runtime=native (env BILLING_RUNTIME) to hide flows an app store forbids in-app.
Hosting your own screens in the hub
The hub navigation is config-driven, and the hub hosts screens it does not own. To slot one of your own app
or auth screens — sessions, connections, set-password, an onboarding step — into the hub, register its route
in billing.navigation (an optional group places it in a labeled section,
billing::account.nav.group.<group>):
// config/billing.php
'navigation' => [
'sessions' => ['label' => 'account.sessions', 'route' => 'app.sessions', 'group' => 'account', 'order' => 60],
],
The entry appears in the navigation only once that route actually exists, so the same config can name a section your app builds later — an unregistered route (or one that needs parameters the hub cannot supply) is silently dropped, never rendered as a broken link. The package ships no ancillary-screen classes of its own; it only slots your route into the shell.