This is kind of over my head, I am successfully created the serverside validation for the subscription but I don’t know where to put it ( From logic perspective ) on my Flutter app.
Now in Flutter, I have to listen to the purchase stream to get purchase details ( token and ..etc ), and here’s the weird part, one below function I get responses under PurchaseStatus.purchased
and PurchaseStatus.restored
when the purchase has been canceled and expired at the same time.
So the _handleSuccessfullySubscribed();
get’s run!
I have another function for server-side validation but don’t know where to place it in my code without doing an infinite loop here.
void _initiated() async {
await _subscription?.cancel();
_subscription = _inAppPurchase.purchaseStream.listen((purchases) async {
for (final purchase in purchases) {
switch (purchase.status) {
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
// handle if purchased here
_handleSuccessfullySubscribed();
break;
case PurchaseStatus.pending:
// handle if pending here
log('Purchase is pending');
break;
case PurchaseStatus.canceled:
// handle if canceled here
log('Purchase is canceled');
break;
case PurchaseStatus.error:
// handle if error here
log('Purchase has an error');
break;
default:
}
if (purchase.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchase);
}
}
});
await _inAppPurchase.restorePurchases();
}
The validation function
@override
Future<Either<Failure, Unit>> verifySubscription(
{required String token, required String productId}) async {
try {
final settings = locator<ISettingsRepository>();
final InternetConnectionChecker connectionChecker =
InternetConnectionChecker();
final either = await settings.get();
either.fold((f) {
// return left(const Failure(code: FailureCode.unexpected));
}, (settings) async {
if (settings.membership == Membership.paid &&
(await connectionChecker.connectionStatus) ==
InternetConnectionStatus.connected) {
await locator<Dio>().post('google-subscriptions/v1/validate', data: {
"productId": productId,
"purchaseToken": token,
"packageName": "app.expamle"
});
}
});
return right(unit);
} on DioException catch (_) {
_handleFailedSubscribed();
return left(const Failure(code: FailureCode.unexpected));
}
}
The whole class
class IapRepository extends IIapRepository {
IapRepository(this._inAppPurchase, this._settingsDAO) {
_initiated();
}
final InAppPurchase _inAppPurchase;
final SettingsDAO _settingsDAO;
StreamSubscription<void>? _subscription;
void _initiated() async {
await _subscription?.cancel();
_subscription = _inAppPurchase.purchaseStream.listen((purchases) async {
for (final purchase in purchases) {
switch (purchase.status) {
case PurchaseStatus.purchased:
case PurchaseStatus.restored:
// handle if purchased here
log(''' Purchase details:
productID ${purchase.productID}
purchaseID ${purchase.purchaseID}
status ${purchase.status}
transactionDate ${purchase.transactionDate}
verificationData ${purchase.verificationData.localVerificationData} - ${purchase.verificationData.serverVerificationData} - ${purchase.verificationData.source}
''');
_handleSuccessfullySubscribed();
break;
case PurchaseStatus.pending:
// handle if pending here
log('Purchase is pending');
break;
case PurchaseStatus.canceled:
// handle if canceled here
log('Purchase is canceled');
break;
case PurchaseStatus.error:
// handle if error here
log('Purchase has an error');
break;
default:
}
if (purchase.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchase);
}
}
});
await _inAppPurchase.restorePurchases();
}
void _handleSuccessfullySubscribed() async {
await _settingsDAO.customUpdate(
r"UPDATE settings SET details = json_set(details, '$.membership', ?) WHERE id = 1;",
variables: [
Variable<String>(Membership.paid.name),
],
updates: {
_settingsDAO.settingsTable,
},
);
final settings = await _settingsDAO.get();
log('Settings data log ${settings.data}');
}
void _handleFailedSubscribed() async {
await _settingsDAO.customUpdate(
r"UPDATE settings SET details = json_set(details, '$.membership', ?) WHERE id = 1;",
variables: [
Variable<String>(Membership.free.name),
],
updates: {
_settingsDAO.settingsTable,
},
);
final settings = await _settingsDAO.get();
log('Settings data log ${settings.data}');
}
@override
Future<Either<Failure, List<ProductDetails>>> getProducts() async {
try {
final bool isAvailable = await _inAppPurchase.isAvailable();
if (!isAvailable) {
return left(const Failure(code: FailureCode.unexpected));
}
final Set<String> productsIds = {
'expample',
};
final ProductDetailsResponse response =
await _inAppPurchase.queryProductDetails(productsIds);
final List<ProductDetails> products = response.productDetails;
return right(products);
} catch (e) {
return left(const Failure(code: FailureCode.unexpected));
}
}
@override
Future<Either<Failure, Unit>> subscribe(ProductDetails details) async {
try {
await _inAppPurchase.buyNonConsumable(
purchaseParam: PurchaseParam(productDetails: details));
return right(unit);
} catch (e) {
return left(const Failure(code: FailureCode.unexpected));
}
}
@override
Future<Either<Failure, Unit>> verifySubscription(
{required String token, required String productId}) async {
try {
final settings = locator<ISettingsRepository>();
final InternetConnectionChecker connectionChecker =
InternetConnectionChecker();
final either = await settings.get();
either.fold((f) {
// return left(const Failure(code: FailureCode.unexpected));
}, (settings) async {
if (settings.membership == Membership.paid &&
(await connectionChecker.connectionStatus) ==
InternetConnectionStatus.connected) {
await locator<Dio>().post('google-subscriptions/v1/validate', data: {
"productId": productId,
"purchaseToken": token,
"packageName": "app.example"
});
}
});
return right(unit);
} on DioException catch (_) {
_handleFailedSubscribed();
return left(const Failure(code: FailureCode.unexpected));
}
}
}