j3k0 / cordova-plugin-purchase

In-App Purchase for Cordova on iOS, Android and Windows
https://purchase.cordova.fovea.cc
1.3k stars 535 forks source link

[IOS] IAP not working during App Review (gets rejected) even though it works on iOS device and in TestFlight #1163

Closed alexp25 closed 3 years ago

alexp25 commented 3 years ago

system info

Ionic:

   Ionic CLI                     : 5.4.15 (/usr/local/lib/node_modules/ionic)
   Ionic Framework               : @ionic/angular 4.11.11
   @angular-devkit/build-angular : 0.801.3
   @angular-devkit/schematics    : 8.1.3
   @angular/cli                  : 8.3.29
   @ionic/angular-toolkit        : 2.0.0

Cordova:

   Cordova CLI       : 10.0.0
   Cordova Platforms : ios 6.1.1
   Cordova Plugins   : cordova-plugin-ionic-keyboard 2.2.0, cordova-plugin-ionic-webview 4.2.1, (and 41 other plugins)

Utility:

   cordova-res (update available: 0.15.3) : 0.6.0
   native-run (update available: 1.3.0)   : 0.2.8

System:

   ios-deploy : 1.9.4
   ios-sim    : 8.0.2
   NodeJS     : v10.16.0 (/usr/local/bin/node)
   npm        : 6.9.0
   OS         : macOS Catalina
   Xcode      : Xcode 12.4 Build version 12D4e

Expected behavior

IAP should work as expected during App Review

Observed behavior

App gets rejected every time because the IAP strangely does not work during App Review.

We added remote logging and here is the output of the plugin that was logged during App Review:

--- app start

[store.js] DEBUG:   store.trigger -> triggering action refreshed
[store.js] DEBUG: ios -> initializing   storekit
[store.js] DEBUG: ios -> loading   products
[store.js] INFO: ios -> storekit ready
[store.js] DEBUG: ios -> products   loaded
[store.js] DEBUG: ios -> product   bcoin_pack_1 is valid (bcoin_pack_1)
[store.js] DEBUG: state: bcoin_pack_1   -> valid
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: ios -> product   bcoin_pack_2 is valid (bcoin_pack_2)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_2   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_3 is valid (bcoin_pack_3)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_3   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_4 is valid (bcoin_pack_4)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_4   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_5 is valid (bcoin_pack_5)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_5   -> valid
[store.js] DEBUG: update()

--- opened inventory

[store.js] DEBUG: store.trigger ->   triggering action refreshed
[store.js] DEBUG: refresh -> product   id bcoin_pack_2 (bcoin_pack_2)
[store.js] DEBUG: refresh -> product   id bcoin_pack_1 (bcoin_pack_1)
[store.js] DEBUG: refresh -> checking   products state (5 products)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_4 (bcoin_pack_4)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_3 (bcoin_pack_3)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_5 (bcoin_pack_5)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: store.trigger ->   triggering action re-refreshed
[store.js] INFO: ios -> restore   completed
[store.js] DEBUG: store.trigger ->   triggering action refresh-completed
[store.js] DEBUG: store.trigger ->   triggering action refresh-finished
[store.js] DEBUG: store.trigger ->   triggering action refresh-finished

The in-app purchases work perfectly when tested on iOS 14.3 device and in TestFlight. Only during App Review something happens that I cannot possibly explain. Here is the message from App Review:

"We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 14.4 on Wi-Fi. Specifically, we were unable to purchase the in-app purchases. User was not able purchase IAP. No modal alert appearing for IAP purchase." (was the same with iOS 14.3)

However, I do not see any error in the logs, and it seems that the products are not even ordered? What could possibly happen, as it's the 5th time the app gets rejected for the same (unknown) reason.

[EDIT] It seems that the update event is not being triggered under the described circumstances, which explains why the store.order method is not even being called, as in the code I wait until the product is refreshed (updated) to make a purchase. Why does this happen only in App Review/App Store, but not on testing device?

Here is a successful log (from my testing device)

--- app start

