question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

What is a good way of restoring and auto-restoring purchases?

See original GitHub issue

Can someone chip in here and tell us what is the proper way to handle restore purchases flows?

[This code is not final!]

const setup = async () => {
    try {
      const status = await initConnection();
      console.log(`Payment Service set up. Status: ${status}`);
    } catch (e) {
      console.log('Payment Service is disabled');
    }

    await automaticallyRestorePurchases();

    purchaseUpdateSubscription = purchaseUpdatedListener(treatIAPPurchaseSuccess);

    purchaseErrorSubscription = purchaseErrorListener(treatIAPPurchaseFailure);

    console.log('Payment Service setup succeeded');
  };

const destroy = async () => {
    try {
      if (purchaseUpdateSubscription) {
        purchaseUpdateSubscription.remove();
        purchaseUpdateSubscription = undefined;
      }
      if (purchaseErrorSubscription) {
        purchaseErrorSubscription.remove();
        purchaseErrorSubscription = undefined;
      }
      await endConnection();
      console.log('Payment Service destroyed');
    } catch {
      /* Do nothing */
    }
  };

  useEffect(() => {
    setup();
    return destroy;
  }, []);

 // ...

 // Called manually
 const restorePurchases = async () => {
    console.log('[RESTORE] restorePurchases called');
    if (!currentUser) {
      return;
    }
    startFlow();
    const purchases = await getPurchaseHistory();
    console.log(`[RESTORE] getPurchaseHistory for user. Results: ${purchases?.length}`);
    for (const purchase of purchases) {
      console.log(purchase);
      await treatIAPPurchaseSuccess(purchase);
    }
    justFinishFlow();
  };

  // Called after startConnection, automatically when app starts and is logged in or has just logged in.
  const automaticallyRestorePurchases = async () => {
    console.log('[AUTO-RESTORE] restorePurchases called');
    if (Platform.OS === 'ios') {
      // Skip on iOS because of user and password are requested
      return;
    } else {
      await restorePurchases();
    }
  };

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Reactions:2
  • Comments:7

github_iconTop GitHub Comments

1reaction
tonycococommented, Dec 29, 2021

Here’s a pretty complete example:

const SUBSCRIPTION_SKUS = Platform.select({
  ios: [
    "com.example.subscription1",
    "com.example.subscription2",
    "com.example.subscription3",
  ],
  android: [
    "com.example.subscription1",
  ],
  default: [],
});

export default function () {
  const {
    connected,
    finishTransaction,
    currentPurchase,
    currentPurchaseError,
    requestSubscription,
    getSubscriptions,
    subscriptions,
    getPurchaseHistories,
    purchaseHistories,
  } = useIAP();
  const [isLoading, setIsLoading] = React.useState(true);
  const [isUpdating, setIsUpdating] = React.useState(false);

  React.useEffect(() => {
    const bootstrap = async () => {
      if (connected) {
        await getSubscriptions(SUBSCRIPTION_SKUS);
        setIsLoading(false);
      }
    };

    bootstrap();
  }, [connected, getSubscriptions]);

  React.useEffect(() => {
    const checkCurrentPurchaseError = async (purchaseError?: PurchaseError) => {
      if (purchaseError) {
        try {
          if (
            purchaseError.code === IAPErrorCode.E_ALREADY_OWNED
          ) {
            // Do something to handle an existing subscriber.
            Alert.alert("Successfully restored your subscription.");
          }
        } catch (error) {
          console.warn(error);
        }
      }
    };

    checkCurrentPurchaseError(currentPurchaseError);
  }, [currentPurchaseError]);

  React.useEffect(() => {
    const checkCurrentPurchase = async (purchase?: Purchase) => {
      if (purchase) {
        const { transactionReceipt, productId } = purchase;

        if (transactionReceipt)
          try {
            // Check the receipt on your backend server.

            const response = await finishTransaction(purchase);
            
            // Handle a new subscriber.
          } catch (error) {
            console.warn(error);
          }
      }
    };

    checkCurrentPurchase(currentPurchase);
  }, [currentPurchase, finishTransaction]);

  if (isLoading) {
    return (
      <ActivityIndicator />
    );
  }

  return (
    <>
      {isUpdating && (
        <ActivityIndicator />
      )}

      <ScrollView>
        {subscriptions.map((item) => (
          <Button
            onPress={async () => {
              try {
                setIsUpdating(true);
                await requestSubscription(item.productId);
              } catch (error) {
                console.warn(error);
              } finally {
                setIsUpdating(false);
              }
            }}
            text="Subscribe"
          />
        ))}

        <Button
          text="Terms and Conditions"
          onPress={() => {
            WebBrowser.openBrowserAsync(TERMS_AND_CONDITIONS_URL);
          }}
        />

        <Button
          text="Privacy Policy"
          onPress={() => {
            WebBrowser.openBrowserAsync(PRIVACY_POLICY_URL);
          }}
        />

        <Button
          text="Restore"
          onPress={async () => {
            try {
              setIsUpdating(true);
              await getPurchaseHistories();

              if (purchaseHistories && purchaseHistories.length > 0) {
                // Restore the purchase.
                Alert.alert("Successfully restored your subscription.");
              } else {
                Alert.alert("No subscription available to restore.");
              }
            } catch (error) {
              console.warn(error);
            } finally {
              setIsUpdating(false);
            }
          }}
        />
      </ScrollView>
    </>
  );
}
1reaction
sbrighiucommented, Dec 1, 2020

