React-native-iap: IOS check if autorenewal subscription is still active

Created on 27 Sep 2018  ·  61Comments  ·  Source: dooboolab/react-native-iap

Version of react-native-iap

2.2.2

Platforms you faced the error (IOS or Android or both?)

IOS

Expected behavior

Using the RNIAP api we could check if a autorenewal subscription is still active

Actual behavior

On android im doing this to check that:

export const isUserSubscriptionActive = async (subscriptionId) =>{
    // Get all the items that the user has
    const availablePurchases = await getAvailablePurchases();
    if(availablePurchases !== null && availablePurchases.length > 0){
        const subscription = availablePurchases.find((element)=>{
            return subscriptionId === element.productId;
        });
        if(subscription){
             // check for the autoRenewingAndroid flag. If it is false the sub period is over
              return subscription["autoRenewingAndroid"] == true;
            }
        }else{
            return false;
        }
    }
}

On ios there is no flag to check that, and the getAvailablePurchases method returns all the purchases made, even the subscriptions that are not active at the moment.

Is there a way to check this?

Regards,
Marcos

📱 iOS 🚶🏻 stale

Most helpful comment

Here is the function I am using that works on both Android and iOS, properly sorting on iOS to ensure we get the latest receipt data:

import * as RNIap from 'react-native-iap';
import {ITUNES_CONNECT_SHARED_SECRET} from 'react-native-dotenv';

const SUBSCRIPTIONS = {
  // This is an example, we actually have this forked by iOS / Android environments
  ALL: ['monthlySubscriptionId', 'yearlySubscriptionId'],
}

async function isSubscriptionActive() {
  if (Platform.OS === 'ios') {
    const availablePurchases = await RNIap.getAvailablePurchases();
    const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );
    const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;

    const isTestEnvironment = __DEV__;
    const decodedReceipt = await RNIap.validateReceiptIos(
      {
        'receipt-data': latestAvailableReceipt,
        password: ITUNES_CONNECT_SHARED_SECRET,
      },
      isTestEnvironment
    );
    const {latest_receipt_info: latestReceiptInfo} = decodedReceipt;
    const isSubValid = !!latestReceiptInfo.find(receipt => {
      const expirationInMilliseconds = Number(receipt.expires_date_ms);
      const nowInMilliseconds = Date.now();
      return expirationInMilliseconds > nowInMilliseconds;
    });
    return isSubValid;
  }

  if (Platform.OS === 'android') {
    // When an active subscription expires, it does not show up in
    // available purchases anymore, therefore we can use the length
    // of the availablePurchases array to determine whether or not
    // they have an active subscription.
    const availablePurchases = await RNIap.getAvailablePurchases();

    for (let i = 0; i < availablePurchases.length; i++) {
      if (SUBSCRIPTIONS.ALL.includes(availablePurchases[i].productId)) {
        return true;
      }
    }
    return false;
  }
} 

All 61 comments

I'm working on figuring this out as well. I haven't tested it yet, but from what I'm reading I'm gathering that it's possible by creating a "shared secret" in iTunes Connect and passing this to validateReceiptIos with the key 'password'. I believe this will then return a JSON object which will contain a code indicating the validation status of the subscription and the keys latest_receipt, latest_receipt_info, and latest_expired_receipt info, among others, which you can use to determine the subscription status. I am literally just figuring this out so I have yet to test it. It's what I'm putting together from the issues and Apple's docs. If this works, it really should be made very clear in the documentation instead of being buried in the issues.
I believe the following links are relevant:
https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

203 #237

EDIT: I can confirm that the process I mentioned above works. I think we should work to get a full explanation of this in the docs. I would implement something like this on app launch to determine whether a subscription has expired:

