the recurring nightmare - rosa gutierrez - codemotion amsterdam 2016

59
The Recurring Nightmare Rosa Gutiérrez AMSTERDAM 11-12 MAY 2016

Upload: codemotion

Post on 16-Apr-2017

311 views

Category:

Technology


10 download

TRANSCRIPT

The Recurring NightmareRosa Gutiérrez

AMSTERDAM 11-12 MAY 2016

Rosa Gutiérrez

@rosapolis@rosaSoftware engineer at Plex

The Recurring Nightmare

Cross platform in-app subscription purchases

In-app purchases

Purchases made from within a mobile app

Everything processed via the mobile platform provider

(in exchange for ~30% of

the money spent)

One-time vs. recurring in-app purchases

One-timeStore (checkout, transactions…)

Client API

One-timeStore (checkout, transactions…)

Client API

Recurring

Our backend

Store (checkout, transactions…)

Client API

One-time vs. recurring in-app purchases

Server side APIOur API

What we need to do

● Verification and fraud prevention

● Grant subscription benefits globally

● Revoke benefits when recurring payments stop

● Reactivate benefits when payments start again

The app store server-side API of our dreams

The app store server-side API of our dreams

Simple unique IDs to get status and all relevant info about subscriptions

The app store server-side API of our dreams

Webhook system to notify changes

The app store server-side API of our dreams

Allow cancellations, refunds

The app store server-side API of our dreams

Good sandbox environment to test

The reality is a bit different...

iTunes Google Play Amazon Appstore

Simple IDs

Webhooks

Cancel, refund

Relevant info

Good sandbox

The reality is a bit different...

iTunes Google Play Amazon Appstore

Simple IDs

Webhooks

Cancel, refund

Relevant info

Good sandbox

No quirks and traps

iTunes “Simple IDs to query purchases you said?”

ewoJInNpZ25hdHVyZSIgPSAiQXJHdWUxa1dYYWhuVEZpU3hKVjhFbXFBaVB5UWhZQUpTNjFDNE1hUzZucWZHSUN4UTZKakRjL1Irczd1SUZBRUU3VDBOVEhkcjA4QXg3R0ZpU3VCeXdBcVUzMEZrRkdCTURUS3VudXA5Qm81eHpTV21TTVZlWmtBUVFXODVqTkNTaGhHTjdTc2hOTWpMZ3NUaGVnd1J3OHhNaTNSYVIxakoxMzllK0x2OTJPL0FBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RXpNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdUxnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25iMzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQTFVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hRd3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tkluclp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSjNwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQU0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBMkxUSTUKSURBNU9qRTVPakV5SUVGdFpYSnBZMkV2VEc5elgwRnVaMlZzWlhNaU93b0pJbkIxY21Ob1lYTmxMV1JoCmRHVXRiWE1pSUQwZ0lqRTBNelUxT1RVMk5URXdNREFpT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeQpJaUE5SUNJNE1EWmxaRFUwTnpCbVpqVmtNVFl3T0RWak9EY3dNemN5WldZMVkySTRNVEZrWTJRMk5qWTEKSWpzS0NTSnZjbWxuYVc1aGJDMTBjbUZ1YzJGamRHbHZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzCk5UZzBNaUk3Q2draVpYaHdhWEpsY3kxa1lYUmxJaUE5SUNJeE5ETTFOVGsxT1RVeE1EQXdJanNLQ1NKMApjbUZ1YzJGamRHbHZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzT0RnMU5pSTdDZ2tpYjNKcFoybHUKWVd3dGNIVnlZMmhoYzJVdFpHRjBaUzF0Y3lJZ1BTQWlNVFF6TlRVNU5EYzFNakF3TUNJN0Nna2lkMlZpCkxXOXlaR1Z5TFd4cGJtVXRhWFJsYlMxcFpDSWdQU0FpTVRBd01EQXdNREF6TURBME5UWTNNaUk3Q2draQpZblp5Y3lJZ1BTQWlNUzR3SWpzS0NTSjFibWx4ZFdVdGRtVnVaRzl5TFdsa1pXNTBhV1pwWlhJaUlEMGcKSWtSRk16Y3lSREV5TFVNNE5qWXRORUV6UkMxQk16VTNMVFZETXpOR1FqZ3pNa0ZFUXlJN0Nna2laWGh3CmFYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDMXdjM1FpSUQwZ0lqSXdNVFV0TURZdE1qa2dNRGs2TXprNgpNVEVnUVcxbGNtbGpZUzlNYjNOZlFXNW5aV3hsY3lJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTVRBeE16azIKTlRRMU5DSTdDZ2tpWlhod2FYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDSWdQU0FpTWpBeE5TMHdOaTB5Ck9TQXhOam96T1RveE1TQkZkR012UjAxVUlqc0tDU0p3Y205a2RXTjBMV2xrSWlBOUlDSmpiMjB1YlhsaApjSEF1WVhCd0xtbHVZWEF1Y0dGemN5NXRiMjUwYUd4NWMzVmljMk55YVhCMGFXOXVJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsSWlBOUlDSXlNREUxTFRBMkxUSTVJREUyT2pNME9qRXhJRVYwWXk5SFRWUWlPd29KCkltOXlhV2RwYm1Gc0xYQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVFV0TURZdE1qa2dNVFk2TVRrNgpNVElnUlhSakwwZE5WQ0k3Q2draVltbGtJaUE5SUNKamIyMHViWGxoY0hBdVlYQndJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsTFhCemRDSWdQU0FpTWpBeE5TMHdOaTB5T1NBd09Ub3pORG94TVNCQmJXVnlhV05oCkwweHZjMTlCYm1kbGJHVnpJanNLQ1NKeGRXRnVkR2wwZVNJZ1BTQWlNU0k3Q24wPSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbmRib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0=

