chirag04 / react-native-in-app-utils

A react-native wrapper for handling in-app payments
MIT License
890 stars 185 forks source link

Handling auto renewing subscriptions #80

Closed the-mace closed 7 years ago

the-mace commented 7 years ago

I'm able to purchase a subscription (marked as auto renewable in iTunes), get receipt data, and send to the server for validation. The server is able to validate the receipt fine and sees the original subscription, when it's valid, and when it expires.

Monthly auto renewable subscriptions are supposed to auto renew 5-6 times in Apple's sandbox but I always see the subscription expire after the first period (5 mins on sandbox for monthly renewable).

Here are Apple's notes around this area: https://developer.apple.com/library/content/technotes/tn2387/_index.html

I see react-native-in-app-utils handling the callbacks, but it's not clear to me how the subscription renewal gets recognized and processed by the app/server.

Will react-native-in-app-utils handle this or is there some way of handling these that someone has figured out?

temitope commented 7 years ago

At the end of the documentation there is a recommended module separate from this repo that is recommended for handling the checking of subscription receipts. There is also a discussion about it in some previously closed issues.

the-mace commented 7 years ago

After a bunch of reading and trial and error I found out that my main issue was that Apple does auto-renew subscriptions in the sandbox. They'll do that for one subscription per day. They will do 5-6 renewals but after that will not autorenew a subscription again for that same account on that same day. That can really throw you off.

I couldn't upgrade to RN 0.40.0 to get the latest version of react-native-in-app-utils that returns a receipt on purchase, so I just call receiptData after the subscription and pass that receipt back to the server. From there verification works for the initial subscirption and all renewals without needing new receipt info.

My backend is Rails and I used the candy_check Gem to verify the receipts.

Thanks again for the support!

booboothefool commented 7 years ago

Hi @the-mace, I am trying to implement the same thing right now, and was wondering if you could elaborate on how

From there verification works for all renewals

I am not sure what I should be doing with the receipt after verifying the initial subscription, basically. When/how do I recheck the receipt?

the-mace commented 7 years ago

Once you get a subscription you need to save the receipt data. That receipt should be checked periodically and handled for subscription expiration etc. Those checks are best done on a backend server (Rails, Django etc). The checks are different depending on whether the receipt is from iOS or Android so send the platform along with your receipt data. You'll need to cache the receipt checks for a period as they're slow. In Rails, I used the candy-check GEM to provide some assistance with the receipt verification. Note that the whole certificate thing for the various stores adds another level of fun.

booboothefool commented 7 years ago

@the-mace My apologies, but I need it dumbed down just a little more. 😝

So after purchasing a subscription I get a receipt like this: MIIC/wYJ...

If I verify that receipt I get this: screen shot 2017-04-18 at 11 44 28 am

I still don't get what I need to be doing with all of this. Can we break it down?

Once you get a subscription you need to save the receipt data.

Save data? Do you mean I just save the actual receipt e.g. MIIc/wYJ..., or I save the stuff after verification e.g. expires_date? I assume you mean the former. Secondly, where do I save it? I have Node.js backend with user accounts, so like this?

// user object
{
  user_id: 'someuserid',
  app_metadata: {
    receipt: 'MIIC/wYJ...
  },
}

That receipt should be checked periodically and handled for subscription expiration etc.

This is the part that is least clear to me. When/how exactly do I do this? The documentation suggests something like a cron job. Does that mean

every day,
for every user in my app,
go through their `app_metadata`,
look for a receipt,
verify the receipt if there is one,
and apply the effects of the subscription based on the verification result

???

Doesn't that seem like a lot? I am sure I am misunderstanding.

the-mace commented 7 years ago

Save the actual receipt (MII...). You will need to pass that back to your receipt verification process in the backend. We keep the user, the platform, the date and that that receipt information (latest_receipt).

What we do is let the app make the calls to the server to check subscription status. If we havent refreshed it in a while we update right then and return current status and cache the result for the next period. That way we're only updating for active people (with subscriptions etc). If expired we don't check again. The whole verification thing was 80 lines of code in RoR.

booboothefool commented 7 years ago

@the-mace I really appreciate the help.

The way you describe it makes a lot more sense to me than cron job because "only updating for active people with subscriptions".

How does this thinking look?

Step 1: User purchase the sub -> save the receipt along with user, platform, date (of what? transaction? request? now?), latest_receipt

// user object saved on the backend
{
  user_id: 'some_user_id',
  app_metadata: {
    receipt: {
      id: 'MIIc/wYJ...',
      platform: 'ios',
      last_checked_date: 149250847900,
      latest_receipt: {...},
    },
  },
}

Step 2: User opens the app

checkUserSubscription() {
  currentReceipt = getReceipt();  // app_metadata.receipt from the backend
  if (currentReceipt has not yet expired) {  // currentReceipt.latest_receipt.expired_date_ms < today
    if (currentReceipt.last_checked_date is older than 1 day) {
      newReceiptData = reverifyReceipt();
      applySubscriptionEffects(newReceiptData.latest_receipt);
    } else {
      applySubscriptionEffects(currentReceipt.latest_receipt);
    }
  } else {  // it is expired
    removeSubscriptionEffects();
  }
}
the-mace commented 7 years ago

We just keep the date for a record. iOS and Android are totally different here.

Keep in mind Apple's notes that they will use a sandbox token on a production build so you need to handle that (error code 21007).

iOS looks like:

v = verifier.verify_subscription(receipt_data, secret)
if v.try(:code)
  if v.code == 21007
    # Someone is using a sandbox ID on production (apple verification), try to verify again with sandbox
    config = CandyCheck::AppStore::Config.new( environment: :sandbox)
    verifier = CandyCheck::AppStore::Verifier.new(config)
    v = verifier.verify_subscription(receipt_data, secret)
    if v.try(:code)
      logger.warn "[SUBSCRIPTION] Subscription verificaton failure with sandbox: #{v.code}: #{v.message}"
      return 'unknown'
    end
  else
    logger.warn "[SUBSCRIPTION] Subscription verificaton failure: #{v.code}: #{v.message}"
    return 'unknown'
  end
end
r = v.receipts.last
if r.expires_date < Time.now.utc
  'expired'
else
  'valid'
end

end

hugoh59 commented 6 years ago

Seems like CandyCheck doesn't support Rail > 5.0