Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

What if requestSubscription succeeds but receipt validation on server fails? #545

Closed
jvandenaardweg opened this issue Jun 21, 2019 · 9 comments
Labels
1️⃣ good first issue Good for newcomers 📱 iOS Related to iOS 🕵️‍♂️ need more investigation Need investigation on current issue Stale

Comments

@jvandenaardweg
Copy link
Contributor

jvandenaardweg commented Jun 21, 2019

Version of react-native-iap

3.0.0

Version of react-native

0.59.9

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

iOS

Expected behavior

Would like the subscription purchase to not follow through when receipt validation fails for whatever reason.

Actual behavior

Subscription purchase succeeds without receipt validation on server

Tested environment (Emulator? Real Device?)

Real Device

Steps to reproduce the behavior

I'm using receipt validation on my server to determine a valid receipt and to store that data on my server for reference if the user is still subscribed. This server validation is recommended by Apple, as this does not allow "man in the middle" attacks.

With the new 3.0.0 release (actually, when I started implementing IAP) i'm missing a way to do this in a transaction-like manner. Because, if my receipt validation server is erroring, down, having maintenance etc... and the user buys a subscription, it will succeed to buy it, but it will fail to validate it. Resulting in a not validated purchased subscription, so i cannot give that user access to subscriber features.

The user will be mad because he is being charged, but not giving the right access.

Above is something that did not happen yet, but theoretically possible.

I think we need some kind of transaction, which is already in the Apple SDK's if i read them correctly:
https://developer.apple.com/documentation/storekit/skpaymentqueue
https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction
https://docs.microsoft.com/nl-nl/xamarin/ios/platform/in-app-purchasing/store-kit-overview-and-retreiving-product-information (nice overview with flowcharts, ignore it's for Xamarin)

But, i don't know that much about the StoreKit API's if this is even possible. But i guess it is possible: https://stackoverflow.com/a/23258538/3194288

The thing I have in mind (proposal):

