MagicStarterBillingController class

Backs the billing screen: six independent reads of what a customer is entitled to, what they have spent, and where they manage it.

Why this controller does NOT use MagicStateMixin

Every sibling controller in this package mixes in MagicStateMixin, and this one deliberately does not. That mixin holds ONE T? _state slot and one RxStatus, and both of its transitions null the state: setLoading() calls setState(null, ...) (magic/lib/src/http/magic_controller.dart:177-179) and so does setError() (:195-197). This screen runs six independent reads whose answers have nothing to do with each other, so routing them through one slot would mean any single failing read wipes the other five: an invoices timeout would blank the plan the customer is paying for. Each read therefore publishes its own field and this controller notifies through refreshUI instead.

Every read degrades, none of them throws

A read that fails leaves its own field at last-known state and logs. The screen is a set of independent cards, and a card with no data is recoverable where an exception out of a read takes the whole screen with it. The payment method additionally keeps its OWN pmLoading and pmError, because it is the one read that dials a payment rail live, so a slow or broken rail must gate that card and nothing else.

What the consumer has to supply

usageCopy is REQUIRED, and that is a decision rather than an oversight. Both of the defaults an optional parameter could carry are defects: a pass-through would put the raw wire key requests_this_month on a customer's screen, and a drop would silently remove the usage surface from every app that forgot to pass one. Neither may be reachable by forgetting.

The other two collaborators are optional, and leaving one out is answered in OPPOSITE directions, which is a decision rather than an inconsistency. No MagicStarterTeamOwnershipReader leaves ownership unresolved and every gate permissive, because hiding a purchase button from a real owner stands between them and paying while the server refuses a non-owner regardless. No MagicStarterStoreFundedTeamReader REFUSES the store purchase, because nothing in that build can promise a second purchase will not transfer another team's subscription away, and a customer cannot undo that.

Because it takes required collaborators, this controller is registered with Magic.put(...) rather than resolved through the Magic.findOrPut singleton getter the sibling controllers expose; there is no zero-argument constructor for findOrPut to call.

Example

Magic.put(
  MagicStarterBillingController(
    usageCopy: withUsageCopy,
    storeFundedTeamReader: readStoreFundedTeam,
    isOwnerReader: readTeamOwnership,
  ),
);
Inheritance

Constructors

MagicStarterBillingController({required MagicStarterUsageCopy usageCopy, required MagicStarterNumberFormat formatNumber, MagicStarterStoreFundedTeamReader? storeFundedTeamReader, MagicStarterTeamOwnershipReader? isOwnerReader, @visibleForTesting BillingService? billingService})
Creates the billing controller.

Properties

awaitingProductKey → String?
The catalogue key of a store purchase the backend has not confirmed yet, or null when nothing is waiting.
no setter
billing → BillingService
The five entitlement READS: the injected contract when there is one, else the rail this build resolved through Payments.billing.
latefinal
canPurchase → bool
Whether this screen may offer to start or change a paid plan on ANY rail.
no setter
canPurchaseViaStore → bool
Whether this screen may offer to buy through the STORE rail.
no setter
canPurchaseViaWeb → bool
Whether this screen may offer to start or change a paid plan through the WEB rail.
no setter
currentPlanId → String?
The active plan id, or null while it is genuinely unknown: before loadEntitlement resolves, and permanently after a failed read.
no setter
cycle → BillingCycle?
How often the customer is charged, or null when nothing has said.
no setter
entitlementLoaded → bool
Whether the live entitlement read has resolved a plan, so currentPlanId is the customer's real tier rather than an unanswered read.
no setter
entitlementSnapshot → MagicStarterEntitlementSnapshot
The entitlement as the last read answered it. See MagicStarterEntitlementSnapshot for why it is read as a whole.
no setter
formatNumber → MagicStarterNumberFormat
Renders every integer this screen shows, in the consumer's locale.
final
hashCode → int
The hash code for this object.
no setterinherited
hasListeners → bool
Whether any listeners are currently registered.
no setterinherited
initialized → bool
Whether the controller has been initialized.
no setterinherited
invoicePages → MagicPaginator<Invoice>?
The paginator behind invoices, or null before the first load.
no setter
invoices → List<Invoice>
The customer's billing history, most recent first. Empty until loadInvoices resolves; stays empty on a failed read.
no setter
isDisposed → bool
Whether the controller has been disposed.
no setterinherited
isOwner → bool?
Whether the signed-in user owns the team, or null when that is genuinely unresolved.
no setter
isOwnerReader → MagicStarterTeamOwnershipReader?
The consumer's ownership check, or null when the consumer registered none.
final
manageUrl → String?
The store-management destination the server passed through from the rail, or null when the rail reported none (which is always, on Stripe).
no setter
manageVia → ManageVia?
The management surface the server computed from the rail, or null while no entitlement read has resolved one.
no setter
paymentMethod → PaymentMethod?
The card on file and the next renewal date, or null until loadPaymentMethod resolves.
no setter
plans → List<MagicStarterPlan>
The plan catalogue, in the order the backend served it (cheapest first).
no setter
planStatus → PlanStatus
Where the paid plan stands in its lifecycle, or PlanStatus.none before any entitlement read has answered.
no setter
pmError → bool
Whether loadPaymentMethod failed at the transport level (a network error, a non-2xx, or a payload this build cannot read).
no setter
pmLoading → bool
Whether loadPaymentMethod is still in flight.
no setter
portalAvailable → bool
Whether the web billing portal is a surface this caller can reach.
no setter
purchaseContext → PurchaseContext
The catalogue facts a store purchase needs to tell an upgrade from a downgrade: the tiers in the catalogue's own order (cheapest first), the tier of every product key the rows list, and the tier of every store product id they name.
no setter
renews → bool?
Whether the subscription will renew at the end of its paid period, or null while no entitlement read has answered.
no setter
runtimeType → Type
A representation of the runtime type of the object.
no setterinherited
storeCheckRegistered → bool
Whether the consumer registered a storeFundedTeamReader at all.
no setter
storeFundedTeam → String?
The NAME of another of the caller's teams that a store account already funds, or null when none does and while the read has not resolved.
no setter
storeFundedTeamReader → MagicStarterStoreFundedTeamReader?
The consumer's cross-team store check, or null when the consumer registered none.
final
storeManaged → bool
Whether a store owns this subscription, so the store owns its management too and nothing here may offer a competing purchase or a web billing surface.
no setter
storeOffers → Map<String, StoreProductOffer>
What the store charges for each catalogue product, keyed by catalogue key.
no setter
storeRail → StoreBillingService?
The STORE rail, or null in a build that cannot serve one.
latefinal
usage → List<UsageStat>
The current cycle's usage stats, with the consumer's copy already paired on by usageCopy.
no setter
usageCopy → MagicStarterUsageCopy
Pairs the consumer's display copy onto every UsageStat the producer reported. Required; see the class docblock for why it has no default.
final
webRail → WebBillingService?
The WEB rail, or null in a build that cannot serve one.
latefinal