iTunes “Simple IDs to query purchases you said?”

Receipt data

Our backend

iTunes

huge base64encoded receipt

huge base64encoded receipt

huge ...receipt response with

subscription data

iTunes

payload = { "receipt-data" => @receipt, "password" => self.class.password}

resp = HTTParty.post url, :body => payload.to_json

Send to iTunes for validation

Sandbox or production

App's shared secret

iTunes

payload = { "receipt-data" => @receipt, "password" => self.class.password}

resp = HTTParty.post url, :body => payload.to_json

Send to iTunes for validation

Parse response{"receipt":{... <json receipt data> ...},

"latest_receipt_info":{... <json latest receipt data> ...}

"status":0, # error codes 21000 to 21008

"latest_receipt":<huge base64 encoded receipt data> }

Store latest receipt for next time

iTunes{

"purchase-date-ms": "1435595651000",

"unique-identifier": "806ed5470ff5d16085c870372ef5cb811dcd6665",

"original-transaction-id": "1000000161275842",

"expires-date": "1435595951000",

"transaction-id": "1000000161278856",

"original-purchase-date-ms": "1435594752000",

"item-id": "1014965453",

"product-id": "com.myapp.app.inap.discworldpass.monthlysubscription",

"purchase-date": "2015-06-29 16:34:11 Etc/GMT",

"bid": "com.myapp.app",

...

}

JSON Receipt data

iTunes

Always check duplicates "transaction-id" => "1000000161278856"

Always check product id "product-id" => "com.zeptolab.ctrbonus.superpower1"

