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 | null—Date.now()timestamp at which the current tier was joined or upgraded.nullwhen 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_supporteris 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
paidstatus based onactivatedAt— 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()— setsstatusto'free', stampsactivatedAtwithDate.now().upgradePaid()— setsstatusto'paid', stampsactivatedAtwithDate.now().getStatus()— returns currentSupporterStatus.isSupporter()— returnstruewhen status is not'none'.isPaid()— returnstruewhen status is'paid'.getDailyBonusCredits()— returns500for paid,200for free,0for none.getXpMultiplier()— returns1.20for paid,1.10for free,1.0for 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 callsloadStatus. - 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 ingetDailyBonusCreditsandgetXpMultiplier. - 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:
joinFreeandupgradePaidalways overwrite. There is nodowngradeaction; callers wanting to drop a tier usereset(). - Type
SupporterStatusis exported alongsideSupporterStateanduseSupporterStorefor consumer typing.