cordova-plugin-purchase on iOS: order() does not work without uninstalling the app first - ios

We are developing an application that uses Ionic (Angular) and Capacitor together with cordova-plugin-purchase to offer subscriptions (monthly and yearly). Purchases work perfectly on Android. However, on iOS, while the first purchase works fine, after that purchase expires (via cancelling the subscription, since it is actually automatically renewing) and the user purchases any product again (now on valid status since it expired already), calling the order() function does not do anything. The product status remains valid, no logs are written in the JavaScript console, nor Xcode. No errors, too. We have verified that our verification servers are returning correct responses. However, when we uninstall the app and install it from Xcode again, purchases start working again, and calling the order() function does show the purchase dialog again, and things work as expected. Until the subscription expires and they try to purchase again.
However, if the product is not yet expired, and the user tries to change subscription by subscribing to another subscription type, calling the order() function works as expected. It is only when a subscription expires, and then it starts to not work until the application is deleted/uninstalled and then installed again.
We implement the purchase using pretty standard code--we tried as minimal as possible to make it simple.
Initialization:
this.store.validator = 'https://xxxxxxx/api/app/purchased';
this.store.register([
{
type: this.store.PAID_SUBSCRIPTION,
id: this.YEARLY_SUBSCRIPTION
},
{
type: this.store.PAID_SUBSCRIPTION,
id: this.MONTHLY_SUBSCRIPTION
}
]);
this.subscriptions.add(
this.status.userData.subscribe(ud => {
this.user = ud;
this.store.applicationUsername = ud.id.toString(10);
})
);
this.store.when(this.YEARLY_SUBSCRIPTION)
.cancelled(this.purchaseCancelled)
.updated(this.purchaseYearlyUpdated)
.approved(this.purchaseApproved)
.verified(this.purchaseVerified)
.owned(this.purchaseOwned);
this.store.when(this.MONTHLY_SUBSCRIPTION)
.cancelled(this.purchaseCancelled)
.updated(this.purchaseMonthlyUpdated)
.approved(this.purchaseApproved)
.verified(this.purchaseVerified)
.owned(this.purchaseOwned);
this.store.error(this.handleError);
this.store.autoFinishTransactions = true;
this.store.refresh();
Other functions:
purchaseCancelled = (p: IAPProduct) => {
console.log(`${ p.id } cancelled`);
}
purchaseYearlyUpdated = (p: IAPProduct) => {
console.log(`${ p.id } updated`);
this.yearlyProduct = p;
this.changeDetectorRef.detectChanges();
}
purchaseMonthlyUpdated = (p: IAPProduct) => {
console.log(`${ p.id } updated`);
this.monthlyProduct = p;
this.changeDetectorRef.detectChanges();
}
purchaseApproved = (p: IAPProduct) => {
console.log(`${ p.id } approved`);
p.verify();
}
purchaseVerified = (p: IAPProduct) => {
console.log(`${ p.id } verified`);
p.finish();
}
purchaseOwned = (p: IAPProduct) => {
console.log(`${ p.id } owned`);
}
handleError = (error) => {
console.log('Error event:', error);
}
purchase(id: string) {
this.store.order(id);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
this.store.off(this.purchaseCancelled);
this.store.off(this.purchaseYearlyUpdated);
this.store.off(this.purchaseMonthlyUpdated);
this.store.off(this.purchaseApproved);
this.store.off(this.purchaseVerified);
this.store.off(this.purchaseOwned);
this.store.off(this.handleError);
}
Steps to reproduce:
Install the app from Xcode.
Purchase a product (user clicks the purchase button, that button triggers store.order)
Exit app
Cancel subscription so it expires
Subscription expires
Open app, and confirm that subscription is indeed expired (status goes from approved to valid)
Try to purchase subscription again by clicking the purchase button (any subscription)
Nothing happens. No logs. No errors. Product status remains valid.
Closing the app and then running again does not help.
Uninstall app
Install app again from Xcode
Purchase works again
I can't see anything wrong with the code, and since it's working perfectly fine on Android, we're guessing that it might be something wrong with the phone or maybe the cordova-plugin-purchase plugin.
I would really appreciate any suggestions, ideas, opinions, anything. Thank you very much in advance. I'm getting quite desperate.

