Reputation: 31
I have been working on a small app which has an option to purchase a premium version. It works perfectly fine for the test cards which automatically accept or decline. However, the slow cards pose a problem. The app will say the purchase is pending (desired behavior), but after the purchase is restored, it will act as if the purchase is charged even if it is still pending, giving access to the premium version until the purchase becomes declined or before they should have access (undesired behavior). The other problem that comes from this is that I locally save if they have premium, so even if they get it for a few minutes, the app will treat as if they have it for life. I have thought of checking for the restore purchase every time the app opens, so it revokes premium privilege to people who didn't yet pay for it, but it comes with the problems of it being avoided through being offline and not preventing anyone from using the a slow, declining card to continually get premium. I mainly just want to know if my code has a problem or if restored purchases are supposed to act like this even if they are payment pending.
I have tried seeing if the purchaseDetailsList
contained more than one restored purchase (did not). I also had the _iap.completePurchase(purchaseDetails)
outside of the two if statements, waiting to see if the purchase was pending completion, but this changed nothing. Finally, I tried a hail mary by getting rid of the restore functionality all together to see if all I needed was purchaseDetails.status == PurchaseStatus.purchased
. It was not.
InAppPurchase _iap = InAppPurchase.instance;
bool _available = true;
List<ProductDetails> _products = [];
List <PurchaseDetails> _purchases = [];
StreamSubscription? _subscription;
BuildContext? contextReference;
void _initialize() async {
_available = await _iap.isAvailable();
if(_available){
_subscription = _iap.purchaseStream.listen((data) => setState(() {
HandlePurchases(data);
}));
List<Future> futures = [_getProducts(), _getPastPurchases()];
await Future.wait(futures);
}
}
Future<void> _getProducts() async {
ProductDetailsResponse response = await _iap.queryProductDetails(Set.from([premiumEditionID]));
if (response.notFoundIDs.isNotEmpty) {
print("Can't Find ID");
}
setState(() {
_products = response.productDetails;
});
}
Future<void> _getPastPurchases() async {
//await Future.delayed(const Duration(milliseconds: 1500));
await _iap.restorePurchases(applicationUserName: null);
}
void HandlePurchases(List<PurchaseDetails> purchaseDetailsList){
purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async {
if (purchaseDetails.status == PurchaseStatus.error) {
PopUp("Error with Purchase");
}
else if (purchaseDetails.status == PurchaseStatus.pending) {
PopUp("Purchase is Pending");
}
else if (purchaseDetails.status == PurchaseStatus.purchased) {
PurchasePremium(true);
if (purchaseDetails.pendingCompletePurchase) {
await _iap.completePurchase(purchaseDetails);
}
}
else if(purchaseDetails.status == PurchaseStatus.restored) {
PurchasePremium(false);
if (purchaseDetails.pendingCompletePurchase){
await _iap.completePurchase(purchaseDetails);
}
}
else if (purchaseDetails.status == PurchaseStatus.canceled) {
PopUp("Purchase was Cancelled");
}
});
}
void PurchasePremium(bool firstPurchase){
setState(() {
if(contextReference != null || firstPurchase == false) {
if(firstPurchase){
Navigator.pop(contextReference!);
}
hasPremium = true;
prefs?.put('premium', true);
if(firstPurchase) {
ScaffoldMessenger.of(contextReference!).showSnackBar(
SnackBar(
content: Text(
"Thank you for your purchase! You now have premium!",
style: GoogleFonts.raleway(
),
),
),
);
}
}
});
}
void PopUp(String text){
setState(() {
if(contextReference != null) {
Navigator.pop(contextReference!);
ScaffoldMessenger.of(contextReference!).showSnackBar(
SnackBar(
content: Text(
text,
style: GoogleFonts.raleway(
),
),
),
);
}
});
}
Upvotes: 2
Views: 941