j3k0 / cordova-plugin-purchase

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

I can't re subscribe after canceled subscription on iOs. The payment sheet doesn't show again. #1350

Open aktivdigital-frontend opened 1 year ago

aktivdigital-frontend commented 1 year ago

Hello,

I have a problen on iOs.

I can't re-subscribe after canceled subscription. The payment sheet doesn't show again. I have an error in the log:

code: 6777020
message: "Transaction expired: Transaction has expired 2022-12-14 10:18:07 Etc/GMT"

I tried clear the purchase history on sandbox user, but still give same error...

nathanielop commented 1 year ago

I have the same issue; running the plugin, built to my actual device. Subscribed, worked as expected, had to fix some bugs, rebuilt, reran, when resubscribing said that I already owned the subscription, asked to continue anyways or cancel, out of curiosity pressed cancel subscription or whatever the exact wording was, now whenever I press the submit button I get two single log outs from the plugin which seem to indicate everything is going smoothly, indicating that I've initiated the order via window.store.order.

But the process hangs and there is no further output or changes (up to 5 minutes after submitting).

I've tried deleting the test app off of my phone, cleaning build cache, rebuilding. Everything short of logging out of my iCloud and logging back in.

zoltan-adm commented 1 year ago

@j3k0 Can you help us please?

nathanielop commented 1 year ago

The reproduction details are somewhat vague which is, I suspect, an issue preventing any resolution from being presented. I also suspect this issue may be solely due to Apple's testing mode for IAP being bugged. I am still curious whether or not anybody else has faced this in the past and whether there was some mitigation to reset the purchase status to at least allow testing again, at the very least?

silviogutierrez commented 1 year ago

Confirming this happens to me on 13.1.3 , iOS.

With a fresh Sandbox account, from a signed out state, I can successfully sign up and register. If I let the registration lapse, validation properly fails and I see my UI to sign up again. But calling order on the product does not show the action sheet at all or any way to move forward.

What did Work for me is signing out of the sandbox user. Then it does properly show the payment sheet prompted me to sign in again and I can finish re-subscribing. Obviously this isn't desirable.

Unsure if this is just for sandbox or production too.

j3k0 commented 1 year ago

What version of the plugin are you using? Can you copy-paste any logs that would help understand the issue.

EDIT: Sorry I wrongly read 13.1.3 as an iOS version. I'll try to reproduce and still, if you have some logs it would be helpful.

j3k0 commented 1 year ago

I cannot reproduce. Notice that I'm testing with receipt validation enabled (using https://www.iaptic.com), as it's necessary to properly handle subscriptions.

Calling order(), whether being subscribed or not, results in the AppStore popup.

Though digging into this I found out a small issues with cached receipts that stick around when switching AppStore user. It should be fixed in 13.1.4.

j3k0 commented 1 year ago

Can you run your app and get some logs? Mark the spot you click "Buy". Set CdvPurchase.store.verbosity = CdvPurchase.LogLevel.DEBUG for max verbosity.

silviogutierrez commented 1 year ago

@j3k0 : updated to 13.1.4, was on 13.1.3 before. Raw log output, hopefully this is helpful (with DEBUG verbosity):

This happens when I press "order" after an expired subscription. As mentioned, the payment sheet never shows up.