RNIap.getPurchaseHistory()
        .then(purchases => {
                RNIap.validateReceiptIos({
                   //Get receipt for the latest purchase
                    'receipt-data': purchases[purchases.length - 1].transactionReceipt,
                    'password': 'whateveryourpasswordis'
                }, __DEV__)
                    .then(receipt => {
                       //latest_receipt_info returns an array of objects each representing a renewal of the most 
                       //recently purchased item. Kinda confusing terminology
                        const renewalHistory = receipt.latest_receipt_info
                       //This returns the expiration date of the latest renewal of the latest purchase
                        const expiration = renewalHistory[renewalHistory.length - 1].expires_date_ms
                       //Boolean for whether it has expired. Can use in your app to enable/disable subscription
                        console.log(expiration > Date.now())
                    })
                    .catch(error => console.log(`Error`))
        })

@kevinEsherick

Brilliant!

Just some things that arent so trivial about your code;

  1. It should be

const expiration = renewalHistory[renewalHistory.length - 1].expires_date_ms

instead of (probably just a typo)

const expiration = latestRenewalReceipt[latestRenewal.length - 1].expires_date_ms

2) For those who dont know what is 'password': 'whateveryourpasswordis':

You can create your shared secret key for your In App paments and use it on your app

Steps are here: https://www.appypie.com/faqs/how-can-i-get-shared-secret-key-for-in-app-purchase

+1 to update the docs, also, maybe @dooboolab you can separate the use cases in IOS and Android seems there are a few things different. I can help you if you want to.

Regards

@kevinEsherick

What happens with autorenewal subscriptions?

Seems that the expires_date_ms is the first expiration date, it is not updated

I have tested this:

  1. Subscribe to a subscription item
  2. Check the expiration date (like 5 minutes after)
  3. Wait 10 minutes
  4. Subscription still active seems is autorenewal subscription
  5. If i check the last renewal receipt expiration is still the same as point 2

Any ideas?

My bad, the expiration date seems to be updated, it just takes a while.

I will leave this comment here in case anyone is in the same scenario.

Regards

  1. It should be

const expiration = renewalHistory[renewalHistory.length - 1].expires_date_ms

instead of (probably just a typo)

const expiration = latestRenewalReceipt[latestRenewal.length - 1].expires_date_ms

Oops yea you are correct, that was a typo. I had changed the name to make its meaning more obvious but forgot to change all occurrences. I've edited it, thanks.

And I haven't had that exact scenario with expiration date but there was some stuff with it not autorenewing at all, though it somehow resolved on its own (which worries me, but I can't reproduce so idk what else to do). The issue you had is a fairly common one. It's expected behavior on Apple's end, so you should leave a buffer period for the subscription to renew before unsubscribing the user. This issue on SO explains in more detail: https://stackoverflow.com/questions/42158460/autorenewable-subscription-iap-renewing-after-expiry-date-in-sandbox

Thanks! Great info, could be the reason

About this issue, i think the @kevinEsherick code should be on the docs 👍 Its a great way to check subs!

Yes. I would appreciate a neat doc in readme if anyone of you request a PR. Thanks.

Hi guys. I've added Q & A section in readme. Hope anyone of you can give us a PR for this issue.

I'll look into making a PR in the next few days :)

I am also looking to use this library specifically for subscriptions. Both iOS and Android. Documenting the proper way to determine if a user has a valid subscription would be greatly appreciated. 😄

@curiousdustin Apple's implementation on this is really terrible if you refer to medium. Since you need to handle this kind of thing in your backend, we couldn't fully support this in our module. However, you can get availablePurchases in android using our method getAvailablePurchases which won't work on ios subscription product.

So are you saying the solution that @marcosmartinez7 and @kevinEsherick are working on in this thread is NOT a solution, and that a backend server other than Apple's is necessary to confirm the status of an iOS subscription?

From reading the Medium article you posted along with the Apple docs, it does seem that using a server is preferred, but it is not impossible to determine subscription status with device only.

Sorry that I've missed some information in my writing. I will manage this now.
Apple suggests to save the receipt in your own server to prevent from middle attack. Therefore you can later check that receipt to find out whether receipt is valid or subscription is still available.

However, it isn't impossible to still try out with react-native-iap since we provide the direct fetch of validating receipt to own Apple server (sandbox & production) which thankfully @marcosmartinez7 and @kevinEsherick worked out.

I think currently this is the solution to work out for checking ios subscription.