ewoJInNpZ25hdHVyZSIgPSAiQXJHdWUxa1dYYWhuVEZpU3hKVjhFbXFBaVB5UWhZQUpTNjFDNE1hUzZucWZHSUN4UTZKakRjL1Irczd1SUZBRUU3VDBOVEhkcjA4QXg3R0ZpU3VCeXdBcVUzMEZrRkdCTURUS3VudXA5Qm81eHpTV21TTVZlWmtBUVFXODVqTkNTaGhHTjdTc2hOTWpMZ3NUaGVnd1J3OHhNaTNSYVIxakoxMzllK0x2OTJPL0FBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RXpNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdUxnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25iMzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQTFVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hRd3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tkluclp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSjNwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQU0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBMkxUSTUKSURBNU9qRTVPakV5SUVGdFpYSnBZMkV2VEc5elgwRnVaMlZzWlhNaU93b0pJbkIxY21Ob1lYTmxMV1JoCmRHVXRiWE1pSUQwZ0lqRTBNelUxT1RVMk5URXdNREFpT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeQpJaUE5SUNJNE1EWmxaRFUwTnpCbVpqVmtNVFl3T0RWak9EY3dNemN5WldZMVkySTRNVEZrWTJRMk5qWTEKSWpzS0NTSnZjbWxuYVc1aGJDMTBjbUZ1YzJGamRHbHZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzCk5UZzBNaUk3Q2draVpYaHdhWEpsY3kxa1lYUmxJaUE5SUNJeE5ETTFOVGsxT1RVeE1EQXdJanNLQ1NKMApjbUZ1YzJGamRHbHZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzT0RnMU5pSTdDZ2tpYjNKcFoybHUKWVd3dGNIVnlZMmhoYzJVdFpHRjBaUzF0Y3lJZ1BTQWlNVFF6TlRVNU5EYzFNakF3TUNJN0Nna2lkMlZpCkxXOXlaR1Z5TFd4cGJtVXRhWFJsYlMxcFpDSWdQU0FpTVRBd01EQXdNREF6TURBME5UWTNNaUk3Q2draQpZblp5Y3lJZ1BTQWlNUzR3SWpzS0NTSjFibWx4ZFdVdGRtVnVaRzl5TFdsa1pXNTBhV1pwWlhJaUlEMGcKSWtSRk16Y3lSREV5TFVNNE5qWXRORUV6UkMxQk16VTNMVFZETXpOR1FqZ3pNa0ZFUXlJN0Nna2laWGh3CmFYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDMXdjM1FpSUQwZ0lqSXdNVFV0TURZdE1qa2dNRGs2TXprNgpNVEVnUVcxbGNtbGpZUzlNYjNOZlFXNW5aV3hsY3lJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTVRBeE16azIKTlRRMU5DSTdDZ2tpWlhod2FYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDSWdQU0FpTWpBeE5TMHdOaTB5Ck9TQXhOam96T1RveE1TQkZkR012UjAxVUlqc0tDU0p3Y205a2RXTjBMV2xrSWlBOUlDSmpiMjB1YlhsaApjSEF1WVhCd0xtbHVZWEF1Y0dGemN5NXRiMjUwYUd4NWMzVmljMk55YVhCMGFXOXVJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsSWlBOUlDSXlNREUxTFRBMkxUSTVJREUyT2pNME9qRXhJRVYwWXk5SFRWUWlPd29KCkltOXlhV2RwYm1Gc0xYQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVFV0TURZdE1qa2dNVFk2TVRrNgpNVElnUlhSakwwZE5WQ0k3Q2draVltbGtJaUE5SUNKamIyMHViWGxoY0hBdVlYQndJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsTFhCemRDSWdQU0FpTWpBeE5TMHdOaTB5T1NBd09Ub3pORG94TVNCQmJXVnlhV05oCkwweHZjMTlCYm1kbGJHVnpJanNLQ1NKeGRXRnVkR2wwZVNJZ1BTQWlNU0k3Q24wPSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbmRib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0=

iTunes “Simple IDs to query purchases you said?”

Receipt data

