purchaseInStoreAndWait method

Future<MagicStarterStorePurchaseOutcome> purchaseInStoreAndWait(
  1. MagicStarterProduct product
)

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.

  1. The entitlement is snapshotted BEFORE the sheet opens: afterwards the answer may already have moved.
  2. A dismissed sheet ends it. A thrown BillingException propagates exactly as purchaseInStore throws it, and a PENDING one still polls in the background, so the gate reopens the moment the backend reflects the purchase rather than only when the window runs out.
  3. When the rail says the change applies at renewal (StoreBillingService.lastChangeTiming), the entitlement is NOT expected to move and polling would only run out the clock. The wait flag is released and the screen reports the pre-sheet period end. The rail's answer is the only one asked: it knows what the store will do, and a guess from the catalogue's tier order told a customer their plan changed later when the store changed it now.
  4. Anything else, an unknown timing included, polls on _pollBackoff (60 s in all) until the snapshot differs.

Implementation

Future<MagicStarterStorePurchaseOutcome> purchaseInStoreAndWait(
  MagicStarterProduct product,
) async {
  // 1. What the backend said BEFORE the sheet. A purchase already waiting is
  //    refused by [purchaseInStore] with the same pending code a store
  //    reports, and that refusal must not start a second poll.
  final MagicStarterEntitlementSnapshot before = _entitlementSnapshot;
  final bool alreadyWaiting = _awaitingProductKey != null;

  // 2. The sheet.
  final bool bought;
  try {
    bought = await purchaseInStore(product);
  } catch (error) {
    if (!alreadyWaiting && _isPending(error)) {
      unawaited(_awaitEntitlementChange(before));
    }
    rethrow;
  }
  if (!bought) return MagicStarterStorePurchaseOutcome.dismissed;

  // 3. Nothing to poll for.
  if (storeRail?.lastChangeTiming == StoreChangeTiming.atRenewal) {
    _clearAwaiting();

    return MagicStarterStorePurchaseOutcome.deferred;
  }

  // 4. Poll.
  return _awaitEntitlementChange(before);
}