Premium in VOZI is currently a demo feature managed by the adult from the parent dashboard. There is no payment processing, no App Store subscription, and no external billing provider. The adult can activate or deactivate Premium from the parent dashboard at any time. When activated, all nine phonemes and their associated rewards become accessible to every child profile on the device.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AlonsoSam/vozi-android/llms.txt
Use this file to discover all available pages before exploring further.
Premium is a demonstration of the paywall architecture, not a live commercial feature. No real payment is collected. The activation toggle exists to let parents and evaluators preview the full app experience.
Free vs Premium Phonemes
Only the R phoneme is available without Premium. All eight remaining phonemes are gated behind Premium status.| Phoneme | Free? | Notes |
|---|---|---|
| R | ✅ Always free | First station on the learning path |
| RR | 🔒 Premium | |
| S | 🔒 Premium | |
| L | 🔒 Premium | |
| TR | 🔒 Premium | |
| PR | 🔒 Premium | |
| PL | 🔒 Premium | |
| BR | 🔒 Premium | |
| BL | 🔒 Premium |
canAccess(phoneme) is the single gating function. It returns true for R unconditionally and for all others only when isPremiumEnabled is true. Developer mode bypasses this check entirely — see Developer Mode below.
PremiumStore
PremiumStore is a ChangeNotifier that holds the Premium state and orchestrates both local persistence and optional Supabase sync. It is constructed in _VoziAppState and exposed to the entire widget tree via PremiumScope.
| Member | Type | Description |
|---|---|---|
isPremiumEnabled | bool | Whether Premium is currently active |
isLoaded | bool | true once the persisted value has been read from shared_preferences |
source | PremiumSource | Where the current state originated |
freePhonemeCode | static const String | 'R' — the one always-free phoneme |
load() | Future<void> | Reads local value, starts observing auth changes |
activateDemo() | Future<PremiumWriteOutcome> | Enables Premium |
deactivateDemo() | Future<PremiumWriteOutcome> | Disables Premium |
refreshFromAccount() | Future<void> | Reads remote premium row and reconciles with local state |
canAccess(Phoneme) | bool | Gate check combining Premium status and free phoneme rule |
isFreePhoneme(Phoneme) | bool | true only for R |
vozi_premium_enabled_v1 in shared_preferences. The _v1 suffix is reserved for future format migrations.
PremiumWriteOutcome Enum
Every call toactivateDemo() or deactivateDemo() returns a PremiumWriteOutcome so the UI can show an appropriate confirmation message.
| Value | Meaning |
|---|---|
localOnly | Saved to shared_preferences only. No adult session is active. |
syncedToAccount | Saved locally and successfully upserted to the Supabase premium table. |
accountFailed | Saved locally, but the Supabase write failed (network error, permission issue, etc.). The local value is the fallback and Premium functions normally. |
_isPremiumEnabled is updated immediately (optimistic local update) and notifyListeners() is called before the Supabase round-trip completes. The UI never waits for the network to reflect the change.
Premium Sync with Supabase
When the adult signs in,PremiumStore observes the Supabase auth state stream via _authSub. On a new or restored session, refreshFromAccount() is called automatically:
Reconcile
If a row exists, the remote value overwrites the local value and is persisted to
shared_preferences. If no row exists (first login ever), the current local value is upserted to Supabase — preserving any demo activation the adult set before signing in._source reverts to PremiumSource.localDemo and the last cached value remains in place. The child’s experience is uninterrupted.
activateDemo() and deactivateDemo(), the flow is always: update local state → notify UI → write to shared_preferences → attempt Supabase upsert → return outcome. The UI update happens before the network call.
Child Home Path: Premium-Locked Stations
InChildHomeScreen, the learning path is rendered as a series of stations. Each station corresponds to one phoneme and can be in one of several states. Premium-locked stations show a gold crown icon and the label “Premium”.
The locked state is driven by the _Stop.premiumLocked value in the home screen’s internal model. When the child taps a locked station:
- A message is shown explaining that Premium is needed
- The app navigates to
AppRouter.premium(/premium) where the adult can activate the demo
PremiumScope.of(context).canAccess(phoneme) call is the single decision point that determines whether a station renders as accessible, locked, or completed.
Developer Mode (DeveloperStore)
Developer mode is a separate toggle from Premium. It is intended for demos and QA — when enabled, all nine phoneme stations appear fully accessible regardless of Premium status or actual child progress.| ✅ Does | ❌ Does not |
|---|---|
| Makes all phoneme stations appear accessible | Modify completedPhonemes or practicedPhonemes |
| Shows all reward characters as unlocked | Award or remove points |
| Persists across app restarts | Change the isPremiumEnabled value in PremiumStore |
| Reverts instantly on toggle-off | Write anything to Supabase |
DeveloperScope.of(context).isEnabled value is checked alongside PremiumScope.of(context).canAccess(phoneme) in screens that render station accessibility. When developer mode is on, the Premium check is bypassed entirely.
PremiumScope
PremiumScope follows the same InheritedNotifier pattern as ProfileScope and DeveloperScope:
PremiumScope uses InheritedNotifier, any widget that calls PremiumScope.of(context) will rebuild automatically when PremiumStore calls notifyListeners(). This means that activating Premium from the parent dashboard causes ChildHomeScreen — which is already mounted under the Navigator — to rebuild immediately and unlock all stations without any manual refresh.