Hey guys, sorry it's been more than a few days, I've been caught up in work but I'll still submit that PR to update the README. @curiousdustin the method I wrote about above definitely works and I'll add that to the docs. I'll submit the PR tonight or tomorrow :)

Thanks the the update.

One more thing I noticed from your example.

'receipt-data': purchases[purchases.length - 1].transactionReceipt,
...
const expiration = renewalHistory[renewalHistory.length - 1].expires_date_ms

In my testing, purchases and renewalHistory aren't necessarily in any particular order. Wouldn't we need to sort them or something to verify we are using the one we intend to?

Same here. Purchases and renewalHistory are definitely not ordered, so we do have to sort first. In dev, that ends up being a lot of iterations.

Here is the function I am using that works on both Android and iOS, properly sorting on iOS to ensure we get the latest receipt data:

import * as RNIap from 'react-native-iap';
import {ITUNES_CONNECT_SHARED_SECRET} from 'react-native-dotenv';

const SUBSCRIPTIONS = {
  // This is an example, we actually have this forked by iOS / Android environments
  ALL: ['monthlySubscriptionId', 'yearlySubscriptionId'],
}

async function isSubscriptionActive() {
  if (Platform.OS === 'ios') {
    const availablePurchases = await RNIap.getAvailablePurchases();
    const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );
    const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;

    const isTestEnvironment = __DEV__;
    const decodedReceipt = await RNIap.validateReceiptIos(
      {
        'receipt-data': latestAvailableReceipt,
        password: ITUNES_CONNECT_SHARED_SECRET,
      },
      isTestEnvironment
    );
    const {latest_receipt_info: latestReceiptInfo} = decodedReceipt;
    const isSubValid = !!latestReceiptInfo.find(receipt => {
      const expirationInMilliseconds = Number(receipt.expires_date_ms);
      const nowInMilliseconds = Date.now();
      return expirationInMilliseconds > nowInMilliseconds;
    });
    return isSubValid;
  }

  if (Platform.OS === 'android') {
    // When an active subscription expires, it does not show up in
    // available purchases anymore, therefore we can use the length
    // of the availablePurchases array to determine whether or not
    // they have an active subscription.
    const availablePurchases = await RNIap.getAvailablePurchases();

    for (let i = 0; i < availablePurchases.length; i++) {
      if (SUBSCRIPTIONS.ALL.includes(availablePurchases[i].productId)) {
        return true;
      }
    }
    return false;
  }
} 

@andrewze

On the android scenario, if the subscription is auto-renewable it will be returned on availablePurchases

@curiousdustin @andrewzey You're right, purchases is not ordered. renewalHistory/latest_receipt_info, however, is always ordered for me. I remember realizing that purchases wasn't ordered but thought I had found some reason that this wasn't actually of concern. I'll look into this in a couple hours once I get home. I don't want to submit a PR til we have this figured out.
EDIT: So the reason I don't think you need to sort the purchases is because expires_date_ms returns the same regardless of which purchase you've queried. Is this not the same for you guys? Or is there some bit of information that you need that requires sorting? Let me know your thoughts. I do agree that the docs should still make clear that purchases aren't ordered chronologically, but as far as I can tell sorting isn't needed to get the expiration date.

@kevinEsherick Thanks! In my case, neither the purchases nor renewalHistory were ordered.

@kevinEsherick @andrewzey Can you guys give PR on this solution? We'd like to share with others who are suffering.

Sure I'll prepare it right after we're done with our public app launch =).

I was waiting for more conversation regarding my last comment. I brought up some issues that remain unresolved. Please see above for reference. Once we get that sorted out one of us can make a PR.

@kevinEsherick Ah sorry, I missed that.

expires_date_ms is definitely unique for me on each renewal receipt. Perhaps I've misunderstood? I noticed that the renewals receipt array attached to each decoded receipt is the same regardless of which receipt is selected, so I could see that you may be right in the following (from my example) not being required:

const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );

But I did it anyways in service of trying to be accurate, and compensating for possible discrepancies in the actual API response from what Apple documents. I don't think a sort operation there will be very expensive, given the total number of receipts you're likely to encounter for auto-renewing subscriptions.

