When we’ve production-ready application builds, all we want is to implement IAP for it (not for all, but on 90% of those though).
This article will dive deep into how to verify Play Store IAP using it on the server side. I had taken RoR as my backend you can implement it for other platforms by referring to it. Gems like candy_check provide inbuilt functionality for purchase token verification, but I’d like to verify it manually using Google APIs.
We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!
IAP verification contains mainly two parts.
With an assumption, you already had configured the app on the play store, Let’s jump directly to the purchase token verification process on the server side. Following values we need handy for play store purchase token verification, If you haven’t generated them yet, then go with the flow described here to get it.
Here, a Google refresh token will be needed to renew the Google Access token, as it comes with a very short lifetime of 1hr.
If Initial purchase token verification returns a 400 status code, it means the Google Access Token passed into headers with the Authorization key is expired, in that case, we must renew it using the Google Refresh token. Let’s do it step by step.
a. In request, we’ll need three parameters.
payload = {
package_name: params[:package_name], #app pkg (eg. "com.myapp")
product_id: params[:product_id], #app SKU/product_id
purchase_token: params[:purchase_token] #purchaseToken
}
But, we have to pass a valid Google Access Token into headers with key Autorization
. For that need to define the global variable $accessToken as below. Later on, we’ll override its value by renewing it if gets expires.
$accessToken = "ya29.axxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2EGD"
b. The requested purchase token will be verified using the below function, by invoking Google play store API.
# purchaseToken verification
def playstore_subscription(payload)url = "https://www.googleapis.com/androidpublisher/v3/applications/#{payload[:package_name]}/purchases/subscriptions/#{payload[:product_id]}/tokens/#{payload[:purchase_token]}"uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = truerequest = Net::HTTP::Get.new(uri.request_uri,"Content-Type" => "application/json","Authorization" => "Bearer #{$accessToken}"
)response = http.request(request)
result = JSON.parse(response.body)return result
end
where, $accessToken = Google Access Token
c. Renew the Google Access token with the below method, if the above method returns a 400 status code.
client_id = ENV.fetch('GOOGLE_KEY') { '12XXXXXXXXXX- f2edxxxxxxxxxxxxxxxxxxxxxxxxxa1s2.apps.googleusercontent.com' }client_secret = ENV.fetch('GOOGLE_SECRET') { 'lW-W3xxxxxxxxxxxxxxxxxXq' }refresh_token = ENV.fetch('GOOGLE_REFRESH_TOKEN') { '1//0423rXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX-XXXXXXE' }# renew access-token
def renew_access_token (client_id, client_secret, refresh_token) url = "https://accounts.google.com/o/oauth2/token? grant_type=refresh_token&client_id=#{client_id}&client_secret=#{client_secret}&refresh_token=#{refresh_token}" uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.request_uri) response = http.request(request)
result = JSON.parse(response.body) return result['access_token'] #returns renewed access-token
end
We need to repeat case b. with the renewed token, now you can see it verifies the purchase token with status code 200.
The below snippet represents the full code of Play Store IAP verification.
verification_controller.rb
# Request payload
payload = {
package_name: params[:package_name], #app pkg (eg. "com.myapp")
product_id: params[:product_id], #app SKU/product_id
purchase_token: params[:purchase_token] #app purchase token
}
# Verify IAP
def verify_playstore_subscription (payload)
client_id = ENV.fetch('GOOGLE_KEY') { '12XXXXXXXXXX- f2edxxxxxxxxxxxxxxxxxxxxxxxxxa1s2.apps.googleusercontent.com' } client_secret = ENV.fetch('GOOGLE_SECRET') { 'lW-W3xxxxxxxxxxxxxxxxxXq' } refresh_token = ENV.fetch('GOOGLE_REFRESH_TOKEN') { '1//0423rXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX-XXXXXXE' }
result = playstore_subscription(payload)
if result && !result['orderId']
case result['error']['code']
when 401 #renew access-token
new_access_token = renew_access_token(client_id, client_secret, refresh_token)if new_access_token #override $accessToken (global variable)
$accessToken = new_access_token
end
#repeat subscription verification
result = playstore_subscription(payload)
if result && !result['orderId']
err_code = result['error']['code']
return err_code == 400 ? { status_code: :bad_request, data: result } : { status_code: :internal_server_error, data: result }
end when 400
return { status_code: :bad_request, data: result }
else
return { status_code: :internal_server_error, data: result }
end
end
# Return expired subscription error
if result['expiryTimeMillis'].to_i < (Time.now.to_f * 1000).to_i
return { status_code: :forbidden, data: { errors: ['Subscription is expired'] } }
end
return { status_code: :ok, data: result }
end
# purchaseToken verification
def playstore_subscription(payload)url = "https://www.googleapis.com/androidpublisher/v3/applications/#{payload[:package_name]}/purchases/subscriptions/#{payload[:product_id]}/tokens/#{payload[:purchase_token]}"uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = truerequest = Net::HTTP::Get.new(uri.request_uri,"Content-Type" => "application/json","Authorization" => "Bearer #{$accessToken}"
)response = http.request(request)
result = JSON.parse(response.body)return result
end
#renew access-token
def renew_access_token (client_id, client_secret, refresh_token)url = "https://accounts.google.com/o/oauth2/token? grant_type=refresh_token&client_id=#{client_id}&client_secret=#{client_secret}&refresh_token=#{refresh_token}"uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.request_uri)response = http.request(request)
result = JSON.parse(response.body)return result['access_token'] #returns renewed access-token
end
$accessToken = Google Access token, which i’ve taken global (global variable in ruby starts with $ sign).
With this, we’ve completed purchase token verification and get our user info from it. Now, it’s time to go for the next step below, to continuously monitor IAP status without running any scheduling tasks or cronjob repeatedly.
Real-Time Developer Notifications allow us to react immediately to subscription state changes. For that, we need to use Google Cloud Pub/Sub which leverages RTDN by pushing notifications from the cloud to our backend server. It comes with a restricted quota though, if gets accessively used. Look at How to configure Google Cloud Pub/Sub for deep details. Kindly take note of the project_id, subscriber, and the topic from the developer console, once you’re done with the setup.
There are two types of subscriptions :
In this article, we’re going to implement the subscription’s Pull method.
Assuming you had all set up for Google Pub/Sub, we’ll go to the implementation now.
Since we’re using the pull method, let’s set up the google/cloud/pubsub gem, which will run the process in the background and will invoke Playstore API, for pulling notifications out of it!
a. Let’s add the required gems and files to the initializers/pub_sub.rb and verify that it gets executed on app startup!
require "google/cloud/pubsub"
require "./app/controllers/verification_controller.rb"
puts "initializing google cloud pub/sub-subscriber"
b. Initialize Google cloud PubSub with google service account key.
if ENV["GOOGLE_SERVICE_ACCOUNT_KEY"].blank?
return
end
# initialize google cloud pub/sub
pubsub = Google::Cloud::PubSub.new(
project_id: 'my_project',
credentials:ENV["GOOGLE_SERVICE_ACCOUNT_KEY"]
)
c. Let’s assign a subscriber of Google PubSub in order to pull notifications published over there.
subscription = pubsub.subscription "your_subscriber_name"
subscriber_name = Get it also from google developer console
for ex. projects/my_project/subscriptions/my-iap-sub
d. Finally, let’s pull notifications from the subscriber, published by PubSub topic, and update the user’s info according to it.
subscriber = subscription.listen do |received_message|# Update user according to received notification from server
User::VerificationController.new.update_user_playstore_subscription_info(received_message.message.data)# Acknowledge received message
received_message.acknowledge!
end
P.S. : received_message.acknowledge! is a must to mark the message as received. You can get more info here.
See the full code of the pub-sub initializer below :
pub_sub.rb
require "google/cloud/pubsub"
require "./app/controllers/verification_controller.rb"
puts "initializing google cloud pub/sub-subscriber"if ENV["GOOGLE_SERVICE_ACCOUNT_KEY"].blank?
return
end
# initialize google cloud pub/sub
pubsub = Google::Cloud::PubSub.new(
project_id: 'my_project',
credentials:ENV["GOOGLE_SERVICE_ACCOUNT_KEY"]
)
# assign google cloud subscriber(created using developer console)
subscription = pubsub.subscription "your_subscriber_name"
# listen published content by topic (topic_name = projects/my_project/topics/my-iap)
subscriber = subscription.listen do |received_message|
# Update user according to received notification from server
User::VerificationController.new.update_user_playstore_subscription_info(received_message.message.data)
# Acknowledge received message
received_message.acknowledge!
end
# Handle exceptions from listener
subscriber.on_error do |exception|
puts "Exception: #{exception.class} #{exception.message}"
end
# all received messages have been processed or 10 seconds have passed.
at_exit do
subscriber.stop!(10)
end
# Start background threads that will call the block passed to listen.
subscriber.start
Hurray !! Had successfully set RTDN and user update with the given info. Last but not least, Playstore pushes the same payload in every type of RTDN, which is the most important part of handling RTDNs. The sample payload received in RTDN is below.
{
"version":"1.0",
"packageName":"com.some.thing",
"eventTimeMillis":"1503349566168", #notification receiving time
"subscriptionNotification":
{
"version":"1.0",
"notificationType":4, # type of notification
"purchaseToken":"PURCHASE_TOKEN", # IAP purchaseToken
"subscriptionId":"my.sku" #SKU/productId of your app
}
}
You can look here for more information about notification payload too, though.
After receiving RTDN from the Google Play Store, to update users with the latest IAP status, we must again verify the user purchase token(received in RTDN), as we’re not getting any user information except the purchase token from RTDN.
That’s all about IAP verification with RTDN using RoR as a backend server.