Related

iOS in app purchases through cordova-plugin-purchase?

I'm using Capacitor (but not Ionic) to package a SvelteKit application for iOS and am trying to get an in-app-purchase working.
Capacitor's page on in-app-purchases is surprisingly unhelpful. I've done my best and:
I have the products set up in appstoreconnect and they're status is "ready to submit"
I've installed cordova-plugin-purchase and run npx cap update and npx cap sync and it's installing
[info] Found 1 Cordova plugin for ios:
cordova-plugin-purchase#13.0.3
I've tried to make the simplest test I could just to see what's going on:
import 'cordova-plugin-purchase'; // This seems to add `CdvPurchase` to the global scope.
function buy() {
const {store, ProductType, Platform} = CdvPurchase;
store.verbosity = store.DEBUG;
store.register([{
type: ProductType.CONSUMABLE,
id: "my-product-id",
platform: Platform.APPLE_APPSTORE,
}]);
store.error(e => {
console.log('error', e);
});
store.when()
.productUpdated(() => {
console.log('product updated', product);
})
.approved(value => {
console.log('approved', value);
})
.verified(value => {
console.log('verified', value);
})
.finished(value => {
console.log('finished', value);
});
store.ready(() => {
console.log('ready', store.products);
store.order('my-product-id');
});
store.initialize(Platform.APPLE_APPSTORE)
.then(() => {
console.log('initialize resolved', store.products);
store.order('my-product-id');
});
}
But I run the buy function, all I get is:
[log] - [CordovaPurchase] INFO: initialize()
The store never reports as ready. None of the listeners are triggered, not even .error().
Have I missed something? How do I debug this?

Flutter ios appstore validateReceipt on non-consumable in-app purchase