Any ETA on the PR? I am considering moving over from this package as it does not handle this case at all.

@schumannd Given that the PR would be only to update the docs, you can move over to react-native-iap now.

I've confirmed that my code snippet does in fact work without issue in production with our recently launched app: https://itunes.apple.com/us/app/rp-diet/id1330041267?ls=1&mt=8

EDIT I'll definitely make the PR (I have it in my "Give back to OSS" list in our Trello), but after the black friday weekend craziness 😄

@schumannd I second what @andrewzey said. Everything a PR would say is contained here in this post. I've been meaning to get around to it but it took us awhile to sort out exactly what needs to be done, and that mixed with travel and hectic startup hours mean I haven't had the time to PR it yet. I still plan to soon, but anyone else can step up in the meantime if they'd like. And @andrewzey congrats man! Looks great, I'll give it a download!

I'm not convinced that this thread accurately documents the correct way to manage auto-renewing subscriptions in iOS but instead describes a "workaround" workflow per the API exposed by this library. However, this is entirely speculation on my part and I am well aware that I could be wrong so I would like feedback on the observations I've outlined below.

I have been researching the correct way to manage auto-renewing subscriptions in React Native for weeks to no avail so I switched gears and started researching how to manage them natively in Objective-C / Swift. This gave me a point of reference to compare the native StoreKit API(s) and documented usage to what has been implemented in this library.

This write-up is the best explanation I have found so far about how subscription purchasing, validation, and restoration should be handled by a native iOS app: https://www.raywenderlich.com/659-in-app-purchases-auto-renewable-subscriptions-tutorial and while all of the APIs are being used in react-native-iap, they are implementing a different workflow.

A key difference I see is the use of appStoreReceiptUrl.

StoreKit provides the appStoreReceiptUrl which, per my understanding, is the correct way to load the current purcahse/receipt information for an iOS app. While I see this being used and referenced in the react-native-iap code, it appears to be in a different context than the workflow documented in the referenced article.

I'm not sure if/how this implementation difference is problematic, but the react-native-iap API seems to be creating an uneccessary dependency on StoreKit's restoreCompletedTransactions which is what is eventually called by getPurchaseHistory.

It seems to me that the react-native-iap API should support the following workflow for iOS:

  • Attempt to load the current app receipt (via appStoreReceiptUrl) at application startup
  • Provide an opportunity to validate the receipt
  • If appStoreReceiptUrl is null, initiate a "restore" on the current device which would then set the value of appStoreReceiptUrl and the validation logic can be retried

Thoughs?

Again, I could be misunderstanding the implementation of this library or the referenced native implementation.

@sellmeadog

I think a key point that is also mentioned in the article you linked is this:

Note: The project app runs SelfieService, which accepts receipts and uploads them directly to Apple’s receipt validation service. This is a “cheat” to keep the tutorial focused on setting up the subscriptions. In a real app, you should use a remote server for this — not the app.

It took me some time figuring the whole auto-renewable subscription thing out, but from what I understand the best way to handle it is using your own backend as the single source of truth:

  • send all transaction data immediately to your backend
  • provide some means of resending in case of transmission troubles (e.g. restoring purchases)
  • periodically (e.g. once a day) verify all stored receipts on your backend
  • app queries backend for subscription state on e.g. return from background.

This to me seemed the only sane way to handle subscriptions. I have also verified this process with other devs using auto-renewable subscriptions commercially for some time.

So my answer to OPs question would be: check subscriptions periodically in the backend, not the frontend

@schumannd

I'm not sure we're understanding or talking about the same issue.

Receipt validation should be done on the server. Per Apple's own documentation:

Do not call the App Store server /verifyReceipt endpoint from your app.

I interpreted the "cheat" in the referenced article as specifically the sample app's direct calls to /verifyReceipt directly from the app for brevity instead of delegating to a backend server.

It seems to me that Apple / the App Store is and should be the single source of truth. Apple is ultimately processing purchases, renewals, cancellations, etc. and generating receipts accordingly and makes that data available via the StoreKit APIs.