[store.js] DEBUG:   store.trigger -> triggering action refreshed
[store.js] DEBUG: ios -> initializing   storekit
[store.js] INFO: ios -> storekit ready
[store.js] DEBUG: ios -> loading   products
[store.js] DEBUG: ios -> products   loaded
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: ios -> product   bcoin_pack_1 is valid (bcoin_pack_1)
[store.js] DEBUG: state: bcoin_pack_1   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_2 is valid (bcoin_pack_2)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_2   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_3 is valid (bcoin_pack_3)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_3   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_4 is valid (bcoin_pack_4)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_4   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_5 is valid (bcoin_pack_5)
[store.js] DEBUG: ios -> owned? false
[store.js] DEBUG: state: bcoin_pack_5   -> valid
[store.js] DEBUG: update()
[store.js] DEBUG: state: com.leplace.town   ->
[store.js] DEBUG: state: com.leplace.town   -> registered
[store.js] DEBUG: ios -> product   com.leplace.town registered
[store.js] DEBUG: state: com.leplace.town   -> approved
[store.js] DEBUG: state: com.leplace.town   -> approved
[store.js] DEBUG: verify ->   {success":true
[store.js] DEBUG: syncWithAppStoreReceipt
[store.js] DEBUG: transaction fields for   com.leplace.town
[store.js] DEBUG: verify -> success:   {id":"com.leplace.town"
[store.js] DEBUG:   {type":"ios-appstore"

--- opened inventory

[store.js] DEBUG: store.trigger ->   triggering action refreshed
[store.js] DEBUG: refresh -> product   id bcoin_pack_2 (bcoin_pack_2)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> checking   products state (6 products)
[store.js] DEBUG: refresh -> product   id bcoin_pack_3 (bcoin_pack_3)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_1 (bcoin_pack_1)
[store.js] DEBUG: refresh -> product   id bcoin_pack_4 (bcoin_pack_4)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_5 (bcoin_pack_5)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id com.leplace.town (application)
[store.js] DEBUG:            in state 'registered'
[store.js] DEBUG: store.trigger ->   triggering action re-refreshed
[store.js] DEBUG: store.trigger ->   triggering action refresh-completed
[store.js] INFO: ios -> restore   completed
[store.js] DEBUG: store.trigger ->   triggering action refresh-finished
[store.js] DEBUG: store.trigger ->   triggering action refresh-finished

--- here we see the store update() event

[store.js] DEBUG: update()
[store.js] DEBUG: state: com.leplace.town   -> approved
[store.js] DEBUG: state: com.leplace.town   -> approved
[store.js] DEBUG: transaction fields for   com.leplace.town
[store.js] DEBUG: verify -> success:   {id":"com.leplace.town"
[store.js] DEBUG: syncWithAppStoreReceipt
[store.js] DEBUG: verify ->   {success":true
[store.js] DEBUG:   {type":"ios-appstore"

--- now the product is being requested and everything works as expected

[store.js] DEBUG: state: bcoin_pack_1   -> requested
[store.js] DEBUG: ios -> is purchasing   bcoin_pack_1
[store.js] DEBUG: state: bcoin_pack_1   -> initiated
[store.js] INFO: ios -> transaction   1000000774082875 purchased (1 in the queue for bcoin_pack_1)
[store.js] DEBUG: state: bcoin_pack_1   -> approved
[store.js] DEBUG: product -> defer   finishing bcoin_pack_1
[store.js] DEBUG: state: bcoin_pack_1   -> finished
[store.js] DEBUG: product -> finishing   bcoin_pack_1
[store.js] DEBUG: ios -> finishing   bcoin_pack_1 (a consumable)
[store.js] DEBUG: state: bcoin_pack_1   -> valid
[store.js] DEBUG: ios -> product   bcoin_pack_1 owned=false
[store.js] DEBUG: store.trigger ->   triggering action refreshed
[store.js] DEBUG: refresh -> checking   products state (6 products)
[store.js] DEBUG: refresh -> product   id bcoin_pack_1 (bcoin_pack_1)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_2 (bcoin_pack_2)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_3 (bcoin_pack_3)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_4 (bcoin_pack_4)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id bcoin_pack_5 (bcoin_pack_5)
[store.js] DEBUG:            in state 'valid'
[store.js] DEBUG: refresh -> product   id com.leplace.town (application)
[store.js] DEBUG:            in state 'approved'
[store.js] DEBUG: store.trigger ->   triggering action re-refreshed
[store.js] DEBUG: store.trigger ->   triggering action refresh-completed
[store.js] INFO: ios -> restore   completed
[store.js] DEBUG: update()
[store.js] DEBUG: state: com.leplace.town   -> approved
[store.js] DEBUG: transaction fields for   com.leplace.town
[store.js] DEBUG: verify ->   {success":true
[store.js] DEBUG:   {type":"ios-appstore"
[store.js] DEBUG: syncWithAppStoreReceipt
[store.js] DEBUG: verify -> success:   {id":"com.leplace.town"

Steps to reproduce

Create an app with in-app purchases and submit it for review on App Store. No other way could I reproduce the problem.

valuks commented 3 years ago

Is any progress?

I can approve: "consumables" and "subscriptions" is not working on the production. We got through the review and left users without payments.

Be caution!

alexp25 commented 3 years ago

I have figured it out. The store.order method should be called immediately when initiating the purchase and not after the update event is triggered after calling store.refresh (never triggers in production, after the products have already been registered). The store.refresh method is called only once after registering all handlers. I've also handled all other events now (but this should not be a problem). Now the app got approved. Can only confirm for "consumables"

valuks commented 3 years ago

I have figured it out. The store.order method should be called immediately when initiating the purchase and not after the update event is triggered after calling store.refresh (never triggers in production, after the products have already been registered). The store.refresh method is called only once after registering all handlers. I've also handled all other events now (but this should not be a problem). Now the app got approved. Can only confirm for "consumables"

Can You show an example (which work and which does not work)?

alexp25 commented 3 years ago
// this works in TestFlight and on real device, doesn't work in production (used to work)
purchaseItem(productId) {
    return new Promise((resolve, reject) => {
        let product = this.store.get(productId);
        // helper function
        waitForIAPEvent(productId, [IAPEvents.APPROVED]).then((event) => {
            // register product
            // ...
            resolve(true);
        });
        // also handle other events (e.g. error, cancelled)
        // ...
        this.waitForIapEvent(productId, [IAPEvents.UPDATED]).then((event) => {
            this.store.order(productId);
        });
        this.store.refresh();
    });
}

// this also works in production
purchaseItem(productId) {
    return new Promise((resolve, reject) => {
        // helper function
        waitForIAPEvent(productId, [IAPEvents.APPROVED]).then((event) => {
            // register product
            // ...
            resolve(true);
        });
        // also handle other events (e.g. error, cancelled)
        // ...
        this.store.order(productId);
        this.store.refresh();
    });
}

In both cases, the initialization (store.register) is done only once, when the app starts, registering all IAP products.

thenaim commented 3 years ago

@alexp25, Hi, I have the same problem but with a subscription. The application works 100%, but the app review does not pass, they write that the application does not respond to pressing the subscription button on the iPad. If it's not difficult, can you share a complete working code or at least a helper function? Thank you!