{ "signature" = "ArGue1kWXahnTFiSxJV8EmqAiPyQhYAJS61C4MaS6nqfGICxQ6JjDc/R+s7uIFAEE7T0NTHdr08Ax7GFiSuBywAqU30FkFGBMDTKunup9Bo5xzSWmSMVeZkAQQW85jNCShhGN7SshNMjLgsThegwRw8xMi3RaR1jJ139e+Lv92O/AAADVzCCA1MwggI7oAMCAQICCBup4+PAhm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXDTE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZhS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJVxVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGTOvA==";"purchase-info"="ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTI5IDA5OjE5OjEyIEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInB1cmNoYXNlLWRhdGUtbXMiID0gIjE0MzU1OTU2NTEwMDAiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICI4MDZlZDU0NzBmZjVkMTYwODVjODcwMzcyZWY1Y2I4MTFkY2Q2NjY1IjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3NTg0MiI7CgkiZXhwaXJlcy1kYXRlIiA9ICIxNDM1NTk1OTUxMDAwIjsKCSJ0cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3ODg1NiI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNTU5NDc1MjAwMCI7Cgkid2ViLW9yZGVyLWxpbmUtaXRlbS1pZCIgPSAiMTAwMDAwMDAzMDA0NTY3MiI7CgkiYnZycyIgPSAiMS4wIjsKCSJ1bmlxdWUtdmVuZG9yLWlkZW50aWZpZXIiID0gIkRFMzcyRDEyLUM4NjYtNEEzRC1BMzU3LTVDMzNGQjgzMkFEQyI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZC1wc3QiID0gIjIwMTUtMDYtMjkgMDk6Mzk6MTEgQW1lcmljYS9Mb3NfQW5nZWxlcyI7CgkiaXRlbS1pZCIgPSAiMTAxMzk2NTQ1NCI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZCIgPSAiMjAxNS0wNi0yOSAxNjozOToxMSBFdGMvR01UIjsKCSJwcm9kdWN0LWlkIiA9ICJjb20ubXlhcHAuYXBwLmluYXAucGFzcy5tb250aGx5c3Vic2NyaXB0aW9uIjsKCSJwdXJjaGFzZS1kYXRlIiA9ICIyMDE1LTA2LTI5IDE2OjM0OjExIEV0Yy9HTVQiOwoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUiID0gIjIwMTUtMDYtMjkgMTY6MTk6MTIgRXRjL0dNVCI7CgkiYmlkIiA9ICJjb20ubXlhcHAuYXBwIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0yOSAwOTozNDoxMSBBbWVyaWNhL0xvc19BbmdlbGVzIjsKCSJxdWFudGl0eSIgPSAiMSI7Cn0="; "environment" = "Sandbox"; "pod" = "100"; "signing-status" = "0";}

iTunes

"purchase-info"

{ "signature" = "ArGue1kWXahnTFiSxJV8EmqAiPyQhYAJS61C4MaS6nqfGICxQ6JjDc/R+s7uIFAEE7T0NTHdr08Ax7GFiSuBywAqU30FkFGBMDTKunup9Bo5xzSWmSMVeZkAQQW85jNCShhGN7SshNMjLgsThegwRw8xMi3RaR1jJ139e+Lv92O/AAADVzCCA1MwggI7oAMCAQICCBup4+PAhm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXDTE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZhS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJVxVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGTOvA==";"purchase-info"="ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTI5IDA5OjE5OjEyIEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInB1cmNoYXNlLWRhdGUtbXMiID0gIjE0MzU1OTU2NTEwMDAiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICI4MDZlZDU0NzBmZjVkMTYwODVjODcwMzcyZWY1Y2I4MTFkY2Q2NjY1IjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3NTg0MiI7CgkiZXhwaXJlcy1kYXRlIiA9ICIxNDM1NTk1OTUxMDAwIjsKCSJ0cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3ODg1NiI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNTU5NDc1MjAwMCI7Cgkid2ViLW9yZGVyLWxpbmUtaXRlbS1pZCIgPSAiMTAwMDAwMDAzMDA0NTY3MiI7CgkiYnZycyIgPSAiMS4wIjsKCSJ1bmlxdWUtdmVuZG9yLWlkZW50aWZpZXIiID0gIkRFMzcyRDEyLUM4NjYtNEEzRC1BMzU3LTVDMzNGQjgzMkFEQyI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZC1wc3QiID0gIjIwMTUtMDYtMjkgMDk6Mzk6MTEgQW1lcmljYS9Mb3NfQW5nZWxlcyI7CgkiaXRlbS1pZCIgPSAiMTAxMzk2NTQ1NCI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZCIgPSAiMjAxNS0wNi0yOSAxNjozOToxMSBFdGMvR01UIjsKCSJwcm9kdWN0LWlkIiA9ICJjb20ubXlhcHAuYXBwLmluYXAucGFzcy5tb250aGx5c3Vic2NyaXB0aW9uIjsKCSJwdXJjaGFzZS1kYXRlIiA9ICIyMDE1LTA2LTI5IDE2OjM0OjExIEV0Yy9HTVQiOwoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUiID0gIjIwMTUtMDYtMjkgMTY6MTk6MTIgRXRjL0dNVCI7CgkiYmlkIiA9ICJjb20ubXlhcHAuYXBwIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0yOSAwOTozNDoxMSBBbWVyaWNhL0xvc19BbmdlbGVzIjsKCSJxdWFudGl0eSIgPSAiMSI7Cn0="; "environment" = "Sandbox"; "pod" = "100"; "signing-status" = "0";}