The most important case nowadays is auto-renewable subscriptions.

For my case, I had 3 subscriptions for the same service (with less or more features), added in a single group in App Store Connect.

The only way I could find to make all of it work in react native is:

  1. Always call getPurchaseHistory() and redeem before purchasing. This is done to be sure that the backend is synchronized with Apple and Google.
  2. Automatically restore purchases also calls getPurchaseHistory(), when the app starts, goes into foreground, when the user logs in. THIS IS ONLY POSSIBLE ON ANDROID, since iOS requests user and password whenever you are trying to do anything else.
  3. On backend, I made an auto-refresh current subscription action when I log in, resume session or specifically call fetch current user. This will use currently stored data on the user to validate with Google and Apple.
  4. On backend, implement App Store Notifications - Server-to-server notifications mechanism to also auto-refresh the current subscription when necessary.
  5. Create a way to know and allow the user to migrate from one platform to another. This can be omitted, but for my ease of mind i just implemented platform switching with 2 chained confirmation alerts, informing the user if he does this and he does not cancel the other platform’s subscription, he may be double charged.
  6. [for multiple subscriptions in the same subscription group] While restoring purchase make sure you sort the subscription details ascending according to the level of the subscription in the subscription group. This will ensure that the biggest feature subscription, if existing, will be redeemed last. That works if you redeem them in that order 😃
  7. Restoring before purchase came as a necessity when working with more than one subscription on android. Since you have to provide the ‘currently active subscription product id on Play Store Servers’ to be able to upgrade to another subscription, basically maintaining just 1 subscription active out of the 2, I had to be 100% sure that it is active, because if you use an inactive/expired product id, you will see a nice generic Google Error and your user will not be able to give you his money… This is something that Google needs to fix somehow… Seems the community is pretty pissed about it.

There is no other method other than getPurchaseHistory() that offers all the up to date transactions existing or old or made on other devices. I consider the other fetch purchases and consumable methods really really really situational and for app only validation (no backend validation) apps.

Ending on an actionable note… I would prefer if the demo app had a working restore purchases flow, but also a recommended way of automatically restoring purchases.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Restoring Purchased Products - Apple Developer
Your app starts restoring completed transactions by calling the restoreCompletedTransactions() method of SKPaymentQueue . This call sends a request to the App ...
Read more >
What does restore purchase mean? - Qonversion
When users purchase non-consumables, auto-renewable subscriptions, or non-renewing ... This method is Apple's recommended approach to restoring purchases.
Read more >
ios - How to restore the correct transaction when using Auto ...
My solution: retrieve the receipt and validate it against your productIdentifiers. Using SKPaymentQueue.defaultQueue().
Read more >
Restoring In-App Purchases - RevenueCat
Restoring purchases is a mechanism by which your user can restore their in-app purchases, reactivating any content that had previously been purchased from ......
Read more >
iOS Restore Purchases Does Not work Correctly - Unity Forum
... I CAN get the purchases to restore, but it's spotty at best, and not a solution I can give to my users....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found