Quando usarlo
Una funzione fa parte di Premium, per esempio una cronologia illimitata o un
export. I passi usano come esempio un widget PremiumGate e, per i dati sul
server, una tabella premium_notes.
Regole da rispettare
- Sul dispositivo l'entitlement arriva solo da
subscriptionStatusProvider, che legge RevenueCat. Non sbloccare mai da un flag locale, da un booleano in cache o da un valore di remote config. - Un errore o un caricamento non sono Premium: la funzione resta bloccata.
- L'app non è un confine di sicurezza. Se la funzione legge o scrive dati in
Supabase, la RLS controlla
subscription_entitlements, che scrive solo la Edge Functionrevenuecat-webhookcon il service role. - Il ripristino resta raggiungibile: gli utenti gratuiti arrivano al paywall, che offre sempre "Ripristina acquisti".
Passi
Crea
lib/features/subscription/presentation/premium_gate.dart, unConsumerWidgetche mostra il figlio solo con un entitlement attivo:return switch (ref.watch(subscriptionStatusProvider)) { AsyncData(value: SubscriptionStatus(isPremium: true)) => child, AsyncLoading() => Center( child: CircularProgressIndicator(semanticsLabel: l10n.loading), ), // Errore o nessun entitlement: la funzione resta bloccata. _ when premiumEnabled => FilledButton( onPressed: () => context.pushNamed(AppRoute.premium.name), child: Text(l10n.premium), ), _ => const SizedBox.shrink(), };premiumEnabledèref.watch(remoteFlagProvider(RemoteFlag.premiumEnabled)): quando il flag è spento, nascondi l'accesso al paywall come faHomePage; la funzione resta bloccata. I nuovi testi vanno nei tre file.arb.Avvolgi la funzione con
PremiumGate. Per i messaggi su prova gratuita, periodo di grazia o scadenza usasubscriptionStage()dilib/features/subscription/domain/subscription_lifecycle.dart; non sblocca nulla oltre aisPremium.Se la funzione gira solo sul dispositivo, fermati qui e passa al passo 6.
Per i dati sul server aggiungi una migration in
supabase/migrations/(append-only). Ogni policy controlla sia il proprietario sia l'entitlement:create policy "premium_notes_select_own" on public.premium_notes for select to authenticated using ( (select auth.uid()) = owner_id and exists ( select 1 from public.subscription_entitlements e where e.user_id = (select auth.uid()) and e.state in ('trial', 'active', 'grace') and (e.expires_at is null or e.expires_at > now()) ) );Ripeti la condizione nelle policy
insert,updateedeleteche servono alla funzione (anche inwith check). Gli utenti possono leggere la propria riga di entitlement, quindi non serve una funzionesecurity definer.Configura la Edge Function
revenuecat-webhookcome descritto indocs/subscription-backend.md: senza di essasubscription_entitlementsresta vuota e il server nega l'accesso a tutti. I webhook arrivano dopo l'acquisto (le disdette anche ore dopo), quindi subito dopo l'acquisto l'app può mostrare Premium mentre il server non restituisce ancora righe: mostra un nuovo tentativo invece di un errore.Testa senza rete. Sostituisci
subscriptionStatusProviderconStream.value(...)e verifica che con Premium la funzione compaia, mentre con stato gratuito, errore o caricamento no. Per passare dal paywall riusa il repository finto ditest/subscription_test.dart. Per i dati sul server aggiungi un test pgTAP insupabase/tests/database/: inserisci l'entitlement come proprietario del test, poi comeauthenticatedverifica che un utente gratuito non ottenga righe e uno Premium sì.Elenca in
PRODUCT.mdcosa include Premium.
Completato quando
- i comandi di verifica superano;
- la funzione è bloccata quando lo stato è gratuito, in errore o in caricamento;
- gli eventuali dati sul server sono protetti dalla RLS sull'entitlement, con un test pgTAP;
PRODUCT.mdelenca la funzione Premium.
