Matthias Nott
2026-06-11 39392618ecfeb89576387b0229fd1ebee6229946
fix(iap): align PurchaseService with the live store products

The paywall scaffolding (PaywallBanner, locked sessions in the drawer, settings
upgrade button) was already in place but pointed at the wrong product id and
had a hard 'Pro=true' override that masked the real flow.

- kFullAccessProductId is now com.tekmidian.pailot.pro to match the IAPs
created today in App Store Connect (Apple ID 6779190925) and Google Play
Console (one-time product, Active).
- PurchaseService.initialize() no longer forces _isPro = true: it hydrates
from the SharedPreferences cache, and only grants Pro automatically in
kDebugMode when the platform store is unavailable (so sideloaded dev
builds still work). Release builds without a store fall back to the
cached value rather than silently unlocking everything.
- isProProvider's default is now false; main.dart still seeds it from
PurchaseService.instance.isPro on boot and keeps it synced via a listener.
- kFreeTierMessageTtl bumped 15m → 30m to match the stated free-tier window
(2 sessions / 30 minutes).
2 files modified
changed files
lib/providers/providers.dart patch | view | blame | history
lib/services/purchase_service.dart patch | view | blame | history
lib/providers/providers.dart
....@@ -191,7 +191,10 @@
191191
192192 // --- Pro / Purchase Status ---
193193
194
-/// Whether the user has purchased PAILot Pro (full access).
195
-/// Defaults to true — PurchaseService sets to false after StoreKit verification
196
-/// confirms no purchase. This way dev/sideloaded builds work without IAP.
197
-final isProProvider = StateProvider<bool>((ref) => true);
194
+/// Pro / unlocked status. Real value is seeded in main.dart from
195
+/// PurchaseService.instance.isPro (SharedPrefs cache + StoreKit / Play Billing
196
+/// verification), and kept in sync via a listener. The `false` default is just
197
+/// a safe fallback for tests or any path that bypasses main(); dev/sideload
198
+/// behaviour is handled in PurchaseService.initialize() (debug builds without
199
+/// an available store get Pro granted automatically).
200
+final isProProvider = StateProvider<bool>((ref) => false);
lib/services/purchase_service.dart
....@@ -5,13 +5,15 @@
55 import 'package:shared_preferences/shared_preferences.dart';
66
77 /// Product ID for the one-time full-access purchase.
8
-const String kFullAccessProductId = 'com.tekmidian.pailot.fullaccess';
8
+/// Matches the IAPs registered in App Store Connect (Apple ID 6779190925)
9
+/// and Google Play Console (one-time product `com.tekmidian.pailot.pro`).
10
+const String kFullAccessProductId = 'com.tekmidian.pailot.pro';
911
1012 /// Maximum sessions allowed on the free tier.
1113 const int kFreeTierMaxSessions = 2;
1214
13
-/// Maximum message age for free-tier users (15 minutes).
14
-const Duration kFreeTierMessageTtl = Duration(minutes: 15);
15
+/// Maximum message age for free-tier users.
16
+const Duration kFreeTierMessageTtl = Duration(minutes: 30);
1517
1618 /// Shared preference key for caching purchase status locally.
1719 const String _kProCacheKey = 'pailot_is_pro';
....@@ -51,20 +53,27 @@
5153
5254 /// Initialize the service. Call once at app startup.
5355 Future<void> initialize() async {
54
- // Restore cached value immediately so UI doesn't flicker.
55
- // Default to true for dev/sideloaded builds (no StoreKit configured).
56
+ // Hydrate from local cache first so UI doesn't flash "Free" before the
57
+ // store reply lands. The cache is authoritative for offline / cold-start
58
+ // rendering; restorePurchases() reconciles it with the platform stores.
5659 final prefs = await SharedPreferences.getInstance();
57
- // Force pro for now — clear any stale false value from earlier testing
58
- _isPro = true;
59
- await prefs.setBool(_kProCacheKey, true);
60
+ _isPro = prefs.getBool(_kProCacheKey) ?? false;
6061 notifyListeners();
6162
62
- // Check if IAP is available — may not be on dev/sideloaded builds
63
+ // If the platform IAP layer isn't available (sideloaded dev build, no
64
+ // network, simulator without StoreKit config), keep the cached value as-is
65
+ // and skip the store dance. In debug, default to Pro so dev testing isn't
66
+ // blocked by the paywall — but never override in release builds.
6367 final available = await InAppPurchase.instance.isAvailable();
6468 if (!available) {
65
- debugPrint('[IAP] StoreKit not available — assuming pro (dev build)');
66
- _isPro = true;
67
- notifyListeners();
69
+ if (kDebugMode && !_isPro) {
70
+ debugPrint('[IAP] Store unavailable in debug build — granting Pro for dev testing');
71
+ _isPro = true;
72
+ await prefs.setBool(_kProCacheKey, true);
73
+ notifyListeners();
74
+ } else {
75
+ debugPrint('[IAP] Store unavailable; using cached isPro=$_isPro');
76
+ }
6877 return;
6978 }
7079