iTunes

def decode_itunes_object(encoded)

decoded = Base64.decode64(encoded)

JSON.parse(decoded.gsub(/(\s+)"?([\w\-]+)"? = (.*);/, '\1"\2": \3,').sub(",\n}", "\n}"))

rescue JSON::ParserError

nil

end

end

{ "original-purchase-date-pst" = "2015-06-29 09:19:12 America/Los_Angeles"; "purchase-date-ms" = "1435595651000"; "unique-identifier" = "806ed5470ff5d16085c870372ef5cb811dcd6665"; "original-transaction-id" = "1000000161275842"; "expires-date" = "1435595951000"; "transaction-id" = "1000000161278856"; "original-purchase-date-ms" = "1435594752000"; "web-order-line-item-id" = "1000000030045672"; "bvrs" = "1.0"; "unique-vendor-identifier" = "DE372D12-C866-4A3D-A357-5C33FB832ADC"; "expires-date-formatted-pst" = "2015-06-29 09:39:11 America/Los_Angeles"; "item-id" = "1013965454"; "expires-date-formatted" = "2015-06-29 16:39:11 Etc/GMT"; "product-id" = "com.myapp.app.inap.pass.monthlysubscription"; "purchase-date" = "2015-06-29 16:34:11 Etc/GMT"; "original-purchase-date" = "2015-06-29 16:19:12 Etc/GMT"; "bid" = "com.myapp.app"; "purchase-date-pst" = "2015-06-29 09:34:11 America/Los_Angeles"; "quantity" = "1";}

iTunes

iTunesTrap #1: Actions outside the app

Cancellations, refunds: detect expired subscriptions and revoke{

"receipt":{... <json receipt data> ...},

"status":21006,

"latest_expired_receipt_info":<huge base64 encoded receipt data>

}

iTunesTrap #1: Actions outside the app

Cancellations, refunds: detect expired subscriptions and revoke{

"receipt":{... <json receipt data> ...},

"status":21006,

"latest_expired_receipt_info":<huge base64 encoded receipt data>

}

Yes, we only notice when it has expired ¬¬

~ $ crontab -l30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes] 2>&1 | logger -t check_subscriptions_iap

iTunesTrap #1: Actions outside the app

Renewals:{"receipt":{... <json receipt data> ...},

"latest_receipt_info":{... <json latest receipt data> ...}

"Status":0, # error codes 21000 to 21008

"latest_receipt":<huge base64 encoded receipt data> }

Reactivations: "Restore Purchase" button

iTunesTrap #2: Support for sandbox & production

def validate_with_itunes(environment = :production)

url = if environment == :production

'https://buy.itunes.apple.com/verifyReceipt'

else

'https://sandbox.itunes.apple.com/verifyReceipt'

end

...

if json["status"] == 0

# process subscription

elsif json["status"] == 21007 && environment == :production

validate_with_itunes(:sandbox)

Google Play “Better late than never”

Our backend

Google Playtoken

tokenpackage namesubscription id

tokensubscription id

package nameresponse with subscription data

Google Play “Better late than never”

