PURPOSE

Tracks Supporter Club membership status for the local player. Provides perk values (daily bonus credits, XP multiplier) derived from the current tier. Three tiers: none, free, paid. The paid tier is conceptually a $2.99/month subscription; at launch it grants gems for free with no real IAP hooked up.

OWNS

  • status: SupporterStatus — current tier, one of 'none' | 'free' | 'paid'. Defaults to 'none'.
  • activatedAt: number | nullDate.now() timestamp at which the current tier was joined or upgraded. null when status is 'none'.

READS FROM

  • Date.now() for timestamping tier activation.
  • Own state via get() for derived perk values and status predicates.

PUSHES TO

  • Nothing directly. Pure Zustand state holder. Persistence to Supabase player_supporter is performed by external sync code (not in this file).

DOES NOT

  • Does not call Supabase, network, or storage APIs.
  • Does not process payments, IAP receipts, or subscription validation.
  • Does not expire paid status based on activatedAt — there is no time-based downgrade logic here.
  • Does not award the daily bonus or apply the XP multiplier; callers must read the perk values and apply them.
  • Does not gate any feature on its own — consumers check isSupporter() / isPaid() themselves.

Signals

Zustand store; no event emitters. Consumers subscribe via useSupporterStore hook or call useSupporterStore.getState() for imperative reads. State changes propagate through Zustand’s subscription model.

Entry points

  • useSupporterStore — Zustand hook exported for React components and imperative access.
  • joinFree() — sets status to 'free', stamps activatedAt with Date.now().
  • upgradePaid() — sets status to 'paid', stamps activatedAt with Date.now().
  • getStatus() — returns current SupporterStatus.
  • isSupporter() — returns true when status is not 'none'.
  • isPaid() — returns true when status is 'paid'.
  • getDailyBonusCredits() — returns 500 for paid, 200 for free, 0 for none.
  • getXpMultiplier() — returns 1.20 for paid, 1.10 for free, 1.0 for none.
  • loadStatus(status, activatedAt) — restores persisted state (used on app load after fetching from Supabase).
  • reset() — wipes back to 'none' / null.

Pattern notes

  • Single flat Zustand store created with create<SupporterState>(). No slices, no middleware, no persist plugin — persistence is delegated to external sync code that calls loadStatus.
  • Perk values are hardcoded in the getter switch statements rather than driven from a data table. Numeric constants (500, 200, 1.20, 1.10) live inline in getDailyBonusCredits and getXpMultiplier.
  • Activation timestamps are recorded but never read inside this file — no expiry, refund window, or anniversary logic is implemented here.
  • Tier transitions are one-way in the API surface: joinFree and upgradePaid always overwrite. There is no downgrade action; callers wanting to drop a tier use reset().
  • Type SupporterStatus is exported alongside SupporterState and useSupporterStore for consumer typing.