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 doesn't recognise purchase after closing/reopening the app #1606

Open kreso22 opened 2 months ago

kreso22 commented 2 months ago

Observed behavior

Making a purchase works (product is owned). Next app load - receipt shows product is not owned. Only after hitting restorePurchase does the receipt show the product as owned

Not using validation. App has one single product.

Expected behavior

Product owned persists.

Code

Currently forcing the restorePurchases() on random event (when app starts). Otherwise, product will be shown as not owned.


        // Register the product
        CdvPurchase.store.register([{
            id: PROD_ID,
            platform: PLATFORM,
            type: CdvPurchase.ProductType.NON_CONSUMABLE
        }]);

        // Handle product updates
        CdvPurchase.store.when().productUpdated(product => {
            if(product.id == PROD_ID) {
                processIAP(product.owned);          
            }
        });

        CdvPurchase.store.when().receiptUpdated(localReceipt => {
            const product = CdvPurchase.store.get(PROD_ID);

            if (product) {
                processIAP(product.owned);
            }
        });

        CdvPurchase.store.when().receiptsReady(() => {
            MyDebug.log("STORE", "receiptsReady! ...");         
            CdvPurchase.store.restorePurchases();
        });

        CdvPurchase.store.when().approved(transaction => {
            processIAP(true);
            transaction.finish();
        });

        CdvPurchase.store.initialize([
            {
                platform: PLATFORM,
                options: { needAppReceipt: false }
            }
        ]);

    }

initiatePurchase() {
        CdvPurchase.store.get(PROD_ID).getOffer().order().then(error => {
                if (error) {
                    // failed to buy
                    alert("Purchase not successful. Error:" + error.code);
                }
            });
    }
j3k0 commented 1 month ago

I understand it's a workaround but do not do this:

CdvPurchase.store.when().receiptsReady(() => {
    CdvPurchase.store.restorePurchases();
});

As mentioned in the documentation, "restorePurchases" should only be called when the user clicks the "restore purchases" button. This call will ask for the user appstore password (except if they already logged in in the last 15 minutes, like just after downloading the app).

Can you share the startup logs when the non consumable is owned (without that call)?

kreso22 commented 1 month ago

I understand it's a workaround but do not do this:

CdvPurchase.store.when().receiptsReady(() => {
  CdvPurchase.store.restorePurchases();
});

As mentioned in the documentation, "restorePurchases" should only be called when the user clicks the "restore purchases" button. This call will ask for the user appstore password (except if they already logged in in the last 15 minutes, like just after downloading the app).

Can you share the startup logs when the non consumable is owned (without that call)?

Sure!

Here is the related Xcode startup log:

[CDVTimer][console] 0.012994ms
[CDVTimer][handleopenurl] 0.012040ms
[CDVTimer][intentandnavigationfilter] 0.587940ms
[CDVTimer][gesturehandler] 0.012994ms
[CDVTimer][applovinmax] 0.067949ms
[CDVTimer][inappbrowser] 0.017047ms
[CdvPurchase.AppleAppStore.objc] Initialized.
[CDVTimer][inapppurchase] 10.787964ms
[CDVTimer][screenedgesplugin] 0.082016ms
[CDVTimer][cdvwkwebviewfilexhr] 49.003005ms
[CDVTimer][socialsharing] 0.018001ms
[CDVTimer][TotalPluginStartup] 60.752034ms
[CdvPurchase.AppleAppStore.objc] (before init): WARNING: Your app should be single page to use in-app-purchases. onReset is not supported.
Create CdvPurchase...

And from inside the app once started:

[STORE] - "initializeStore" - "CVD Platform:" - "ios-appstore" - "Product name:" XXXX
[STORE] - "[productUpdated]. product:" - SKProduct {className: "Product", title: "Remove ads", description: "No more ads", ...
[STORE] - "Processing ownership. Owned:" - false
[STORE] - "[receiptsReady]."
[STORE] - "[receiptUpdated]. localreceipt:" - SKApplicationReceipt {className: "Receipt", transactions: [1, platform: "ios-appstore", ...}
[STORE] - "Processing ownership. Owned:" - false
[STORE] - "[receiptUpdated]. localreceipt:" - Receipt {className:
"Receipt", transactions: [1, platform: "ios-appstore", ...}
[STORE] - "Processing ownership. Owned: " - false

The first receiptUpdated is the app. Second receiptUpdate looks empty (no transactions, nothing ...).

On each receiptUpdate I check if it is for desired product (I have only 1 in my app) like so:


CdvPurchase.store.when().receiptUpdated(localReceipt => {

     log("STORE", "[receiptUpdated]. localreceipt:", localReceipt);

     const product = CdvPurchase.store.get(PROD_ID);

     if (product) {
          processOwnership(product.owned);
     }
});
j3k0 commented 1 month ago

Sorry the logs do not contain enough information to see what happens. Can you please include the content of the localReceipt? Set logs level to debug CdvPurchase.store.verbosity = CdvPurchase.LogLevel.DEBUG and include everything in logs that contain the "CdvPurchase" tag.

kreso22 commented 1 month ago

No problem! Here is the log with DEBUG verbosity. I have redacted actual receipt, and product and app ids.

[CdvPurchase.AppleAppStore.objc] Initialized.
[CdvPurchase.AppleAppStore.objc] (before init): WARNING: Your app should be single page to use in-app-purchases. onReset is not supported.
Create CdvPurchase...
[CdvPurchase] INFO: initialize([{"platform":"ios-appstore","options":{"needAppReceipt":false}}]) v13.11.1
[CdvPurchase.Adapters] INFO: Adding platforms: [{"platform":"ios-appstore","options":{"needAppReceipt":false}}]
[CdvPurchase.Adapters] INFO:
[CdvPurchase.Adapters] INFO: AppStore initializing...
[CdvPurchase.AppleAppStore] INFO: bridge.init
[CdvPurchase.AppleAppStore.objc] setup: OK
[CdvPurchase.AppleAppStore.Bridge] DEBUG: setup ok
[CdvPurchase.AppleAppStore] INFO: ready
[CdvPurchase.AppleAppStore] INFO: bridge.init done
[CdvPurchase.AppleAppStore.objc] canMakePayments: Device can make payments.
[CdvPurchase.Adapters] INFO: AppStore initialized.
[CdvPurchase.Adapters] INFO: AppStore products: [{"id":"com.my.productid","platform":"ios-appstore","type":"non consumable"}]
[CdvPurchase.AppleAppStore] INFO: bridge.load
[CdvPurchase.AppleAppStore.Bridge] DEBUG: load ["com.my.productid"]
[CdvPurchase.AppleAppStore.objc] load: Getting products data
[CdvPurchase.AppleAppStore.objc] load: Set has 1 elements
[CdvPurchase.AppleAppStore.objc] load:  - com.my.productid
[CdvPurchase.AppleAppStore.objc] load: Starting product request...
[CdvPurchase.AppleAppStore.objc] load: Product request started
[CdvPurchase.AppleAppStore.Bridge] DEBUG: processing pending transactions
[CdvPurchase.AppleAppStore.objc] processPendingTransactionUpdates
[CdvPurchase.AppleAppStore] DEBUG: loading appstore receipt...
[CdvPurchase.AppleAppStore.Bridge] DEBUG: loading appStoreReceipt
[CdvPurchase.AppleAppStore.objc] appStoreReceipt:
[CdvPurchase.AppleAppStore.Bridge] DEBUG: infoPlist: com.my.appid,1.1.60,0,????
[CdvPurchase.AppleAppStore] DEBUG: appstore receipt loaded
[CdvPurchase.AdapterListener] DEBUG: receiptsReady: ios-appstore (1/0)
[CdvPurchase.AppleAppStore] DEBUG: receipt updated and ready.
[CdvPurchase.AdapterListener] DEBUG: receiptsUpdated: [{"className":"Receipt","transactions":[],"platform":"ios-appstore","nativeData":{"appStoreReceipt":"MII...Q==","bundleIdentifier":"com.my.appid","bundleShortVersion":"1.1.60","bundleNumericVersion":0,"bundleSignature":"????"}},{"className":"Receipt","transactions":[],"platform":"ios-appstore"}]
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase.AppleAppStore.objc] BatchProductsRequestDelegate.productsRequest:didReceiveResponse:
[CdvPurchase.AppleAppStore.objc] BatchProductsRequestDelegate.productsRequest:didReceiveResponse: Has 1 validProducts
[CdvPurchase.AppleAppStore.objc] BatchProductsRequestDelegate.productsRequest:didReceiveResponse:  - com.my.productid: Remove ads
[CdvPurchase.AppleAppStore.objc] BatchProductsRequestDelegate.productsRequest:didReceiveResponse: sendPluginResult: (
        (
                {
            billingPeriod = 0;
            billingPeriodUnit = Day;
            countryCode = US;
            currency = USD;
            description = "No more ads";
            discounts =             (
            );
            group = "<null>";
            id = "com.my.productid";
            introPrice = "<null>";
            introPriceMicros = "<null>";
            introPricePaymentMode = "<null>";
            introPricePeriod = "<null>";
            introPricePeriodUnit = "<null>";
            price = "$1.99";
            priceMicros = 1990000;
            title = "Remove ads";
        }
    ),
        (
    )
)
[CdvPurchase.AppleAppStore.Bridge] DEBUG: load ok: { valid:[{"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null}] invalid:[] }
[CdvPurchase.AppleAppStore] INFO: bridge.loaded: {"validProducts":[{"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null}],"invalidProducts":[]}
[CdvPurchase.AppleAppStore] DEBUG: load eligibility: [{"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null}]
[CdvPurchase.AppleAppStore] DEBUG: No discount eligibility determiner, skipping...
[CdvPurchase.AppleAppStore] INFO: eligibilities ready: {"request":[],"response":[]}
[CdvPurchase.AppleAppStore] DEBUG: com.my.productid is valid: {"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null}
[CdvPurchase.AppleAppStore] DEBUG: registering new product
[CdvPurchase.AppleAppStore] DEBUG: Products loaded: [{"className":"Product","title":"Remove ads","description":"No more ads","platform":"ios-appstore","type":"non consumable","id":"com.my.productid","offers":[{"className":"Offer","id":"$","pricingPhases":[{"price":"$1.99","priceMicros":1990000,"currency":"USD","paymentMode":"UpFront","recurrenceMode":"NON_RECURRING"}],"productId":"com.my.productid","productType":"non consumable","platform":"ios-appstore","offerType":"Default"}],"raw":{"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null},"countryCode":"US"}]
[CdvPurchase.Adapters] INFO: AppStore products loaded: [{"className":"Product","title":"Remove ads","description":"No more ads","platform":"ios-appstore","type":"non consumable","id":"com.my.productid","offers":[{"className":"Offer","id":"$","pricingPhases":[{"price":"$1.99","priceMicros":1990000,"currency":"USD","paymentMode":"UpFront","recurrenceMode":"NON_RECURRING"}],"productId":"com.my.productid","productType":"non consumable","platform":"ios-appstore","offerType":"Default"}],"raw":{"id":"com.my.productid","description":"No more ads","introPrice":null,"introPricePaymentMode":null,"billingPeriodUnit":"Day","countryCode":"US","introPricePeriodUnit":null,"discounts":[],"title":"Remove ads","price":"$1.99","billingPeriod":0,"group":null,"priceMicros":1990000,"currency":"USD","introPricePeriod":null,"introPriceMicros":null},"countryCode":"US"}]
[CdvPurchase.Adapters] INFO: AppStore receipts loaded: [{"className":"Receipt","transactions":[],"platform":"ios-appstore","nativeData":{"appStoreReceipt":"MII...Q==","bundleIdentifier":"com.my.appid","bundleShortVersion":"1.1.60","bundleNumericVersion":0,"bundleSignature":"????"}},{"className":"Receipt","transactions":[],"platform":"ios-appstore"}]
[CdvPurchase.AdapterListener] DEBUG: setSupportedPlatforms: ios-appstore (1 have their receipts ready)
[CdvPurchase.AdapterListener] DEBUG: triggering receiptsReady()
[CdvPurchase] DEBUG: Calling callback: type=productUpdated() name=#3532c2af0630224c668390e441c804b7 reason=adapterListener_productsUpdated
[CdvPurchase] DEBUG: Calling callback: type=receiptsReady() name=receiptsMonitor_setup reason=adapterListener_setSupportedPlatforms
[CdvPurchase.ReceiptsMonitor] DEBUG: receiptsReady...
[CdvPurchase] DEBUG: Calling callback: type=receiptsReady() name=#62332256874e449c2d0c447e06ef51b9 reason=adapterListener_setSupportedPlatforms
[CdvPurchase.ReceiptsMonitor] DEBUG: check(0/0)
[CdvPurchase.ReceiptsMonitor] INFO: receiptsVerified()

And here is what I see after I press RESTORE PURCHASE:

[CdvPurchase.AppleAppStore.objc] paymentQueue:updatedTransactions: com.my.productid
[CdvPurchase.AppleAppStore.objc] paymentQueue:updatedTransactions: State: PaymentTransactionStateRestored
[CdvPurchase.AppleAppStore.objc] processTransactionUpdate:withArgs: transactionIdentifier=2000000734344505
[CdvPurchase.AppleAppStore.objc] paymentQueueRestoreCompletedTransactionsFinished:
[CdvPurchase.AppleAppStore.Bridge] DEBUG: transaction updated:2000000734344505 state:PaymentTransactionStateRestored product:com.my.productid
[CdvPurchase.AppleAppStore] INFO: restore: 2000000734344505 - com.my.productid
[CdvPurchase.AppleAppStore] DEBUG: initializeAppReceipt() => already initialized.
[CdvPurchase.AppleAppStore] INFO: restoreCompleted
[CdvPurchase.AppleAppStore.Bridge] DEBUG: refreshing appStoreReceipt
[CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Request to refresh app receipt
[CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Starting receipt refresh request...
[CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Receipt refresh request started
[CdvPurchase.AppleAppStore.objc] RefreshReceiptDelegate.requestDidFinish: Got refreshed receipt
[CdvPurchase.AppleAppStore.objc] RefreshReceiptDelegate.requestDidFinish: Send new receipt data
[CdvPurchase.AppleAppStore.Bridge] DEBUG: infoPlist: com.my.appid,1.1.60,0,????
[CdvPurchase.AppleAppStore] INFO: receiptsRefreshed
[CdvPurchase.AppleAppStore] DEBUG: receipt updated and ready.
[CdvPurchase.AdapterListener] DEBUG: receiptsUpdated: [{"className":"Receipt","transactions":[{"className":"Transaction","transactionId":"2000000734344505","state":"approved","products":[{"id":"com.my.productid"}],"platform":"ios-appstore"}],"platform":"ios-appstore","nativeData":{"appStoreReceipt":"MII.../4N","bundleIdentifier":"com.my.appid","bundleShortVersion":"1.1.60","bundleNumericVersion":0,"bundleSignature":"????"}},{"className":"Receipt","transactions":[],"platform":"ios-appstore"}]
[CdvPurchase.AdapterListener] DEBUG: receiptsReady: ios-appstore(skipping)
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase] DEBUG: Calling callback: type=approved() name=transactionStateMonitors_callOnChange reason=adapterListener_receiptsUpdated_approved
[CdvPurchase] DEBUG: Calling callback: type=approved() name=#667b6c0a81ad70b56ce13d01a4e6794d reason=adapterListener_receiptsUpdated_approved
[CdvPurchase] INFO: finish(Transaction)
[CdvPurchase.AppleAppStore] INFO: finish(2000000734344505)
[CdvPurchase.AppleAppStore.objc] finishTransaction: Transaction 2000000734344505 finished.
[CdvPurchase.AppleAppStore.objc] transactionFinished: 2000000734344505
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase.AppleAppStore.Bridge] DEBUG: transaction updated:2000000734344505 state:PaymentTransactionStateFinished product:com.my.productid
[CdvPurchase.AppleAppStore] INFO: finish: 2000000734344505 - com.my.productid
[CdvPurchase.AppleAppStore] DEBUG: initializeAppReceipt() => already initialized.
[CdvPurchase.AdapterListener] DEBUG: receiptsUpdated: [{"className":"Receipt","transactions":[{"className":"Transaction","transactionId":"2000000734344505","state":"finished","products":[{"id":"com.my.productid"}],"platform":"ios-appstore"}],"platform":"ios-appstore","nativeData":{"appStoreReceipt":"MII.../4N","bundleIdentifier":"com.my.appid","bundleShortVersion":"1.1.60","bundleNumericVersion":0,"bundleSignature":"????"}}]
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase] DEBUG: Calling callback: type=finished() name=transactionStateMonitors_callOnChange reason=adapterListener_receiptsUpdated_finished
[CdvPurchase.AppleAppStore] DEBUG: receipt updated and ready.
[CdvPurchase.AdapterListener] DEBUG: receiptsUpdated: [{"className":"Receipt","transactions":[{"className":"Transaction","transactionId":"2000000734344505","state":"finished","products":[{"id":"com.my.productid"}],"platform":"ios-appstore"}],"platform":"ios-appstore","nativeData":{"appStoreReceipt":"MII.../4N","bundleIdentifier":"com.my.appid","bundleShortVersion":"1.1.60","bundleNumericVersion":0,"bundleSignature":"????"}},{"className":"Receipt","transactions":[],"platform":"ios-appstore"}]
[CdvPurchase.AdapterListener] DEBUG: receiptsReady: ios-appstore(skipping)
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
[CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#5121f05a236978d1ebad4e26ac18ce54 reason=adapterListener_receiptsUpdated
kreso22 commented 1 month ago

Perhaps this may be related to iOS 18? It seemed to have worked prior to some few weeks ago when I started getting user emails.

Perhaps if I added server-side validation of receipt it would work? If I add server-side validation now, would that ignore previous purchases?

Thank you in advance.

kreso22 commented 1 week ago

I spent many more days trying to figure this out. The plugin simply silently fails, and there is no information on why.

While I can't possibly know if this is true - perhaps it could have to do with using a different IAP adapter for Cordova a while back. Possibly receipts that were created by the previous platform somehow are not recognized when the app initializes today - but are recognized when Restore is initiated.

Could this be the issue?

DesignerApparelSales commented 1 week ago

Perhaps this may be related to iOS 18? It seemed to have worked prior to some few weeks ago when I started getting user emails.

Perhaps if I added server-side validation of receipt it would work? If I add server-side validation now, would that ignore previous purchases?

Thank you in advance.

I have the same problem and my real device is not ios 18. I have test in different devices in the sandbox. all the devices have the same problem. only restore purchase seem to fix it. Did you ever find the cause and solution?

kreso22 commented 1 week ago

Unfortunately I was unable to figure this out so I moved to Revenuecat's plugin. My app works now. Good luck!

DesignerApparelSales commented 1 week ago

Unfortunately I was unable to figure this out so I moved to Revenuecat's plugin. My app works now. Good luck!

Thanks for the reply, I'm using cordova, revenuecat is still supporting cordova? I think i'll jump over there too. i have been messing with this plugin for 3 days with no luck.

kreso22 commented 1 week ago

Yes, you are right. They are gradually dropping support - but we should be good for another 1-2 years. I will reconsider my options when that moment arrives.

DesignerApparelSales commented 1 week ago

I also saw this https://qonversion.io/ while searching for alternative to this plugin. Do you know if qonversion any good?