j3k0 / cordova-plugin-purchase

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

Is restorePurchases Active in 13x? #1384

Closed aesculus closed 1 year ago

aesculus commented 1 year ago

I have upgraded my app from V9.x to 13.3 and finally gotten everything working but the iOS refresh purchases. I had a custom Restore Purchase routine before that forced a verify on my three subscriptions and then called store.refreh() to complete it. It worked fine. But of course store.refresh() is no longer available and the docs say in this instance to use CdvPurchase.store.restorePurchases().

But this call stops my app from functioning in the routine it is called in. I was expecting it to do something similar to store.update.

Should I just use store.update() and is there any value in calling the verify on each of my products before?

j3k0 commented 1 year ago

Hi. Yes restorePurchases is active. I just tested, works on my setup. This call is specific to iOS, it's what your app should do when the user hits the "Restore Purchases" button that Apple requires you to add to your app in certain circumstances: it'll basically replay all transactions so your app gets a chance to properly handle them if you need to store local data related to a user's purchase. Most generally this only applies to non-consumable, sometimes to subscriptions. It's a "heavy" call, it'll often request the user's AppStore credentials, and might take some time to process.

See the discussion section here for details: https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions

update() on the other hand is a lightweight method that'll re-validate the application receipt (still on iOS), if you have a receipt validation service in place. It will refresh the user's purchases.

On Android, it'll fetch the list of purchases locally (as it's cached by the underlying SDK).

On all platforms, update() will also update the price of purchasable products. This is the method you might want to call if the user enters your Store screen a while after the plugin has been initialized, or if you want to provide a manual option to refresh the page (in case prices have changed or something).

aesculus commented 1 year ago

My use case is as defined by Apple: Either adding a new device or restoring a device that was wiped out.

My app acts just like the statement is not valid as execution does not run in that function call.

Can you verify what the calling procedure would look like in Javascript? Maybe I have something wrong in the calling statement.

j3k0 commented 1 year ago

You should do CdvPurchase.store.restorePurchases() - (store.restorePurchases() is a shortcut if you're storing "store" in the global namespace). Could it be this?

aesculus commented 1 year ago

Well the good news is that I just swapped update() for restorePurchases() in my function and it did not break there. But I cannot test it yet because I need to get over problem #1389

Once that is complete I can test the entire process.

lafbarroso commented 1 year ago

update() on the other hand is a lightweight method that'll re-validate the application receipt (still on iOS), if you have a receipt validation service in place. It will refresh the user's purchases.

@j3k0 This is still true on v13.3.11 for iOS, right? I cannot find a way to revalidate the receipt when I want to (e.g when the logged-in user changes). I have tried with update() and restorePurchases() and neither seems to call my custom validator after a previous successful validation. (I'm using an auto-renewable subscription)

All is working fine on Android with update().

j3k0 commented 1 year ago

@lafbarroso Still true. You could try this to force re-validation of the receipt on iOS:

CdvPurchase.store.localReceipts[0].verify();
hhimanshu commented 1 year ago

@j3k0 , I know you have closed this task, but I have a question. Since the API return Promise<void> what happens if a user taps on this button and

Case 1: They had previously purchased and changed phone Case 2: They had not previously purchased

How can I separate out between 2 situations given this API return method. Currently, I call following method if a user taps on the "Restore purchase" button

image
const restorePurchase = async () => {
        store.restorePurchases().then(() => {
            console.log(`purchases restored`)

            // assume user had purchased previously update db to make user premium
           // but if this assumption is wrong, every free user can become premium user without paying

        }).catch(e => console.error(`failed to restore purchases`))
    }

I am sure I am missing something fundamental here and need your help in understanding. Kindly guide.

j3k0 commented 1 year ago

The promise is resolved once the local receipt has been refreshed. In any case, "update DB" should happen in your store.when().approved(...) handler. This way you will include all cases: restored purchases, pending transactions processed at startup, live purchases, family shared purchases, etc.

This promise is just to show/hide your loading indicator.

hhimanshu commented 1 year ago

Thanks, so you mean, do something like this

store.when()
                .approved((transaction) =>{
 transaction.verify()
 // check transaction.products to ensure product id matches and update DB, make user premium
})

The above code happens when user opens the app. Is that correct?

Also, I am wondering about when user hits the "Restore Purchase" button. There could be 2 users

  1. UserPremium
  2. UserFreemium

How do I ensure that only UserPremium is upgraded, but not UserFreemium? Could you please guide?

hhimanshu commented 1 year ago

I updated my code to following based on your guidance

store.when()
                .approved((transaction) => handleApprovedTransaction(transaction))

where handleApprovedTransaction() is

const handleApprovedTransaction = async (transaction: CdvPurchase.Transaction) => {
        await transaction.verify()
        const alreadyPurchased = transaction.purchaseId !== undefined &&
            transaction.products.filter(p => p.id === productId)[0] !== undefined
        if (alreadyPurchased) {
            console.log(`user has already purchased ${productId}, upgrading to premium`)
            await updateUserStatus()
        }
    }

I believe this handles the recommendation you have. However, my question still remains about when user clicks on "Restore Purchase" button.

I want to ensure that only UserPremium is upgraded and not UserFreemium. Otherwise, every iOS user who taps on this button becomes premium user without paying :-)

denisyarkov commented 1 year ago

Hello.

Can you explain how the 'restorePurchases' method should work on Android when called without any valid 'restorable' purchase? I don't see any event handler called in that case despite native Java code returning an empty purchases array:

sendToListener() -> setPurchases data -> {"purchases":[]}

Using version 13.6.0