116 lines
3.3 KiB
Dart
116 lines
3.3 KiB
Dart
import 'dart:async';
|
|
import 'package:crypto/crypto.dart';
|
|
import 'billing_models.dart';
|
|
import 'billing_service.dart';
|
|
import 'product_catalog.dart';
|
|
|
|
/// Test/Mock implementation of [BillingService] for automated test suites.
|
|
/// Never selected in release execution paths.
|
|
class FakeBillingService implements BillingService {
|
|
final StreamController<List<StorePurchase>> _purchaseStreamController =
|
|
StreamController<List<StorePurchase>>.broadcast();
|
|
|
|
final Set<String> _completedPurchaseTokens = <String>{};
|
|
bool available = true;
|
|
bool initializeSuccess = true;
|
|
List<StoreProduct> mockProducts = [
|
|
const StoreProduct(
|
|
id: ProductCatalog.proMonthly,
|
|
title: 'MotorSignal Pro Monthly',
|
|
description: 'Monthly unlimited access to all Pro features',
|
|
price: '\$4.99',
|
|
rawPrice: 4.99,
|
|
currencyCode: 'USD',
|
|
currencySymbol: '\$',
|
|
isSubscription: true,
|
|
),
|
|
const StoreProduct(
|
|
id: ProductCatalog.proLifetime,
|
|
title: 'MotorSignal Pro Lifetime',
|
|
description: 'Permanent unlimited access to all Pro features',
|
|
price: '\$49.99',
|
|
rawPrice: 49.99,
|
|
currencyCode: 'USD',
|
|
currencySymbol: '\$',
|
|
isSubscription: false,
|
|
),
|
|
];
|
|
|
|
int launchCount = 0;
|
|
int restoreCount = 0;
|
|
String? lastPurchasedProductId;
|
|
|
|
@override
|
|
Stream<List<StorePurchase>> get purchaseStream =>
|
|
_purchaseStreamController.stream;
|
|
|
|
@override
|
|
Future<bool> initialize() async => initializeSuccess && available;
|
|
|
|
@override
|
|
Future<bool> isAvailable() async => available;
|
|
|
|
@override
|
|
Future<List<StoreProduct>> queryProducts() async {
|
|
if (!available) return [];
|
|
return mockProducts;
|
|
}
|
|
|
|
@override
|
|
Future<bool> launchPurchaseFlow(String productId) async {
|
|
launchCount++;
|
|
lastPurchasedProductId = productId;
|
|
if (!available || !ProductCatalog.isValidProduct(productId)) return false;
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Future<void> restorePurchases() async {
|
|
restoreCount++;
|
|
}
|
|
|
|
@override
|
|
Future<void> completePurchase(StorePurchase purchase) async {
|
|
final token = purchase.purchaseToken ?? purchase.purchaseId;
|
|
_completedPurchaseTokens.add(token);
|
|
}
|
|
|
|
bool isPurchaseCompleted(String tokenOrId) =>
|
|
_completedPurchaseTokens.contains(tokenOrId);
|
|
|
|
/// Helper for tests to simulate incoming purchase events from the store.
|
|
void emitPurchases(List<StorePurchase> purchases) {
|
|
if (!_purchaseStreamController.isClosed) {
|
|
_purchaseStreamController.add(purchases);
|
|
}
|
|
}
|
|
|
|
/// Helper to create a fake StorePurchase for tests.
|
|
StorePurchase createFakePurchase({
|
|
required String productId,
|
|
required StorePurchaseStatus status,
|
|
String? purchaseId,
|
|
String? token,
|
|
bool pendingComplete = true,
|
|
}) {
|
|
final pToken =
|
|
token ?? 'fake_token_${DateTime.now().millisecondsSinceEpoch}';
|
|
return StorePurchase(
|
|
purchaseId:
|
|
purchaseId ??
|
|
'fake_purchase_${DateTime.now().millisecondsSinceEpoch}',
|
|
productId: productId,
|
|
status: status,
|
|
transactionDate: DateTime.now().toIso8601String(),
|
|
purchaseToken: pToken,
|
|
purchaseTokenHash: sha256.convert(pToken.codeUnits).toString(),
|
|
pendingCompletePurchase: pendingComplete,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_purchaseStreamController.close();
|
|
}
|
|
}
|