All very nice at first lookGET https://www.googleapis.com/androidpublisher/v2/applications/package_name/purchases/subscriptions/subscription_id/tokens/token

Package name: com.myapp.androidSubscription id: monthly_subToken: lppdmbnkljkkgcldbinmhmji.AO-J1OyBx_FJSc6_fZwPnb9urd6u3jOJdaPmonghNlWcFlqG9hLAIphJia8ETGqY6bIZJNzLVKm226pCc91DjvRPkipLHbhh5IEHniNGNj5yDxnXejuSvd-1wXA-z2HVh49wQAe4fFKs9uRA53TMxHWPLTmMGO5gpB

Even with cancel, refund and revoke!POST https://www.googleapis.com/androidpublisher/v2/applications/package_name/purchases/subscriptions/subscription_id/tokens/token:(cancel|refund|revoke)

HTTP/1.1 200

{

"kind": "androidpublisher#subscriptionPurchase",

"startTimeMillis": "1454584613607",

"expiryTimeMillis": "1457090213000",

"autoRenewing": true,

"priceCurrencyCode": "GBP",

"priceAmountMicros": "3990000",

"countryCode": "GB",

"developerPayload": "Rincewind",

"paymentState": 1

}

Google Play “Better late than never”

A totally sane response

Google Play

HTTP/1.1 200

{

"kind": "androidpublisher#subscriptionPurchase",

"startTimeMillis": "1454584613607",

"expiryTimeMillis": "1457090213000",

"autoRenewing": true,

"priceCurrencyCode": "GBP",

"priceAmountMicros": "3990000",

"countryCode": "GB",

"developerPayload": "Rincewind",

"paymentState": 1

}

“Better late than never”

A totally sane response

Google PlaySurprises with the token

lppdmbnkljkkgcldbinmhmji.AO-J1OyBx_FJSc6_fZwPnb9urd6u3jOJdaPmonghNlWcFlqG9hLAIphJia8ETGqY6bIZJNzLVKm226pCc91DjvRPkipLHbhh5IEHniNGNj5yDxnXejuSvd-1wXA-z2HVh49wQAe4fFKs9uRA53TMxHWPLTmMGO5gpB

Google PlaySurprises with the token

lppdmbnkljkkgcldbinmhmji.AO-J1OyBx_FJSc6_fZwPnb9urd6u3jOJdaPmonghNlWcFlqG9hLAIphJia8ETGqY6bIZJNzLVKm226pCc91DjvRPkipLHbhh5IEHniNGNj5yDxnXejuSvd-1wXA-z2HVh49wQAe4fFKs9uRA53TMxHWPLTmMGO5gpB

Google PlayDeveloper payload to the rescueHTTP/1.1 200

{

"kind": "androidpublisher#subscriptionPurchase",

"startTimeMillis": "1454584613607",

"expiryTimeMillis": "1457090213000",

"autoRenewing": true,

"priceCurrencyCode": "GBP",

"priceAmountMicros": "3990000",

"countryCode": "GB",

"developerPayload": "{\"username\": \"Rincewind\", \"id\": \"42-8-42\"}",

"paymentState": 1

}

Trap #1: Order IDs(What the users will send to your Support team)

Base Merchant Order number + recurrenceGPA.1234-5678-9012-34567 (base order number)GPA.1234-5678-9012-34567..0 (first recurrence orderID)GPA.1234-5678-9012-34567..1 (second recurrence orderID)GPA.1234-5678-9012-34567..2 (third recurrence orderID)

Google Play

Our backend

Google Playbase order numbertoken

tokensubscription id

package nametokenpackage namesubscription idbase order number

response with subscription data

android.test.purchased, android.test.canceled...

Trap #1.5: No real sandbox

Static responses (useless)

Google Play

Test accounts

In-app purchases without paying

Set up test accounts

Publish app to the alpha distribution channel

No emulators, only devices allowed

Trap #2: Actions outside the app

Google PlayWe already know this one!

Detect cancellations and renewals{

...

"expiryTimeMillis": "1457090213000",

"autoRenewing": false,

"cancelReason": 0,

...

}No reactivations outside \o/