[log] - [CdvPurchase] INFO: order(com.joyapp.ios.iap.basic)
["options": [com.joyapp.ios.iap.basic, 1, iap7@joyapp.com, {
}]]
⚡️  [log] - [CdvPurchase.AppleAppStore] INFO: order
2022-12-21 11:33:29.937530-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] purchase: About to do IAP
2022-12-21 11:33:29.937660-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] purchase applicationUsername (iap7@joyapp.com).
2022-12-21 11:33:29.937994-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] paymentQueue:updatedTransactions: com.joyapp.ios.iap.basic
2022-12-21 11:33:29.938069-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] paymentQueue:updatedTransactions: State: PaymentTransactionStatePurchased
2022-12-21 11:33:29.938121-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] processTransactionUpdate:withArgs: transactionIdentifier=2000000232848261
⚡️  [log] - [CdvPurchase.AppleAppStore.Bridge] DEBUG: product com.joyapp.ios.iap.basic has a transaction in progress: 2000000232848261
⚡️  [log] - [CdvPurchase.AppleAppStore] INFO: purchase: id:2000000232848261 product:com.joyapp.ios.iap.basic originalTransaction:2000000231843667 - date:1671576619000.000000 - discount:
⚡️  [log] - [CdvPurchase] INFO: verify(Transaction)
⚡️  [log] - [CdvPurchase.Validator] DEBUG: Schedule validation: {"className":"Transaction","transactionId":"appstore.application","state":"approved","products":[{"id":"com.joyapp.ios"}],"platform":"ios-appstore"}
⚡️  [log] - [CdvPurchase] INFO: verify(Transaction)
⚡️  [log] - [CdvPurchase.Validator] DEBUG: Schedule validation: {"className":"Transaction","transactionId":"2000000232865500","state":"approved","products":[{"id":"com.joyapp.ios.iap.basic","offerId":""}],"platform":"ios-appstore","originalTransactionId":"2000000231843667","purchaseDate":"2022-12-20T23:35:19.000Z"}
⚡️  [log] - [CdvPurchase] INFO: verify(Transaction)
⚡️  [log] - [CdvPurchase.Validator] DEBUG: Schedule validation: {"className":"Transaction","transactionId":"2000000232848261","state":"approved","products":[{"id":"com.joyapp.ios.iap.basic","offerId":""}],"platform":"ios-appstore","originalTransactionId":"2000000231843667","purchaseDate":"2022-12-20T22:50:19.000Z"}
⚡️  [log] - [CdvPurchase.AppleAppStore] INFO: Cannot prepare the receipt validation body, because appStoreReceipt is missing. Refreshing...
To Native Cordova ->  InAppPurchase appStoreRefreshReceipt InAppPurchase145171822 ⚡️  [log] - [CdvPurchase.AppleAppStore.Bridge] DEBUG: refreshing appStoreReceipt
["options": []]
2022-12-21 11:33:30.144976-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Request to refresh app receipt
2022-12-21 11:33:30.145215-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Starting receipt refresh request...
2022-12-21 11:33:30.148605-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] appStoreRefreshReceipt: Receipt refresh request started
2022-12-21 11:33:30.176340-0500 App[76297:4358386] <SKReceiptRefreshRequest: 0x282cea900>: Finished refreshing receipt with error: Error Domain=ASDErrorDomain Code=603 "Request throttled" UserInfo={NSLocalizedFailureReason=Unified receipt is valid and current, NSLocalizedDescription=Request throttled, AMSServerErrorCode=0}
2022-12-21 11:33:30.176442-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] RefreshReceiptDelegate.requestDidFinish: Got refreshed receipt
2022-12-21 11:33:30.179439-0500 App[76297:4358386] [CdvPurchase.AppleAppStore.objc] RefreshReceiptDelegate.requestDidFinish: Send new receipt data
⚡️  [log] - [CdvPurchase.AppleAppStore.Bridge] DEBUG: infoPlist: com.joyapp.ios,1.0,16809984,null
⚡️  [log] - [CdvPurchase.AppleAppStore] INFO: receiptsRefreshed
⚡️  [log] - [CdvPurchase.AppleAppStore] INFO: Receipt refreshed.
⚡️  To Native ->  CapacitorHttp request 87359891
2022-12-21 11:33:31.149749-0500 App[76297:4358416] [NSURLSession sharedSession] may not be invalidated
⚡️  TO JS {"data":"The data couldn’t be read because it isn’t in the correct format.","status":200,"headers":{"Server":"cloudflare","Date":"Wed, 21 Dec 2022 16:33:31 GMT","Content-Type":"application\/json","Vary":"Cookie, Origin","x-api-version":"localhost","report-
2022-12-21 11:33:35.868921-0500 App[76297:4358686] [connection] nw_connection_add_timestamp_locked_on_nw_queue [C1] Hit maximum timestamp count, will start dropping events
rodrigoreal commented 1 year ago

@j3k0 Im having the same problem, I can't re subscribe after canceled subscription on iOs. The payment sheet doesn't show up, but if i go to the app store connect and clear this user old transactions, the pop up show for a fresh subscription.

⚡️  [log] - [store.js] DEBUG: state: premiumMonthly -> requested
2022-12-24 13:39:48.712982-0300 App[41966:6894383] InAppPurchase[objc]: purchase: About to do IAP
2022-12-24 13:39:48.714114-0300 App[41966:6894383] InAppPurchase[objc]: paymentQueue:updatedTransactions: premiumMonthly
2022-12-24 13:39:48.714190-0300 App[41966:6894383] InAppPurchase[objc]: paymentQueue:updatedTransactions: State: PaymentTransactionStatePurchased
2022-12-24 13:39:48.714221-0300 App[41966:6894383] InAppPurchase[objc]: processTransactionUpdate:withArgs: transactionIdentifier=2000000228271625
⚡️  [log] - InAppPurchase[js]: product premiumMonthly has a transaction in progress: 2000000228271625
To Native Cordova ->  InAppPurchase appStoreReceipt InAppPurchase1441870700 ["options": []]
2022-12-24 13:39:48.723763-0300 App[41966:6894383] InAppPurchase[objc]: appStoreReceipt:
⚡️  [log] - InAppPurchase[js]: loading appStoreReceipt
2022-12-24 13:39:48.766902-0300 App[41966:6894852] 8.9.1 - [Firebase/Analytics][I-ACS023141] Purchase is a duplicate and will not be reported. Product ID: premiumMonthly

I have 4 plans(2 monthly and 2 yearly) what i discovered is that the previous purchased plan that expired now have a canPurchase: false with a transaction attached to it, and the other 3 plans have a canPurchase: true without a transaction, if i try to re-subscribe using a different plan the pop-up show up, but if i try to re-subscribe using the same plan the user previous purchased the pop-up don't show up.

I think it has to do something with cache like you said in a comment here before, im using the 13.1.5 version.

cozyop commented 1 year ago

check out the ts class for internal code because that is not implemented, only verbosely

Maartenvanzomeren commented 12 months ago

Hi,

I have the same issue on an Apple iPhone with PLUGIN_VERSION 13.4.

After doing the following steps:

  1. With a sandbox user buy a monthly subscription. Make sure it is processed correctly. With finished and so on.
  2. Let it expire. This will take a couple of payments.
  3. Try to buy again. It fails…

See the following screenshot for the logging. Screenshot 2023-05-24 at 16 22 42

In the logging I can see that the app store receipt is cached. Can we clear that cache? Can you clear the cache?

The validation on the server tells me the receipt is expired… Is there a callback result I can do such that the plugin knows that receipt is expired and needs to be removed from the cache?

I'd love to hear from you.

Kind regards!

awltr commented 6 months ago

Same issue as @Maartenvanzomeren

Using latest version 13.8.6. When sandbox subscription expires and I order again, it says that the receipt is cached. But this receipt is expired, which of course does not produce the desired result.

Is there a solution for that?

edit: The problem does not only occur in the sandbox. Most likely receipt.finish() must also be called on unverified if the subscription has expired.

bboldi commented 6 months ago

I am experiencing the same problem with the latest version (v13.8.6). I have subscribed to the product in the sandbox environment, but when the subscription expires or I cancel it, the re-subscribe dialog does not appear ( it only triggers approve hook ). Additionally, when I decode the receipt on the server side and validate it according to Apple's guidelines, it shows that the subscription has expired. If I go to the subscriptions section in the sandbox account (in the Phone App Store, under the sandbox account, go to Manage -> Subscriptions), I can see that the subscription is expired. I am unsure if this will cause any problems in production and therefore hesitate to go live with it.

image

silviogutierrez commented 6 months ago

This is happening with the live environment as well, so it's not limited to the development mode.

One workaround a user of mine suggested that does work: they can go to that same subscriptions page that shows expired, and renew from there.

Not great, but it does work. I may add a prompt to detect if it's a previous subscriber and suggest the workaround.

awltr commented 6 months ago

As said in the edit of my post you have to finish unverified receipts. I can provide an example later if needed.

bboldi commented 6 months ago

This is happening with the live environment as well, so it's not limited to the development mode.

One workaround a user of mine suggested that does work: they can go to that same subscriptions page that shows expired, and renew from there.

Not great, but it does work. I may add a prompt to detect if it's a previous subscriber and suggest the workaround.

Please provide an example if it's not too big of a problem. Thanks in advance!

awltr commented 6 months ago

Should be something like this:

CdvPurchase.store.when().unverified(res => {
  const code = res.payload?.data?.code
  if (code === CdvPurchase.ErrorCode.PAYMENT_EXPIRED) {
    res.receipt.finish()
  }
  // handle app state
})

If you use custom validation, your validation function must at least return the status for expired correctly.

edit: I'm not sure if this is the best practice. At least for me it worked. What confuses me is that the error code VALIDATOR_SUBSCRIPTION_EXPIRED is deperecated with the comment Validator should now return the transaction in the collection as expired.

bboldi commented 6 months ago

Thanks for this, I'll test it as soon as I can. Can you drop a simple example for validator function that returns the status correctly? I'm not sure I understand ( sorry )

If you use custom validation, your validation function must at least return the status for expired correctly.

awltr commented 6 months ago

Can you drop a simple example for validator function that returns the status correctly?

Validation of subscriptions must be implemented in the backend. The validation function just calls the backend endpoint and if necessary maps the response coming from your backend to the error codes of the plugin. As far as I have understood and implemented it now, the only decisive point is to return a status for expired subscriptions - of course, beside the successful validation or any other error. That's all I can say about it, this plugin of course doesn't reveal much about the custom validation because the business case is obviously to sell the backend service called iaptic for it.

bboldi commented 6 months ago

Thanks!

j3k0 commented 6 months ago

Notice that the API documentation shows in details the input and output of the validator function: cf https://github.com/j3k0/cordova-plugin-purchase/blob/master/api/interfaces/CdvPurchase.Validator.Function.md

bboldi commented 6 months ago

Thanks, documentation is neat, and also the code is well written. It's probably me, but my usecase is a bit out of the logic of the plugin... I need to validate the receipts myself - or to be precise it's validated on our backend - ( would be much easier to just use iaptic, but unfortunately I cannot do that due to our implementation and existing payment microservice ) .I'm trying for weeks to get this working, and at this point and when I solve one problem, I bump into another ... I registered with iaptic, just to see what data is sent on successful and unsuccesful cases, but the data that our server returns does not contain the data required to construct the callback object ( CdvPurchase.Validator.Response.Payload ) ... I'm not even sure if everything is needed... so a few questions:

for the implementation:

Sorry for the long comment / many questions ... the more I'm trying to implement this, the more question I have :) !

Thanks in advance, every bit of information helps ...

tolutronics commented 6 months ago

I am having this issue also, did anyone managed to solve this or a workaround?

awltr commented 6 months ago

@bboldi

it would look like this: user buys subscription, I let the server know and it gives user premium on our serice

How do you want to prevent, that the user "let the server know" that he/she buyed the subscription but actually didn't? The only source you can trust is the issuer (apple/google) and thats why you need backend validation.

what's the minimum data that required for callback in order to successfully handle receipt validation

My backend validation basically just returns valid true/false and a reason if false. On clientside on successful validation I return ok=true and data with id and transaction (which I just passthrough from the product of the validation function parameter). On error I return ok=false and data with at least the code for expired subscription and a message if exists. Works for me but can not assure that it fits for every usecase.

@tolutronics As already said, you need to finish the receipt on unverified as well.

tolutronics commented 6 months ago

@awltr seems unverified is not triggered in my case, although i added the finish receipt on unverified, but still same issue.

` private setListeners() { CdvPurchase.store .when() .approved(async (transaction) => { transaction.verify(); }) .verified(async (receipt: any) => { receipt.finish() }) .pending(async (product: any) => { console.log("pending=====>", product);
}) .unverified(async (res: any) => {

            const code = res.payload?.data?.code;
            if (code === CdvPurchase.ErrorCode.PAYMENT_EXPIRED) {
                res.receipt.finish();
            }
        })
        .receiptsReady(async (receipt: any) => {

        })

        .finished(async (transaction: CdvPurchase.Transaction) => {

        })
        .receiptUpdated((r: CdvPurchase.Receipt) => {

        })

}

`

tolutronics commented 6 months ago

this is the DEBUG result , the purchase that does not trigger the apple subscription modal

[Log] [CdvPurchase] INFO: order(Owner_Pro_id) (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] INFO: order (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore.Bridge] DEBUG: Purchase enqueued Owner_Pro_id (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] INFO: purchaseEnqueued: Owner_Pro_id - 1 (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] INFO: order.success (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore.Bridge] DEBUG: transaction updated:2000000462484152 state:PaymentTransactionStatePurchased product:Owner_Pro_id (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] INFO: purchase: id:2000000462484152 product:Owner_Pro_id originalTransaction:2000000462457364 - date:1700510311000.000000 - discount: (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] DEBUG: initializeAppReceipt() => already initialized. (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] INFO: order.paymentMonitor => purchased (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#78ec86bb966353bff730b1b8ce6e4d69 (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AppleAppStore] DEBUG: receipt updated and ready. (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase.AdapterListener] DEBUG: receiptsReady: ios-appstore(skipping) (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase] DEBUG: Calling callback: type=receiptUpdated() name=#78ec86bb966353bff730b1b8ce6e4d69 (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase] DEBUG: Calling callback: type=approved() name=#35c667b44be70a31a949e94da1391546 (main.79a5051ff4b315b3.js, line 1) [Log] [CdvPurchase] DEBUG: Calling callback: type=approved() name=#e0a942d9411d7b190bfb85cf660b93e3 (main.79a5051ff4b315b3.js, line 1)