loadStoreFundedTeam method

Future<void> loadStoreFundedTeam()

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.

Asked only on a build with a store rail, because it exists to gate a store purchase and a web build has none to gate: a screen with no store affordance would be spending a request on an answer it cannot use. Skipped entirely when no reader is registered, which storeCheckRegistered reports separately so the gate can refuse rather than guess.

Deliberate degradation on failure: storeFundedTeam keeps whatever it held, which for a first read means null and therefore permissive. The producer's TRANSFER handling is what keeps the entitlement itself honest either way, so this check exists to stop a customer being surprised, not to stop the data being wrong.

The degradation stays deliberate but it does NOT stay silent. This read is the only thing standing between a store purchase and transferring another team's subscription away, it fails permissive, and the purchase path asks it again at the moment money moves. Swallowing it silently let a real 500 on this endpoint pass the transfer through with nothing in any log to explain it afterwards.

It only ever SETS a name and never clears one, which is a consequence rather than an oversight: it runs at mount and again before a purchase, and the second call is only reachable while the answer was already null (a named refusal renders no CTA to tap), so a name-to-null transition has no trigger here. A future caller that could produce one has to publish the empty answer too.

Implementation

Future<void> loadStoreFundedTeam() async {
  final int session = _sessionGeneration;
  final int readToken = _latestRead.begin(_storeFundedTeamKey);
  final MagicStarterStoreFundedTeamReader? reader = storeFundedTeamReader;
  if (reader == null) return;

  try {
    // Inside the try, because resolving a rail is itself a call that can
    // fail: `PaymentsManager._optional` returns null for an unfilled role but
    // THROWS a BillingException when the role holds something that cannot
    // serve the contract, which is what a consumer's mistyped `extend` call
    // produces. Reading it above the try would let that escape `load()`,
    // where `onInit` fires it unawaited and nothing is left to catch it.
    if (storeRail == null) return;

    final String? name = await reader();
    if (session != _sessionGeneration) return;
    if (!_latestRead.isCurrent(readToken, _storeFundedTeamKey)) return;
    if (name == null || name.isEmpty) return;

    _storeFundedTeam = name;
    refreshUI();
  } catch (error) {
    if (session != _sessionGeneration) return;
    if (!_latestRead.isCurrent(readToken, _storeFundedTeamKey)) return;
    _reportDegradation('storeFundedTeamReader', error);
  }
}