There are four types of in-app purchases provided by apple, as described here. This article describes how to verify Auto‑Renewable Subscriptions on the server side and implement S2S notifications to stay informed of our app users’ subscription statuses using Ruby on Rails.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
There are a few gems like candy_check and venice, but here we’re doing IAP manually (using Apple’s IAP verification URL).
Here, I’m assuming that you’ve already set up your application on the App Store. Generate app shared secret ad given in the link, which will be needed later. The whole process is divided into two parts.
Validation can be performed in the following steps :
The receipt validation URL is different for the sandbox and production environment, as described below:
For details, look at Apple’s official documentation.
If your app is live on the app store already, then you might need to verify with the production URL (https://buy.itunes.apple.com/verifyReceipt), otherwise, it can be verified with the sandbox URL (https://sandbox.itunes.apple.com/verifyReceipt), but here we’ll test both the environments sequentially, in case we’re not sure about the environment in which our app live.
Know more here about the testing environment provided by Apple.
For receipt validation, create a class apple_receipt_verifier.rb.
i. Create verify_receipt method for validation as below,
def verify(receipt)
url = "https://buy.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
if response["status"] == 21007
url = "https://sandbox.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
end
return response
end
where,
ii. First it will verify the requested receipt verification in a production environment, if it returns status code 21007, it’ll verify with the sandbox environment. For status code return by Apple receipt validation visit here.
def verify(receipt)
url = "https://buy.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
if response["status"] == 21007
url = "https://sandbox.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
end
return response
end
Full code for apple_receipt_verifier.rb :
require 'net/http'
class AppleReceiptVerifier
def verify(receipt)
url = "https://buy.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
if response["status"] == 21007
url = "https://sandbox.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
end
return response
end
def validate_receipt(url, receipt)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
if uri.scheme == 'https'
request = Net::HTTP::Post.new(uri.request_uri)
request["Accept"] = "application/json"
request.content_type = "application/json"
request.body = {
"receipt-data" => receipt,
"password" => ENV.fetch('APP_STORE_SHARED_SECRET') {
'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
}
}.to_json
response = http.request(request).body
return JSON.parse(response)
end
end
iii. Include the above-created apple_receipt_verifier into the verification code that will decide whether the given receipt is valid or not and accordingly update the user with the latest subscription information.
You can check the response object from receipt validation, it consists of a lot of purchase history, among which we need to select the latest receipt info for retrieving our newest subscription expiry date.
require 'apple_receipt_verifier'
response = AppleReceiptVerifier.new.verify(receipt_data)
unless response["status"] == 0
error = ""
case response["status"]
when 21002
error = "The data in the receipt_data property was malformed or the service experienced a temporary issue"
when 21004
error = "The shared secret does not match the shared secret on file for your account"
when 21003
error = "The receipt could not be authenticated"
when 21006
error = "Receipt is valid but subscription has expired"
else
error = "Error in validating receipt"
end
return render json: {
errors: [error, status: response["status"]]
}, status:: bad_request
else
latest_receipt_info = get_latest_receipt_info(response)
if latest_receipt_info["expires_date_ms"].to_i < (Time.now.to_f * 1000).to_i
return render json: {
errors: ['Subscription is expired']
}, status:: forbidden
end
end
Get the latest receipt from the receipt validation response.
def get_latest_receipt_info(receipt_data)
# use in_app array incase of latest_receipt_info not available
if receipt_data["latest_receipt_info"].blank ?
receipt_info = receipt_data["in_app"]
else
receipt_info = receipt_data["latest_receipt_info"]
end
# neither in_app nor latest_receipt_info is set
if receipt_info.blank ?
return render json: {
errors: ['latest receipt info not found']
}, status:: forbidden
end
result = receipt_info.sort_by!{
| x | x['expires_date_ms']
}.reverse!
return result[0]
end
if request.raw_post.blank ?
return render json: {
errors: ['empty request']
}, status:: bad_request
end
info = JSON.parse(request.raw_post)
if info["receipt_data"].blank ?
return render json: {
errors: ['receipt not provided']
}, status:: bad_request
end
receipt_data = info["receipt_data"]
Yay 🎉 🍰, we‘re done with app store receipt validation…have a look at the final code below.
def appstore_verification
require 'apple_receipt_verifier'
if request.raw_post.blank ?
return render json: {
errors: ['empty request']
}, status:: bad_request
end
receipt = JSON.parse(request.raw_post)
if receipt["receipt_data"].blank ?
return render json: {
errors: ['receipt not provided']
}, status:: bad_request
end
receipt_data = receipt["receipt_data"]
response = AppleReceiptVerifier.new.verify(receipt_data)
unless response["status"] == 0
error = ""
case response["status"]
when 21002
error = "The data in the receipt_data property was malformed or the service experienced a temporary issue"
when 21004
error = "The shared secret does not match the shared secret on file for your account"
when 21003
error = "The receipt could not be authenticated"
when 21006
error = "Receipt is valid but subscription has expired"
else
error = "Error in validating receipt"
end
return render json: {
errors: [error, status: response["status"]]
}, status:: bad_request
else
latest_receipt_info = get_latest_receipt_info(response)
if latest_receipt_info["expires_date_ms"].to_i < (Time.now.to_f * 1000).to_i
return render json: {
errors: ['Subscription is expired']
}, status:: forbidden
end
# prevent receipt duplication with different user account
user_with_same_receipt = User::find_by(app_store_transaction_id: latest_receipt_info["original_transaction_id"])
if !user_with_same_receipt.blank ? && user_with_same_receipt.id != @current_user.id
return render json: {
errors: ['Duplicate receipt']
}, status:: forbidden
end
// update user with receipt validation response data
return render json: {
message: "Appstore receipt validated successfully"
}, status:: ok
end
end
Receive notifications about subscription status
P.S. This step is required only if implementing auto-renewable type subscriptions
Now is the time, when the App Store notifies the backend server(RoR here) if any change occurs in the subscriptions. Look at app store S2S notifications and configuration steps, before going ahead.
Appstore sends different types of events for status change, as described here, what we need to do is just receive and handle them.
It looks cumbersome task initially, but it’s very handy to be done with. let’s start the code for receiving notifications.
The main difference is we will get receipt_data (unified_receipt) from the app store, instead of the user.
Note: The App Store has a unique identifier per user(email) named original_transaction_id, which will always remain unique for a lifetime.
def appstore_change
puts "appstore_subscription_status_change_data"
if request.raw_post.blank?
return render json: { errors: ["bad request"] }, status: :bad_request
end
puts request.raw_post
requestData = JSON.parse(request.raw_post)
receipt_info = get_latest_receipt_info(requestData["unified_receipt"])
case requestData["notification_type"]
when "CANCEL", "DID_FAIL_TO_RENEW", "REFUND"
when "DID_CHANGE_RENEWAL_PREF", "DID_RECOVER", "DID_RENEW"
when "DID_CHANGE_RENEWAL_STATUS"
when "INITIAL_BUY"
when "INTERACTIVE_RENEWAL"
when "PRICE_INCREASE_CONSENT"
when "REVOKE"
else
puts "notification is of different type than handled types"
end
// call receipt validation method to verify latest receipt
render json: { "message": "User's subscription info updated successfully" }, status: :ok
end
Finally, we’re done with the app store auto-renewable subscription verification!