import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; class PurchaseResult { final bool success; final String? purchaseToken; final String? error; const PurchaseResult({this.success = false, this.purchaseToken, this.error}); factory PurchaseResult.ok(String token) => PurchaseResult(success: true, purchaseToken: token); factory PurchaseResult.failed([String? error]) => PurchaseResult(success: false, error: error); } abstract class BillingService { static const proProductId = 'casha_pro_lifetime'; Future purchasePro(); Future restorePurchases(); Future queryPastPurchase(); Future completePurchase(String purchaseToken); Stream> get purchaseStream; Future dispose(); } class PlayBillingService implements BillingService { final InAppPurchase _inAppPurchase = InAppPurchase.instance; late final StreamSubscription> _sub; final _controller = StreamController>.broadcast(); PlayBillingService() { _sub = _inAppPurchase.purchaseStream.listen((purchases) { _controller.add(purchases); }); } @override Stream> get purchaseStream => _controller.stream; @override Future purchasePro() async { final available = await _inAppPurchase.isAvailable(); if (!available) { return PurchaseResult.failed('Billing not available'); } final response = await _inAppPurchase.queryProductDetails( {BillingService.proProductId}, ); if (response.productDetails.isEmpty) { return PurchaseResult.failed('Product not found'); } final product = response.productDetails.first; final purchaseParam = PurchaseParam(productDetails: product); final started = await _inAppPurchase.buyNonConsumable( purchaseParam: purchaseParam, ); if (!started) { return PurchaseResult.failed('Could not start purchase'); } final completer = Completer(); late StreamSubscription sub; sub = purchaseStream.timeout( const Duration(seconds: 60), onTimeout: (sink) { if (!completer.isCompleted) { completer.complete(PurchaseResult.failed('Purchase timed out')); } sub.cancel(); }, ).listen((purchases) { for (final p in purchases) { if (p.productID == BillingService.proProductId && p.status == PurchaseStatus.purchased) { if (!completer.isCompleted) { completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); } sub.cancel(); return; } if (p.status == PurchaseStatus.error) { if (!completer.isCompleted) { completer.complete(PurchaseResult.failed(p.error?.message)); } sub.cancel(); return; } } }); return completer.future; } @override Future restorePurchases() async { final available = await _inAppPurchase.isAvailable(); if (!available) { return PurchaseResult.failed('Billing not available'); } await _inAppPurchase.restorePurchases(); final completer = Completer(); late StreamSubscription sub; sub = purchaseStream.timeout( const Duration(seconds: 15), onTimeout: (sink) { if (!completer.isCompleted) { completer.complete(PurchaseResult.failed('Restore timed out')); } sub.cancel(); }, ).listen((purchases) { for (final p in purchases) { if (p.productID == BillingService.proProductId && (p.status == PurchaseStatus.restored || p.status == PurchaseStatus.purchased)) { if (!completer.isCompleted) { completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); } sub.cancel(); return; } } }); return completer.future; } @override Future queryPastPurchase() async { final available = await _inAppPurchase.isAvailable(); if (!available) { return const PurchaseResult(); } final response = await _inAppPurchase.queryProductDetails( {BillingService.proProductId}, ); if (response.productDetails.isEmpty) { return const PurchaseResult(); } final completer = Completer(); late StreamSubscription sub; sub = purchaseStream.timeout( const Duration(seconds: 10), onTimeout: (sink) { if (!completer.isCompleted) { completer.complete(const PurchaseResult()); } sub.cancel(); }, ).listen((purchases) { for (final p in purchases) { if (p.productID == BillingService.proProductId && (p.status == PurchaseStatus.restored || p.status == PurchaseStatus.purchased)) { if (!completer.isCompleted) { completer.complete(PurchaseResult.ok(p.verificationData.serverVerificationData)); } sub.cancel(); return; } } }); await _inAppPurchase.restorePurchases(); return completer.future; } @override Future completePurchase(String purchaseToken) async { if (kDebugMode) { print('completePurchase: $purchaseToken'); } } @override Future dispose() async { await _sub.cancel(); await _controller.close(); } }