Tutti i playbookConsumer Subscription App

Riservare una funzione a Premium

Sbloccare una funzione solo con un entitlement RevenueCat attivo, portare gli utenti gratuiti al paywall e applicare la stessa regola in Supabase quando la funzione ha dati sul server.

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 Function revenuecat-webhook con il service role.
  • Il ripristino resta raggiungibile: gli utenti gratuiti arrivano al paywall, che offre sempre "Ripristina acquisti".

Passi

  1. Crea lib/features/subscription/presentation/premium_gate.dart, un ConsumerWidget che 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 fa HomePage; la funzione resta bloccata. I nuovi testi vanno nei tre file .arb.

  2. Avvolgi la funzione con PremiumGate. Per i messaggi su prova gratuita, periodo di grazia o scadenza usa subscriptionStage() di lib/features/subscription/domain/subscription_lifecycle.dart; non sblocca nulla oltre a isPremium.

  3. Se la funzione gira solo sul dispositivo, fermati qui e passa al passo 6.

  4. 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, update e delete che servono alla funzione (anche in with check). Gli utenti possono leggere la propria riga di entitlement, quindi non serve una funzione security definer.

  5. Configura la Edge Function revenuecat-webhook come descritto in docs/subscription-backend.md: senza di essa subscription_entitlements resta 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.

  6. Testa senza rete. Sostituisci subscriptionStatusProvider con Stream.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 di test/subscription_test.dart. Per i dati sul server aggiungi un test pgTAP in supabase/tests/database/: inserisci l'entitlement come proprietario del test, poi come authenticated verifica che un utente gratuito non ottenga righe e uno Premium sì.

  7. Elenca in PRODUCT.md cosa 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.md elenca la funzione Premium.