I seem to be stuck on this. Trying to validate the receipt (server side) on an in-app purchase on IOS (haven't tried with android, yet.)
I'm using the official in_app_purchase pub package.
This is the setup to initialize the purchase:
Future<bool> initiatePurchase() async {
...
(verify store is available)
..
print ("==> Store available, initiating purchase");
final PurchaseParam purchaseParam =
PurchaseParam(productDetails: _productDetails![0]);
await InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
return true;
}
Here's my verify purchase call:
Future<bool> _verifyPurchase(PurchaseDetails purchaseDetails) async {
PurchaseVerifRest purchaseRest = PurchaseVerifRest();
Map<String,dynamic> rsp = await purchaseRest.verifyPurchase(
{
"source": purchaseDetails.verificationData.source,
"vfdata": purchaseDetails.verificationData.serverVerificationData
});
// bundle up the source and verificationData in a map and send to rest
// call
return rsp['status'] == 200;
}
On the server side, the code looks like this (NodeJS/express app)
// (in router.post() call - 'purchaseData' is the map sent in the above code,
// the 'vfdata' member is the 'serverVerificationData'
//. in the 'purchaseDetails' object)
if (purchaseData.source == ('app_store')) {
const IOS_SHARED_SECRET = process.env...;
let postData = {
'receipt-data': purchaseData['vfdata'],
'password': IOS_SHARED_SECRET
};
try {
let verif_rsp = await execPost(postData);
retStatus = verif_rsp.statusCode;
msg = verif_rsp.data;
} catch (e) {
retStatus = e.statusCode;
}
}
What I get back, invariably is
210003 - Receipt could not be authenticated
... even though the purchase seems to go through, whether I validate or not.
Details/questions:
Testing with a sandbox account.
This is for a 'non-consumable' product purchase.
I'm assuming that purchaseDetails.verificationData.serverVerificationData is the payload containing the receipt to send to Apple for verification. Is this not correct? Is there another step I need to do to get the receipt data?
I've read in other posts that the verification step is only for recurring subscriptions and not for other types of products. Is this correct? I don't see anything in Apple's docs to indicate this.
Any thoughts appreciated.

getPurchaseHistory won't work with expo in app purchases

I am currently trying to implement a feature that allows you to restore an old in-app purchase a user may have. I am currently using expo (38.0) along with expo in-app purchases (9.1) and testing on iOS testflight. I have already implemented the purchasing feature and it works just fine, but this feature won't work. Here is the code for it.
restorePurchase = async () => {
this.setState({store: true})
this.setState({isLoading: true})
let found = false
const { responseCode, results } = await InAppPurchases.getPurchaseHistoryAsync()
if (responseCode === InAppPurchases.IAPResponseCode.OK){
results.forEach(result => {
if (result.acknowledged){
found = true
}
})
}
if (responseCode === InAppPurchases.IAPResponseCode.ERROR) {
this.setState({isLoading: false, store: false})
Alert.alert('Error has occured, please report this bug to the email found in the settings.')
} else if (found) {
const paid = true
const JSONpaid = JSON.stringify(paid)
await AsyncStorage.setItem("Paid", JSONpaid)
this.setState({isLoading: false, store: false})
Alert.alert('Purchase was found and your account was updated! Thank you for your support!')
} else {
this.setState({isLoading: false, store: false})
Alert.alert('No purchase was found, sorry!')
}
}
In theory, the code should grab their purchase history, look for an acknowledged purchase, and then update their data accordingly. Once the button is clicked, it ends up stuck on the loading screen indefinitely with no resolve. Since I am using testflight, I unfortunately do not have any way to check the errors, unless someone knows of a way to do so. I don't know how to approach this issue, so please let me know if anyone has any ideas. Thank you in advance!

Cordova/Ionic IAP not always works

I have made my Cordova/Ionic app with in app purchases. During test period all works fine, published it live in Google Play and App Store all seems to be fine. Get lots of purchases, tested it myself on different devices and all seems ok. But then after a week the first email came in they purchases an IAP and it was not unlocked (iOS, have any issues heared from Android users). Tow weeks later a second... now I have like 15 people where is does not work and I have seen prove that they have purchased it.
I am using this IAP plugin:
https://github.com/AlexDisler/cordova-plugin-inapppurchase
Before that one I used another IAP plugin, but because of the same problem I started to use the first one:
https://github.com/j3k0/cordova-plugin-purchase
I also made a issue post on github but still no answer:
As I am not allowed to post more links atm, it is issue 113.
The code I use works in 99.9%:
loadProducts = function () {
$ionicLoading.show({ template: spinner + 'Loading...' });
inAppPurchase
.getProducts(productIds)
.then(function (products) {
$ionicLoading.hide();
// Load all products by ID
products.forEach(function (product) {
// Add extra field to the product object
product.owned = false;
switch (product.productId) {
case ncProductProVer:
if (GlobalVars.vars.fullVersion == true) {
product.owned = true;
}
break;
case ncProductNoAds:
if (GlobalVars.vars.noAds == true) {
product.owned = true;
}
break;
}
});
//alert(products[0].productId + ' ' + products[1].productId);
$scope.products = products;
})
.catch(function (err) {
$ionicLoading.hide();
//$cordovaDialogs.alert('Error loading products. Details:\n- Code: ' + err.code + '\n- Message: ' + err.message, 'Error', 'OK')
$scope.CodeError = true;
});
};
// BUY a product
$scope.buy = function (productId) {
$ionicLoading.show({ template: spinner + 'Buying...' });
inAppPurchase
.buy(productId)
.then(function (data) {
$ionicLoading.hide();
enableIAP(productId, true);
})
.then(function () {
$ionicLoading.hide();
loadProducts();
})
.catch(function (err) {
$ionicLoading.hide();
errHandling(err.code, productId);
});
};
// RESTORE in app purchases
$scope.restore = function () {
$ionicLoading.show({ template: spinner + 'Restoring purchases...' });
inAppPurchase
.restorePurchases()
.then(function (purchases) {
$ionicLoading.hide();
purchases.forEach(function (purchase) {
// Unlock the relevant feature based on this product id, if any
// Store the purchase in localStorage
enableIAP(purchase.productId, true);
});
// Reload the productlist
loadProducts();
})
.catch(function (err) {
$ionicLoading.hide();
});
};
I have tried many different things already, but cant figure out what the problem can be or how I can see what goes wrong as the app runs on a device where I don't have access to :).
The only thing I have is that people say the 'Buying...' popup keeps on loading till they close the app and run it again. So the problem must me in the .buy() function and does not give a error back but otherwise it will be in the .catch().
Things I can think of is:
- Apple does not always sends the same string back
- The string send back from Apple contains chars that are not correctly handled by the IAP code
To make things more odd, I managed to get in contact with somebody where it did not work. So I started testing some things as I thought maybe it could be device settings. So I had the iPhone and signed in with my iTunes account, downloaded the app and restored the IAP and it worked. Did the same thing, sign in with their iTunes account, did not work. That person also had an iPad mini, where it all worked as expected, the extra item in the app where unlocked as they should.
Anybody seen this strange behaviour? Been busy many weeks/months already to figure out what it could be.

Error in FB.login() phonegap facebook plugin

When i run fb.login() in an ios 6 device using the account setted in Settings/facebook i get the following
operation couldn't be completed (com.facebook.sdk error 2)
in ios 5 the app works perfectly, the problem is the native login
help please ! this is my code
this is the facebook init
this.appId = appId;
var me = this;
document.addEventListener('deviceready', function() {
try {
FB.init({ appId: "294003447379425", status: true, cookie: true, nativeInterface: CDV.FB, useCachedDialogs: false });
} catch (e) {
console.log(e);
}
}, false);
if (!this.appId) {
Ext.Logger.error('No Facebook Application ID set.');
return;
}
var me = this;
me.hasCheckedStatus = false;
FB.Event.subscribe('auth.logout', function() {
// This event can be fired as soon as the page loads which may cause undesired behaviour, so we wait
// until after we've specifically checked the login status.
if (me.hasCheckedStatus) {
me.fireEvent('logout');
}
});
// Get the user login status from Facebook.
FB.getLoginStatus(function(response) {
me.fireEvent('loginStatus');
clearTimeout(me.fbLoginTimeout);
me.hasCheckedStatus = true;
if (response.status == 'connected') {
me.fireEvent('connected');
Ext.Viewport.add(Ext.create('CinePass.view.Main'));
} else {
me.fireEvent('unauthorized');
console.log('noconectado');
Ext.Viewport.add(Ext.create('CinePass.view.LoggedOut'));
}
});
// We set a timeout in case there is no response from the Facebook `init` method. This often happens if the
// Facebook application is incorrectly configured (for example if the browser URL does not match the one
// configured on the Facebook app.)
me.fbLoginTimeout = setTimeout(function() {
me.fireEvent('loginStatus');
me.fireEvent('exception', {
type: 'timeout',
msg: 'The request to Facebook timed out.'
});
Ext.Msg.alert('CinePass', 'Existe un problema con Facebook, se iniciará la aplicación sin conexión a Facebook.', Ext.emptyFn);
Ext.Viewport.add(Ext.create('CinePass.view.Main'));
}, me.fbTimeout);
this is the login
FB.login(
function(response) {
if (response.session) {
alert(response.session);
} else {
alert(response.session);
}
},
{ scope: "email,user_about_me,user_activities,user_birthday,user_hometown,user_interests,user_likes,user_location,friends_interests" }
);
Having the same problem and took me few days to figure this out, there are a number of reasons:
1) The first time you launch your app, if you deny the "Do you allow this app to use your facebook permission", you'll get this error, what you need to do is, go to Setting > General > Facebook > turn on your app OR go to Setting > General > Reset > Reset Location & Privacy (this will reset and ask you the "Do you allow this app to use your facebook permission" again for all app.
2) Slow/No Internet can caused the error
3) Create your certificate again, and go to build.phonegap.com, create a new key for IOS and change the IOS key (This works for me, I have no idea how it works but it does, I notice a significant app file size increase after i use a new IOS Key)
4) If your app is in sand box mode, and your IOS current Facebook account is not an app tester you'll get the error too. I just disable the sand box mode and it works, make sure your Facebook App Setting has the correct bundle ID, if your app is still under developement, put 0 to app store ID.

Resources