~ $ crontab -l30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes,google] 2>&1 | logger -t check_subscriptions_iap

Trap #3: false renewals with payment problems

Google Play

{

...

"expiryTimeMillis": "1457090213000",

"autoRenewing": true,

"paymentState": 0,

...

}

Get updated every ~24 hours

Amazon Appstore “The Pandora’s sandbox”

Amazon Appstore “The Pandora’s sandbox”

“Oh, this seems nice!”

Image from: https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/verifying-receipts-in-iap-2.0

user IDreceipt ID

Our backend

Amazon RVSuser IDreceipt ID

shared secretreceipt IDuser ID

response with subscription data

Amazon Appstore “The Pandora’s sandbox”

GET https://appstore-sdk.amazon.com/version/1.0/verifyReceiptId/developer/

shared_secret/user/user_id/receiptId/receipt_id

Shared secret: 2:ZzH1YJ4gFT6Y87blswfPDgidMX7VAv71Xaog:8912WkCsHQayBgdjxK2g==User Id: l3HL7XppEMhrOGDnur9-ujhyhdSg6qyODKmah76lJU=Receipt Id: HiOJ8ji36YngnQovTqSIHQxR53GsMLqkR1tKLp5c=:5:12

Request to RVS Different in sandbox

Amazon Appstore“Let’s test with the sandbox! … ouch -_-”

1. Use App Tester tool in Android device

2. Install and configure Tomcat locally

3. Deploy RVSSandbox.war locally

4. Deploy RVSSandbox.war somewhere

public (optional)

Amazon Appstore“Let’s test with the sandbox! … ouch -_-”

1. Use App Tester tool in Android device

2. Install and configure Tomcat locally

3. Deploy RVSSandbox.war locally

4. Deploy RVSSandbox.war somewhere

public (optional)

5. … And better forget about it

HTTP/1.1 200

{

"betaProduct":true,

"cancelDate":null,

"parentProductId":null,

"productId":"my_subscription_v1",

"productType":"SUBSCRIPTION",

"purchaseDate":1400784371000,

"quantity":null,

"receiptId":"HiOJ8ji36YngnQovTqSIHQxR53GsMLqkR1tKLp5c=:5:12",

"testTransaction":true

}

Sandbox response (and current docs!)

Amazon Appstore

HTTP/1.1 200

{

...

"cancelDate":null,

"parentProductId":null,

"productId":"my_subscription_v1",

"productType":"SUBSCRIPTION",

"renewalDate":1463407610000,

"term":"1 Month",

"termSku":"my_subscription_monthly_v1"

...

}

Real responseAmazon Appstore

HTTP/1.1 200

{

...

"cancelDate":null,

"parentProductId":null,

"productId":"my_subscription_v1",

"productType":"SUBSCRIPTION",

"renewalDate":1463407610000,

"term":"1 Month",

"termSku":"my_subscription_monthly_v1"

...

}

Real responseAmazon Appstore

Use Live App Testing!

Trap #1: Actions outside the app

Cancellations: detect expired subscriptions and revoke

Amazon AppstoreYep, same story

HTTP/1.1 200

{

...

"cancelDate":1463407610000,

...

}

Trap #1: Actions outside the app

Cancellations: detect expired subscriptions and revoke

Amazon AppstoreYep, same story

HTTP/1.1 200

{

...

"cancelDate":1463407610000,

...

}

Again, we only notice when it

has expired ¬¬

~ $ crontab -l30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes,google,amazon] 2>&1 | logger -t check_subscriptions_iap

Trap #1: Actions outside the app

Cancellations: detect expired subscriptions and revoke

Amazon AppstoreYep, same story

HTTP/1.1 200

{

...

"cancelDate":1463407610000,

...

}

Again, we only notice when it

has expired ¬¬

~ $ crontab -l30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes,google,amazon] 2>&1 | logger -t check_subscriptions_iap

In future adventures...

The End

All Pictures and logos belong to their respective owners/companies