componentDidMount() {
  this.transactionListenerSubscription = RNIap.transactionListener(({ isFinished, isStopped }) => {
    if (isStopped) return Alert.alert('Failed...')

    return Alert.alert('Success!')
  }
}

handleOnPressUpgrade = () => {
  RNIap.transaction(async (t) => {
    try {
      const { receipt } = await RNIap.requestSubscription('com.app.something.premium')
      await validateReceiptOnServer(receipt)
      t.finish()
    } catch (err) {
      t.stop()
    }
  }
}

Only after t.finish() is called, the purchase should be confirmed.
t.stop() should just rollback/cancel all

Below is how I do it now, which could fail when validateSubscriptionReceipt fails:

componentDidMount() {
    this.purchaseUpdateSubscription = RNIap.purchaseUpdatedListener(async (purchase: RNIap.ProductPurchase) => {
      try {
        await this.props.validateSubscriptionReceipt(subscription.id, purchase.transactionReceipt);

        // The validation result is handled in componentDidUpdate
      } catch (err) {
        const errorMessage = (err && err.message) ? err.message : 'An uknown error happened while upgrading.';
        this.showErrorAlert('Upgrade error', errorMessage);
      } finally {
        this.setState({ isLoadingRestorePurchases: false, isLoadingBuySubscription: false });
      }
    });

    this.purchaseErrorSubscription = RNIap.purchaseErrorListener(async (error: RNIap.PurchaseError) => {
      this.showErrorAlert('Oops!', `An error happened. Please contact our support with this information:\n\n ${JSON.stringify(error)}`);
      this.setState({ isLoadingRestorePurchases: false, isLoadingBuySubscription: false });
    });
  }

handleOnPressUpgrade = async () => {
    return this.setState({ isLoadingBuySubscription: true }, async () => {
      try {
        await RNIap.requestSubscription(SUBSCRIPTION_PRODUCT_ID);
      } catch (err) {
        const errorMessage = (err && err.message) ? err.message : 'An uknown error happened while upgrading.';

        return this.setState({ isLoadingBuySubscription: false }, () =>
          this.showErrorAlert('Upgrade error', errorMessage)
        );
      }
    });
  }
@hyochan
Copy link
Owner

hyochan commented Jun 22, 2019

Very considerable issue!. Yes you are right and this could be improved. Also note in mind that you can still use buyProduct in 3.0.+ which will be removed in later releases like 4.0.0 maybe.

Let's try to investigate this issue.

@vilius
Copy link

vilius commented Jun 28, 2019

Trying to solve the same issue. Am I right to think that following is currently an alternative:

  • User buys. Server fails.
  • User clicks "Restore Purchase" which kicks following flow:
const purchases = await RNIap.getPurchaseHistory();
purchases.forEach((purchase) => {
  validateSubscriptionReceipt(purchase.transactionReceipt);
});

Server in return will process the purchase or ignore it if it was already consumed.

For some reason when dealing with consumable products in iOS Sandbox both methods RNIap.getPurchaseHistory and RNIap.getAvailablePurchases return empty arrays.

I see that there used to be a method called buyProductWithoutFinishTransaction. I assume this could also help solve this problem.

@garfiaslopez
Copy link

I think that this issue is fixed with the implementation of finishTransactionIOS()? Like in the Readme code example? Or this could still be happening?

componentDidMount() {
    this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase: ProductPurchase) => {
      console.log('purchaseUpdatedListener', purchase);
      const receipt = purchase.transactionReceipt;
      if (receipt) {
        yourAPI.deliverOrDownloadFancyInAppPurchase(purchase.transactionReceipt)
        .then((deliveryResult) => {
          if (isSuccess(deliveryResult)) {
            // Tell the store that you have delivered what has been paid for.
            // Failure to do this will result in the purchase being refunded on Android and
            // the purchase event will reappear on every relaunch of the app until you succeed
            // in doing the below. It will also be impossible for the user to purchase consumables
            // again untill you do this.
            if (Platform.OS === 'ios') {
              RNIap.finishTransactionIOS(purchase.transactionId);
            } else if (Platform.OS === 'android') {
              // If consumable (can be purchased again)
              RNIap.consumePurchaseAndroid(purchase.purchaseToken);
              // If not consumable
              RNIap.acknowledgePurchaseAndroid(purchase.purchaseToken);
            }
          } else {
            // Retry / conclude the purchase is fraudulent, etc...
          }
        });
      }
    });

@vilius
Copy link

vilius commented Aug 6, 2019

@garfiaslopez you are correct, properly disclosed and fixed in this PR #581

@matamicen
Copy link

@garfiaslopez Thanks for share, We did the same approach.

Can you help me with the Validation in Android? I couldn't find a nice tutorial to do it, I mean at least I want to do it from POStman in order to check the flow then I can code in any language. I don't know what information to pass to the POST of https://www.googleapis.com/auth/androidpublisher API in order to get the token, it goes on the body? on the header? Do i need to create just a Service Account or and oAuth2 ? Any help will be appreciated.
@hyochan

Thanks guys.
Matt.

@hyochan
Copy link
Owner

hyochan commented Oct 26, 2019

@matamicen Although this blog post is written in Korean, I think it may help you the idea.

@hyochan hyochan added ℹ needs more info Needs more detailed information to move forward 🕵️‍♂️ need more investigation Need investigation on current issue and removed ℹ needs more info Needs more detailed information to move forward 🕵️‍♂️ need investigation labels Feb 7, 2021
@gg-vhong
Copy link

gg-vhong commented Mar 1, 2021

So just some additional info. It doesn't look like consumables are returned on getPurchaseHistory.

getPurchaseHistory calls [[SKPaymentQueue defaultQueue] restoreCompletedTransactions].

Apple's restoreCompletedTransactions documentation (link)

  • This method has no effect in the following situations: You tried to restore items that are not restorable, such as a non-renewing subscription or a consumable product.

Which suggests that (@vilius) looping over getPurchaseHistory (for consumables) returning empty is as intended.

Apple's finishtransaction documentation [(link)] (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction) mentions a few more things:

  • If you validate receipts, validate them before completing the transaction, and take one of the paths described above.
  • Perform all necessary actions to unlock the functionality the user has purchased before finishing the transaction. (which is what was suggested by @garfiaslopez in the readme example).

So it does sound like restoring consumables are kinda left to the individual apps. From as far as 2013, people have used the keychain as a persistent store - which may be more reliable than tracking receipts server side (link).

The most annoying type of API edge case. Low frequency, high customer stress, and a good amount of manual impl cost.

@stale
Copy link

stale bot commented Jun 4, 2021

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.

@stale stale bot added the Stale label Jun 4, 2021
@stale
Copy link

stale bot commented Jul 8, 2021

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.

@stale stale bot closed this as completed Jul 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
1️⃣ good first issue Good for newcomers 📱 iOS Related to iOS 🕵️‍♂️ need more investigation Need investigation on current issue Stale
Projects
None yet
Development

No branches or pull requests

6 participants