react-native-iap handles purchasing and restoring purchases without issue, but I still think there is a gap in accessing the App Store receipt data that is made available locally at appStoreReceiptUrl which is again Apple's documented way of accessing receipt data:

To retrieve the receipt data, use the appStoreReceiptURL method of NSBundle to locate the app’s receipt...

The workflow that I understand from reading through the Apple docs and referencing native Object-C / Swift implementations is as follows:

  • Load products for purchase
  • Purchase a product (consumable, non-consumable, subscription, doesn't matter)
  • A receipt for that purchase is stored locally at appStoreReceiptUrl by StoreKit behind the scenes
  • The app should send receipt data to a backend server (not the App Store /verifyReceipt directly) for validation as needed
  • appStoreReceiptUrl is updated by StoreKit whenever cancellations and/or renewals occur (assuming the proper StoreKit delegates are registered correctly)
  • When appStoreReceiptUrl is null, because user is on a new device, etc., use the restore purchase workflow which will retrieve current receipt data from Apple / AppStore and StoreKit will persist this locally at appStoreReceiptUrl behind the scenes

Again, react-native-iap handles all of this except for loading receipt data from appStoreReceiptUrl. I think a getReceipt method that queried for the local receipt would ease the burden of managing subscriptions and/or any other purchases.

Apple has made this much more difficult to reason about than it should be.

appStoreReceiptUrl is updated by StoreKit whenever cancellations and/or renewals occur (assuming the proper StoreKit delegates are registered correctly)

This describes the problem I have with this approach: You don't get notified of any renewal / cancellation / expiration etc. if the user does not open your app afterwards with a working internet connection.

Other than that, it seems like a good alternative to the approach I described. I wonder which one is used the most for production apps.

@sellmeadog I think that's the same idea I posted here: #356
Validating receipts locally is one method, apple recommends. That's completely independent of any server / network request.

Yep. There are downsides regarding "no network connection - no validation".
But there are use cases for that.
I want a fast validation of the receipt on every app launch and don't want to keep track about in-app purchases on my own via UserDefaults or Keychain. StoreKit already provides a storing mechanism.

How are you supposed to check if the IOS user renewed or cancelled their subscription without prompting them to login to the itunes account via something like getAvailablePurchases?

For Example:
Upon first purchase of subscription, we validate the receipt and save the expiration date to their firebase profile. We use that firebase node to check on start-up if they have an active subscription. The issue comes when that expiration date has passed and the new billing period starts. How can we check to see if they auto-renewed or canceled? As mentioned, we were using getAvailablePurchases, but that prompts the itunes login like a restore button would -- which is terrible UI

I also wonder the same thing.

In our app we don't need this, as we have a server side API that communicates directly with Apple to check the status. After the original purchase, the receipt data is stored on our servers, and then later used to ask for the updated status.

However, I believe this should still be possible on the device side. I am not an expert on this, but the way I understand it, the app will receive an updated transactions/receipts from the OS whenever a renewal occurs. I suspect that this plugin IS handling these receipts on app launch. (I see a bunch of logs when running from Xcode, that appear to be processing multiple transactions) BUT, from what I can tell, there is no way to tap into those events in the JS code.

Could we get some more info on this?

@csumrell @curiousdustin do you guys have this behavior confirmed in a production environment? I'm wondering if it could just be occurring because of sandboxing, with the sandbox account needing to sign in since there's presumably another, non-sandbox account on your device, whereas in production the user will presumably be signed into their normal account and this module might be able to getPurchases without prompting login.

Sorry, I have NOT confirmed that getAvailablePurchases() causes password prompts in production.

I just assumed it would because the docs talk about this method being used for the feature commonly referred to as Restoring Purchases, which in my experience involves authentication, at least on iOS.

@hyochan @JJMoon Could we get more info on this as well? Specifically, is there a way to reliably get available or historic purchases without triggering the OS to attempt authentication? Also please see my question 2 comments above, regarding the ability to react to the purchases that are detected and processed on app launch.

@curiousdustin yea so when I sign out of my normal iTunes account and into the sandbox account it no longer prompts me to login when this method is called. So my guess/hope is that in production, where the users don't have a sandbox account in use, the issue should not be popping up. Beta testing should work the same way as production bc the users don't have sandbox accounts set up on device, it's just a problem of the development devices. So I'm going to test this tomorrow with beta users and let you know what I find.
EDIT: I can confirm that this happens on non-sandbox devices. This is terrible UX and is quite problematic. Anyone, any ideas?

So when you validate on your own server by calling the App Store server /verifyReceipt endpoint from your app, it will always return the latest update to date info for that user with just the original first purchase receipt.

Does anyone know if using the local validate method will also always return the most up to date info? Or is it strictly for that receipt that you locally validate it with?

Also, does anyone have advice for setting up your own server to validate with Apple? I do not have experience with that

@csumrell you can get the receipt for the latest purchase by calling getPurchaseHistory and sorting for it. Comments above detail how to do this. This should have the most up to date info.

And I'm about to explore the option of validating on the server so we can help each other out. Would love to hear how anyone else went about it though.

@kevinEsherick yes but getPurchaseHistory will trigger the Itunes login popup. I am talking about saving the original receipt to local storage when the user first purchases. Then instead of calling getPurchaseHistory to check the next billing, calling validateReceiptIos(originalReceipt) which is locally validated. However, I am unsure if validateReceiptIos returns the up to date transactions like the apple server /verifyReceipt would do

@csumrell Gotcha. Well that's quickly and easily testable. Have you not tried it? If not I will check it out later today.
EDIT: @csumrell validateReceiptIos does in fact track the latest autorenewals on that purchase so this is a valid method. I would say that this is better than the above proposed methods for checking subscriptions because it avoids the iTunes login prompt (unless someone can come up with a fix for that). The drawback is that you have to store the receipts locally. This shouldn't be too much of a hassle. Also you may not want these receipts going to your backend for security reasons or the fact that they're massive (~60k characters), so it would mean that if a subscribed user signs out or starts using a new device then you would need to prompt them to login to get their latest purchase receipt. You can store their subscription status internally so that you only prompt users who were subscribed last you checked.

@hyochan has funded $20.00 to this issue. See it on IssueHunt

@hyochan what specific solution are you funding? We've provided a few here in this thread, and I think my latest comment provides the strongest solution yet with the fewest issues. Are you funding someone to collect these solutions into one concise PR to the README? Or is this there an actual code problem you still want to see resolved?

@kevinEsherick

Are you funding someone to collect these solutions into one concise PR to the README?

Yes. This is absolutely right. Also, I just wanted to test out how issue hunt could work out of the box. My next plan is to fund on making test cases.

@kevinEsherick You are correct, local validation with validateReceiptIos does in fact return the latest info about the subscription using only the original first transaction ID (stored in redux state)

I will be using this method to avoid having to set up my own server, and to avoid the itunes login popup while checking if the subscription is active.

A note for those considering using this validation flow that @csumrell and I have laid out: I've found that in development the iTunes login prompt is still popping up on first launch. This does not happen in production. As long as you're following the steps we've laid out all should be fine. As to why it's doing this, perhaps when iOS sees that IAP is being used/is part of the module map in development it automatically prompts sign in. I'm not quite sure, but it appears to just be a feature of sandbox testing.

Hey there, it looks like there has been no activity on this issue recently. Has the issue been fixed, or does it still require the community's attention? This issue may be closed if no further activity occurs. You may also label this issue as "For Discussion" or "Good first issue" and I will leave it open. Thank you for your contributions.

Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please feel free to create a new issue with up-to-date information.

Perhaps this whole flow needs clarifying in the documentation?

Especially when it comes to the Auto-renewing subscription? It is very unclear to a newcomer whether you are able to check the expires_date_ms for whether the subscription is still active or not?

I suggest the best place for this would be in a more complete working example?

Would people be interested in this? If I get some time, I could work on this?

@alexpchin yes that would definitely save a ton of dev time and your karma will get crystal clean:D

Agree! Would anyone be interested in #856 ?

Hi,

@andrewzey's method has worked great for us until yesterday when we were suddenly no longer to validate any receipts for our subscriptions - we're getting a JSON parse error back from iOS when calling validateReceiptIos(). I take it no one else has ran into this...? The only change that was made since yesterday was the addition of a promo code on ITC which we have since removed to help eliminate variables. Is there any reason why a receipt should fail JSON parsing? It fails for every receipt returned - not just the receipt at index 0 of sortedAvailablePurchases. The code is basically identical to andrewzey's example - I've omitted everything after validateReceiptIos since the error is thrown there.

We're running RN 0.61.4, RNIap: 4.4.2, and iOS 13.3.1

We've tried:

  • Reinstalling app
  • New user account
  • Using a new app secret
  • Testing all receipts on each user - not a single receipt can't be parsed

We're wondering if we weren't using finishTransactionIOS correctly when we made the sandbox purchases?

What's really strange is when we validate the receipt using this online tool, everything looks normal and we can see all of the receipt metadata.

  isSubscriptionActive = async () => {
      const availablePurchases = await RNIap.getAvailablePurchases();
      const sortedAvailablePurchases = availablePurchases.sort(
        (a, b) => b.transactionDate - a.transactionDate
      );
      const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;
      const isTestEnvironment = __DEV__;

      try {
        const decodedReceipt = await RNIap.validateReceiptIos(
          {
            'receipt-data': latestAvailableReceipt,
            password: Config.IN_APP_PURCHASE_SECRET,
          },
          isTestEnvironment
        );
        console.log('response!', decodedReceipt)
      } catch (error) {
        console.warn('Error validating receipt', error) // JSON PARSE ERROR HERE
      }
...

Question regarding the code @andrewzey placed. Wouldn't Date.now() be the current device time and a user could possibly change this and have an endless subscription? 🤔

Also, isn't verifying the receipt from the app itself not discouraged?

@doteric Could you point me to where they mention its discouraged? I know that you can set up server side notifications, but it seems much easier from my perspective to handle this validation on the client instead of the server.
I'm struggling to find the right documentation from apple or other solid react native sources.

@doteric yes Date.now() is the device time so it could be worked around. Yet the chances of a user doing this are minuscule, and even other methods could be worked around for an endless subscription. For example if using a simple server side validation, they could enter airplane mode prior to opening the app to prevent the app from realizing the subscription is expired. Of course there are other protections you could put in place, but my point is that client side using Date.now() is certainly functional. @captaincole I don't have the docs on hand but I can confirm that I too have read that client side validation is discouraged. I read it in Apple docs I believe. That being said, I think client side gets the job done.

Hi,

I was trying one of the approaches discussed in this thread, using validateReceiptIos and the latest_receipt_data (comparing expires_date_ms with actual date). It worked great while developing in Xcode. However, when I tested it in Testflight it didn't work anymore. Its hard to debug so I am not being able to identify the exact problem, it seems it is not getting the receipt data. Did anyone have a similar issue with testflight?

Thanks in advance!

Hi,

I was trying one of the approaches discussed in this thread, using validateReceiptIos and the latest_receipt_data (comparing expires_date_ms with actual date). It worked great while developing in Xcode. However, when I tested it in Testflight it didn't work anymore. Its hard to debug so I am not being able to identify the exact problem, it seems it is not getting the receipt data. Did anyone have a similar issue with testflight?

Thanks in advance!

+1

@asobralr @Somnus007 I may be wrong, but I don't think you can test IAPs with testflight since users can't make purchases through testflight. Apple does not provide great opportunities for testing purchases in production-like environments

@asobralr @Somnus007 I may be wrong, but I don't think you can test IAPs with testflight since users can't make purchases through testflight. Apple does not provide great opportunities for testing purchases in production-like environments

Correct. You can't even simulate failure scenarios in sandbox, which is a shame. 😞

Hi @kevinEsherick , thanks for your reply. You may be wrong. User can make purchase through teslflight. On testflight, it seems that user have to login a sandbox account which has the same email address with its real apple account. Then everything works well. BTW, will getPurchaseHistory trigger the Itunes login popup in production env?

Looks like ios 14 breaks this solution due to the fact that there are issues when you call getAvailablePurchases() before calling requestSubscription().

Was this page helpful?
0 / 5 - 0 ratings