Methods

addListener(VoidCallback listener) → void
Register a closure to be called when the object changes.
inherited
authorize(String ability, [Object? arguments]) → void
Authorize an action against the current Auth.user via the Gate.
inherited
cancelWait() → void
Stops a running wait and releases whoever is awaiting it with MagicStarterStorePurchaseOutcome.abandoned.
dispose() → void
Runs onClose if it has not run yet, then releases the notifier.
load() → Future<void>
Dispatches all six reads AT ONCE and resolves when the last one settles.
loadEntitlement() → Future<void>
Reads the customer's current entitlement and republishes currentPlanId, manageVia, manageUrl and renews.
loadInvoices() → Future<void>
Reads the first page of the customer's billing history and republishes invoices.
loadPaymentMethod() → Future<void>
Reads the card on file and republishes paymentMethod.
loadPlans() → Future<void>
Reads the plan catalogue, decodes each row into a MagicStarterPlan and republishes plans.
loadStoreFundedTeam() → Future<void>
Asks the consumer's storeFundedTeamReader whether a store account already funds another of the caller's teams, and republishes storeFundedTeam with its name when one does.
loadStoreProducts() → Future<void>
Asks the store what it charges for every SELLABLE product in plans that has an id in THIS store and republishes storeOffers. A grandfathered product is never offered, and neither is one this store carries no id for, so neither price is asked for.
loadUsage() → Future<void>
Reads the current cycle's usage, pairs the consumer's copy onto it through usageCopy and republishes usage.
noSuchMethod(Invocation invocation) → dynamic
Invoked when a nonexistent method or property is accessed.
inherited
notifyListeners() → void
Call all the registered listeners.
inherited
onClose() → void
Called when the controller is about to be disposed.
inherited
onInit() → void
Called when the controller is first created.
purchaseInStore(MagicStarterProduct product) → Future<bool>
Buys product through the STORE rail with this catalogue's purchaseContext, and answers whether the store reported a completed transaction.
purchaseInStoreAndWait(MagicStarterProduct product) → Future<MagicStarterStorePurchaseOutcome>
Buys product and then waits for the backend to reflect it, which is the whole of what a customer needs from a purchase: a sheet that closed on true is the STORE's word, and the plan they see is the backend's.
refreshUI() → void
Refresh the UI by notifying listeners.
inherited
removeListener(VoidCallback listener) → void
Remove a previously registered closure from the list of closures that are notified when the object changes.
inherited
resetForSession() → Future<void>
Drops every field the six reads populate, publishes the cleared state, then refetches for the identity that is now authenticated.
toString() → String
A string representation of this object.
inherited

Operators

operator ==(Object other) → bool
The equality operator.
inherited