diff --git a/lib/comp_16.dart b/lib/comp_16.dart index 1ee5a12..b8b15bf 100644 --- a/lib/comp_16.dart +++ b/lib/comp_16.dart @@ -15,6 +15,7 @@ import 'global_data.dart'; import 'main.dart'; import 'comp_6-7.dart'; import 'comp_1-5.dart'; +import 'screens/paywall_screen.dart'; class Comp16Screen extends StatefulWidget { const Comp16Screen({super.key}); @@ -397,8 +398,28 @@ class _Comp16ScreenState extends State with WidgetsBindingObserver } Future _vaiAScambioDati() async { - await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScambioDatiScreen())); - _verificaStatoPostScambio(); + if (!GlobalData.isPro) { + // Mostra il Paywall se l'utente non ha un abbonamento attivo + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PaywallScreen( + onSuccess: () async { + // Se l'acquisto va a buon fine, naviga subito alla schermata del QR + await Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const ScambioDatiScreen()), + ); + _verificaStatoPostScambio(); + }, + ), + ), + ); + } else { + // Utente PRO, vai direttamente alla schermata di scambio + await Navigator.push(context, MaterialPageRoute(builder: (context) => const ScambioDatiScreen())); + _verificaStatoPostScambio(); + } } void _apriAnteprimaSchermoIntero() { diff --git a/lib/main.dart b/lib/main.dart index eddc64d..1b8ad58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,8 @@ import 'package:flutter_localizations/flutter_localizations.dart'; // --- LIBRERIE META SDK E TRACCIAMENTO APPLE --- import 'package:app_tracking_transparency/app_tracking_transparency.dart'; import 'package:facebook_app_events/facebook_app_events.dart'; +import 'package:cid_app/screens/info_screen.dart'; +import 'package:cid_app/services/subscription_service.dart'; // ⚠️ IMPORTANTE: Assicurati che questo file esista (generato da flutterfire configure) import 'firebase_options.dart'; @@ -36,6 +38,9 @@ void main() async { ); } + // Inizializza gli abbonamenti In-App (RevenueCat) + await SubscriptionService.init(); + _effettuaLoginAnonimo(); runApp(const MyApp()); } diff --git a/lib/screens/paywall_screen.dart b/lib/screens/paywall_screen.dart new file mode 100644 index 0000000..fab6ad5 --- /dev/null +++ b/lib/screens/paywall_screen.dart @@ -0,0 +1,299 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:purchases_flutter/purchases_flutter.dart'; +import 'package:cid_app/services/subscription_service.dart'; + +class PaywallScreen extends StatefulWidget { + final VoidCallback onSuccess; + + const PaywallScreen({super.key, required this.onSuccess}); + + @override + State createState() => _PaywallScreenState(); +} + +class _PaywallScreenState extends State with SingleTickerProviderStateMixin { + Package? _yearlyPackage; + bool _isLoading = true; + bool _isPurchasing = false; + late AnimationController _animationController; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800)); + _fadeAnimation = CurvedAnimation(parent: _animationController, curve: Curves.easeOut); + _fetchOffers(); + _animationController.forward(); + } + + Future _fetchOffers() async { + final offerings = await SubscriptionService.fetchOfferings(); + if (offerings != null && offerings.current != null) { + setState(() { + // Cerchiamo il pacchetto annuale + _yearlyPackage = offerings.current!.annual; + _isLoading = false; + }); + } else { + setState(() { + _isLoading = false; + }); + } + } + + Future _buyPackage() async { + if (_yearlyPackage == null) return; + setState(() { + _isPurchasing = true; + }); + + final success = await SubscriptionService.purchasePackage(_yearlyPackage!); + + if (mounted) { + setState(() { + _isPurchasing = false; + }); + if (success) { + widget.onSuccess(); + Navigator.of(context).pop(); // Chiude il paywall in caso di successo + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Acquisto annullato o non andato a buon fine.')), + ); + } + } + } + + Future _restorePurchases() async { + setState(() { + _isPurchasing = true; + }); + + final success = await SubscriptionService.restorePurchases(); + + if (mounted) { + setState(() { + _isPurchasing = false; + }); + if (success) { + widget.onSuccess(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Acquisti ripristinati con successo! Bentornato.')), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Nessun abbonamento attivo trovato su questo account.')), + ); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, // Sfondo scuro elegante + body: Stack( + children: [ + // Sfondo con gradienti animati (Simulato staticamente qui per performance) + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1E3A8A), Color(0xFF0F172A)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ), + ), + ), + // Decorazione circolare sfocata + Positioned( + top: -100, + right: -50, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.blueAccent.withValues(alpha: 0.3), + ), + ), + ), + Positioned( + bottom: -50, + left: -100, + child: Container( + width: 300, + height: 300, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.purpleAccent.withValues(alpha: 0.2), + ), + ), + ), + // Blur globale + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50), + child: Container(color: Colors.transparent), + ), + ), + + // Contenuto Principale + SafeArea( + child: FadeTransition( + opacity: _fadeAnimation, + child: Column( + children: [ + Align( + alignment: Alignment.topRight, + child: IconButton( + icon: const Icon(Icons.close, color: Colors.white70), + onPressed: () => Navigator.of(context).pop(), + ), + ), + const Spacer(), + const Icon(Icons.qr_code_scanner_rounded, size: 80, color: Colors.white), + const SizedBox(height: 20), + const Text( + 'Sblocca la Sincronizzazione', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 15), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'Unisci i dati dei due veicoli in un istante e genera il CID PDF valido ai fini assicurativi.', + style: TextStyle( + color: Colors.white70, + fontSize: 16, + height: 1.5, + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 40), + + // Card Glassmorphism + Container( + margin: const EdgeInsets.symmetric(horizontal: 30), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.2)), + ), + child: Column( + children: [ + _buildFeatureRow(Icons.check_circle, 'Scansione QR Code istantanea'), + const SizedBox(height: 12), + _buildFeatureRow(Icons.check_circle, 'Generazione PDF Ufficiale'), + const SizedBox(height: 12), + _buildFeatureRow(Icons.check_circle, 'Nessuna Pubblicità'), + ], + ), + ), + const SizedBox(height: 40), + + // Bottone Acquisto + if (_isLoading) + const CircularProgressIndicator(color: Colors.white) + else if (_yearlyPackage != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30), + child: ElevatedButton( + onPressed: _isPurchasing ? null : _buyPackage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 10, + shadowColor: Colors.blueAccent.withValues(alpha: 0.5), + minimumSize: const Size(double.infinity, 55), + ), + child: _isPurchasing + ? const SizedBox( + height: 20, width: 20, + child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2), + ) + : Text( + 'Passa a PRO - \${_yearlyPackage!.storeProduct.priceString} / anno', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + ) + else + const Text('Pacchetti non disponibili. Riprova più tardi.', style: TextStyle(color: Colors.redAccent)), + + const SizedBox(height: 20), + + // Tasto Ripristina + TextButton( + onPressed: _isPurchasing ? null : _restorePurchases, + child: const Text( + 'Hai già un abbonamento? Ripristina acquisti', + style: TextStyle(color: Colors.white70, decoration: TextDecoration.underline), + ), + ), + const Spacer(), + + // Footer Legale (Obbligatorio per Apple) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { /* TODO Apri Terms */ }, + child: const Text('Terms of Use', style: TextStyle(color: Colors.white54, fontSize: 12)), + ), + const Text('|', style: TextStyle(color: Colors.white54, fontSize: 12)), + TextButton( + onPressed: () { /* TODO Apri Privacy */ }, + child: const Text('Privacy Policy', style: TextStyle(color: Colors.white54, fontSize: 12)), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildFeatureRow(IconData icon, String text) { + return Row( + children: [ + Icon(icon, color: Colors.greenAccent, size: 24), + const SizedBox(width: 15), + Expanded( + child: Text( + text, + style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w500), + ), + ), + ], + ); + } +} diff --git a/lib/services/subscription_service.dart b/lib/services/subscription_service.dart index 3070ab0..eaf4c30 100644 --- a/lib/services/subscription_service.dart +++ b/lib/services/subscription_service.dart @@ -4,9 +4,8 @@ import 'package:purchases_flutter/purchases_flutter.dart'; import 'package:cid_app/global_data.dart'; class SubscriptionService { - // TODO: Inserisci qui le tue API Key prese dalla dashboard di RevenueCat - static const _appleApiKey = 'appl_YOUR_APPLE_API_KEY_HERE'; - static const _googleApiKey = 'goog_YOUR_GOOGLE_API_KEY_HERE'; + // Chiave unificata di test fornita da RevenueCat + static const _apiKey = 'test_xlLcZsCHGnotDSfUoDBmDCrjfaZ'; static const entitlementID = 'pro'; // Il nome dell'entitlement su RevenueCat static Future init() async { @@ -15,9 +14,9 @@ class SubscriptionService { PurchasesConfiguration? configuration; if (Platform.isAndroid) { - configuration = PurchasesConfiguration(_googleApiKey); + configuration = PurchasesConfiguration(_apiKey); } else if (Platform.isIOS) { - configuration = PurchasesConfiguration(_appleApiKey); + configuration = PurchasesConfiguration(_apiKey); } if (configuration != null) { @@ -50,6 +49,16 @@ class SubscriptionService { // TODO: Notifica l'interfaccia utente se necessario (es. tramite un Provider o ValueNotifier) } + static Future fetchOfferings() async { + try { + final offerings = await Purchases.getOfferings(); + return offerings; + } on PlatformException catch (e) { + print('Errore fetch offerte: \${e.message}'); + return null; + } + } + static Future purchasePackage(Package package) async { try { final customerInfo = await Purchases.purchasePackage(package); diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index d942a0c..a9b4b39 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -1,5 +1,6 @@ # Change Log +- **2026-04-24**: Implementazione In-App Purchases (RevenueCat) con aggiunta del plugin `purchases_flutter`. Creato `SubscriptionService`, aggiunto campo `isPro` in `GlobalData`, e costruito il Paywall Custom (Glassmorphism) per bloccare la funzionalità "Scambio Dati" agli utenti non paganti. - **2026-04-24**: Sostituiti tutti i metodi deprecati `.withOpacity(...)` con `.withValues(alpha: ...)` in 12 file dell'app, rimuovendo 36 avvisi dal compilatore Dart. - **2026-04-24**: Rimossa la cartella deprecata `lib/temp/` per pulire l'analizzatore Dart da oltre 350 falsi errori. Aggiunto controllo `if (!mounted) return;` in `lib/test_scraping.dart` per prevenire crash asincroni. - **2026-04-24**: Creazione iniziale della Memory Bank. diff --git a/memory-bank/current-state.md b/memory-bank/current-state.md index acad346..02f64cb 100644 --- a/memory-bank/current-state.md +++ b/memory-bank/current-state.md @@ -8,6 +8,7 @@ L'app "CAI Facile" è in uno stato avanzato di sviluppo. Lo scambio dati remoto - Scambio Dati P2P sicuro tramite Firebase (Host/Guest via QR). - Generazione PDF e condivisione. - Parsing dati protetto (`fixCircostanze`). +- **Monetizzazione**: Sistema di abbonamenti (In-App Purchases) attivo tramite RevenueCat con Paywall integrato sul flusso di Scambio Dati. ## Problemi Aperti / TODO - **Debito Tecnico**: Sono presenti variabili non formattate in `lowerCamelCase` all'interno di `lib/global_data.dart` (es. `Cod_fiscale_cond_B`). diff --git a/memory-bank/decisions.md b/memory-bank/decisions.md index feb63ad..93a4d8a 100644 --- a/memory-bank/decisions.md +++ b/memory-bank/decisions.md @@ -13,3 +13,7 @@ ## Sincronizzazione Dati Condivisi - Il "Cassetto 1" (Grafico Incidente con auto e strade) viene gestito **esclusivamente dal Conducente A** e sincronizzato in sovrascrittura su B per evitare conflitti logici. - Le Firme ("Cassetto 2") restano indipendenti (A non sovrascrive B). + +## Monetizzazione & Paywall (RevenueCat) +- **Decisione**: L'app utilizza un modello "Hard Paywall parziale". Gli utenti possono compilare il modulo offline gratuitamente, ma la **funzionalità core di Scambio Dati (QR Code e sincronizzazione Firebase)** è bloccata dietro un abbonamento annuale. +- **Implementazione**: Utilizzo di RevenueCat (`purchases_flutter`) per delegare la validazione delle ricevute lato server. Interfaccia Paywall custom (`PaywallScreen`) piuttosto che il pacchetto UI di RevenueCat per mantenere coerenza stilistica e aumentare le conversioni con copy persuasivo ("Sblocca la sincronizzazione").