Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Recurring Nightmare. Cross platform in-app ...

Rosa
May 11, 2016

The Recurring Nightmare. Cross platform in-app subscription purchases

Most app stores (iTunes, Google Play, Amazon Appstore) let us offer in-app recurring purchases in exchange for a percent of the price but that doesn't come without headaches. Unlike most one-time in-app purchases the backend piece is critical for subscriptions. Trials, expirations, renewals, hacking attempts... In this talk we'll see an overview of what to consider when handling different stores and some quirks and annoyances we'll need to deal with.

Talk given at Codemotion Amsterdam 2016.

Rosa

May 11, 2016
Tweet

More Decks by Rosa

Other Decks in Technology

Transcript

  1. 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)
  2. One-time Store (checkout, transactions…) Client API Recurring Our backend Store

    (checkout, transactions…) Client API One-time vs. recurring in-app purchases Server side API Our API
  3. 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
  4. The app store server-side API of our dreams Simple unique

    IDs to get status and all relevant info about subscriptions
  5. The reality is a bit different... iTunes Google Play Amazon

    Appstore Simple IDs Webhooks Cancel, refund Relevant info Good sandbox
  6. The reality is a bit different... iTunes Google Play Amazon

    Appstore Simple IDs Webhooks Cancel, refund Relevant info Good sandbox No quirks and traps
  7. ewoJInNpZ25hdHVyZSIgPSAiQXJHdWUxa1dYYWhuVEZpU3hKVjhFbXFBaVB5UWhZQUpTNjFDNE1hUzZucWZHSUN4UTZKakRjL1Irczd1SUZBRUU3VDBOVEhkcj A4QXg3R0ZpU3VCeXdBcVUzMEZrRkdCTURUS3VudXA5Qm81eHpTV21TTVZlWmtBUVFXODVqTkNTaGhHTjdTc2hOTWpMZ3NUaGVnd1J3OHhNaTNSYVIxakoxMzll K0x2OTJPL0FBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRF ZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3 YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RX pNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5 eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdU xnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25i MzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQT FVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hR

    d3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3 FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tklu clp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSj NwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQ U0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZH BibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBMkxUSTUKSURBNU9qRTVPakV5SUVGdFpYSnBZMkV2VEc5elgwRnVaMlZzWlhNaU93 b0pJbkIxY21Ob1lYTmxMV1JoCmRHVXRiWE1pSUQwZ0lqRTBNelUxT1RVMk5URXdNREFpT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeQpJaUE5SUNJNE1EWm xaRFUwTnpCbVpqVmtNVFl3T0RWak9EY3dNemN5WldZMVkySTRNVEZrWTJRMk5qWTEKSWpzS0NTSnZjbWxuYVc1aGJDMTBjbUZ1YzJGamRHbHZiaTFwWkNJZ1BT QWlNVEF3TURBd01ERTJNVEkzCk5UZzBNaUk3Q2draVpYaHdhWEpsY3kxa1lYUmxJaUE5SUNJeE5ETTFOVGsxT1RVeE1EQXdJanNLQ1NKMApjbUZ1YzJGamRHbH ZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzT0RnMU5pSTdDZ2tpYjNKcFoybHUKWVd3dGNIVnlZMmhoYzJVdFpHRjBaUzF0Y3lJZ1BTQWlNVFF6TlRVNU5E YzFNakF3TUNJN0Nna2lkMlZpCkxXOXlaR1Z5TFd4cGJtVXRhWFJsYlMxcFpDSWdQU0FpTVRBd01EQXdNREF6TURBME5UWTNNaUk3Q2draQpZblp5Y3lJZ1BTQW lNUzR3SWpzS0NTSjFibWx4ZFdVdGRtVnVaRzl5TFdsa1pXNTBhV1pwWlhJaUlEMGcKSWtSRk16Y3lSREV5TFVNNE5qWXRORUV6UkMxQk16VTNMVFZETXpOR1Fq Z3pNa0ZFUXlJN0Nna2laWGh3CmFYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDMXdjM1FpSUQwZ0lqSXdNVFV0TURZdE1qa2dNRGs2TXprNgpNVEVnUVcxbGNtbG pZUzlNYjNOZlFXNW5aV3hsY3lJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTVRBeE16azIKTlRRMU5DSTdDZ2tpWlhod2FYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpD SWdQU0FpTWpBeE5TMHdOaTB5Ck9TQXhOam96T1RveE1TQkZkR012UjAxVUlqc0tDU0p3Y205a2RXTjBMV2xrSWlBOUlDSmpiMjB1YlhsaApjSEF1WVhCd0xtbH VZWEF1Y0dGemN5NXRiMjUwYUd4NWMzVmljMk55YVhCMGFXOXVJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsSWlBOUlDSXlNREUxTFRBMkxUSTVJREUyT2pNME9q RXhJRVYwWXk5SFRWUWlPd29KCkltOXlhV2RwYm1Gc0xYQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVFV0TURZdE1qa2dNVFk2TVRrNgpNVElnUlhSakwwZE 5WQ0k3Q2draVltbGtJaUE5SUNKamIyMHViWGxoY0hBdVlYQndJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsTFhCemRDSWdQU0FpTWpBeE5TMHdOaTB5T1NBd09U b3pORG94TVNCQmJXVnlhV05oCkwweHZjMTlCYm1kbGJHVnpJanNLQ1NKeGRXRnVkR2wwZVNJZ1BTQWlNU0k3Q24wPSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbm Rib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0= iTunes “Simple IDs to query purchases you said?” Receipt data
  8. Our backend iTunes huge base64 encoded receipt huge base64 encoded

    receipt huge ... receipt response with subscription data
  9. 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
  10. 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
  11. 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
  12. ewoJInNpZ25hdHVyZSIgPSAiQXJHdWUxa1dYYWhuVEZpU3hKVjhFbXFBaVB5UWhZQUpTNjFDNE1hUzZucWZHSUN4UTZKakRjL1Irczd1SUZBRUU3VDBOVEhkcj A4QXg3R0ZpU3VCeXdBcVUzMEZrRkdCTURUS3VudXA5Qm81eHpTV21TTVZlWmtBUVFXODVqTkNTaGhHTjdTc2hOTWpMZ3NUaGVnd1J3OHhNaTNSYVIxakoxMzll K0x2OTJPL0FBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRF ZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3 YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RX pNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5 eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdU xnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25i MzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQT FVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hR

    d3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3 FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tklu clp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSj NwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQ U0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZH BibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBMkxUSTUKSURBNU9qRTVPakV5SUVGdFpYSnBZMkV2VEc5elgwRnVaMlZzWlhNaU93 b0pJbkIxY21Ob1lYTmxMV1JoCmRHVXRiWE1pSUQwZ0lqRTBNelUxT1RVMk5URXdNREFpT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeQpJaUE5SUNJNE1EWm xaRFUwTnpCbVpqVmtNVFl3T0RWak9EY3dNemN5WldZMVkySTRNVEZrWTJRMk5qWTEKSWpzS0NTSnZjbWxuYVc1aGJDMTBjbUZ1YzJGamRHbHZiaTFwWkNJZ1BT QWlNVEF3TURBd01ERTJNVEkzCk5UZzBNaUk3Q2draVpYaHdhWEpsY3kxa1lYUmxJaUE5SUNJeE5ETTFOVGsxT1RVeE1EQXdJanNLQ1NKMApjbUZ1YzJGamRHbH ZiaTFwWkNJZ1BTQWlNVEF3TURBd01ERTJNVEkzT0RnMU5pSTdDZ2tpYjNKcFoybHUKWVd3dGNIVnlZMmhoYzJVdFpHRjBaUzF0Y3lJZ1BTQWlNVFF6TlRVNU5E YzFNakF3TUNJN0Nna2lkMlZpCkxXOXlaR1Z5TFd4cGJtVXRhWFJsYlMxcFpDSWdQU0FpTVRBd01EQXdNREF6TURBME5UWTNNaUk3Q2draQpZblp5Y3lJZ1BTQW lNUzR3SWpzS0NTSjFibWx4ZFdVdGRtVnVaRzl5TFdsa1pXNTBhV1pwWlhJaUlEMGcKSWtSRk16Y3lSREV5TFVNNE5qWXRORUV6UkMxQk16VTNMVFZETXpOR1Fq Z3pNa0ZFUXlJN0Nna2laWGh3CmFYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpDMXdjM1FpSUQwZ0lqSXdNVFV0TURZdE1qa2dNRGs2TXprNgpNVEVnUVcxbGNtbG pZUzlNYjNOZlFXNW5aV3hsY3lJN0Nna2lhWFJsYlMxcFpDSWdQU0FpTVRBeE16azIKTlRRMU5DSTdDZ2tpWlhod2FYSmxjeTFrWVhSbExXWnZjbTFoZEhSbFpD SWdQU0FpTWpBeE5TMHdOaTB5Ck9TQXhOam96T1RveE1TQkZkR012UjAxVUlqc0tDU0p3Y205a2RXTjBMV2xrSWlBOUlDSmpiMjB1YlhsaApjSEF1WVhCd0xtbH VZWEF1Y0dGemN5NXRiMjUwYUd4NWMzVmljMk55YVhCMGFXOXVJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsSWlBOUlDSXlNREUxTFRBMkxUSTVJREUyT2pNME9q RXhJRVYwWXk5SFRWUWlPd29KCkltOXlhV2RwYm1Gc0xYQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVFV0TURZdE1qa2dNVFk2TVRrNgpNVElnUlhSakwwZE 5WQ0k3Q2draVltbGtJaUE5SUNKamIyMHViWGxoY0hBdVlYQndJanNLQ1NKd2RYSmoKYUdGelpTMWtZWFJsTFhCemRDSWdQU0FpTWpBeE5TMHdOaTB5T1NBd09U b3pORG94TVNCQmJXVnlhV05oCkwweHZjMTlCYm1kbGJHVnpJanNLQ1NKeGRXRnVkR2wwZVNJZ1BTQWlNU0k3Q24wPSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbm Rib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0= iTunes “Simple IDs to query purchases you said?” Receipt data
  13. { "signature" = "ArGue1kWXahnTFiSxJV8EmqAiPyQhYAJS61C4MaS6nqfGICxQ6JjDc/R+s7uIFAEE7T0NTHdr08Ax7GFiSuBywAqU30FkF GBMDTKunup9Bo5xzSWmSMVeZkAQQW85jNCShhGN7SshNMjLgsThegwRw8xMi3RaR1jJ139e+Lv92O/AAADVzCCA1MwggI7oAMCAQICCBup4+PA hm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW 9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXD TE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTET MBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe 6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZ

    hS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdI wQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJV xVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGT OvA=="; "purchase-info"=" ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTI5IDA5OjE5OjEyIEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInB1cmNoY XNlLWRhdGUtbXMiID0gIjE0MzU1OTU2NTEwMDAiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICI4MDZlZDU0NzBmZjVkM TYwODVjODcwMzcyZWY1Y2I4MTFkY2Q2NjY1IjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3NTg0MiI7CgkiZXhw aXJlcy1kYXRlIiA9ICIxNDM1NTk1OTUxMDAwIjsKCSJ0cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3ODg1NiI7Cgkib3JpZ2luYWwtcHV yY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNTU5NDc1MjAwMCI7Cgkid2ViLW9yZGVyLWxpbmUtaXRlbS1pZCIgPSAiMTAwMDAwMDAzMDA0NTY3MiI7Cg kiYnZycyIgPSAiMS4wIjsKCSJ1bmlxdWUtdmVuZG9yLWlkZW50aWZpZXIiID0gIkRFMzcyRDEyLUM4NjYtNEEzRC1BMzU3LTVDMzNGQjgzMkFEQ yI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZC1wc3QiID0gIjIwMTUtMDYtMjkgMDk6Mzk6MTEgQW1lcmljYS9Mb3NfQW5nZWxlcyI7CgkiaXRl bS1pZCIgPSAiMTAxMzk2NTQ1NCI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZCIgPSAiMjAxNS0wNi0yOSAxNjozOToxMSBFdGMvR01UIjsKCSJ wcm9kdWN0LWlkIiA9ICJjb20ubXlhcHAuYXBwLmluYXAucGFzcy5tb250aGx5c3Vic2NyaXB0aW9uIjsKCSJwdXJjaGFzZS1kYXRlIiA9ICIyMD E1LTA2LTI5IDE2OjM0OjExIEV0Yy9HTVQiOwoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUiID0gIjIwMTUtMDYtMjkgMTY6MTk6MTIgRXRjL0dNV CI7CgkiYmlkIiA9ICJjb20ubXlhcHAuYXBwIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0yOSAwOTozNDoxMSBBbWVyaWNhL0xv c19BbmdlbGVzIjsKCSJxdWFudGl0eSIgPSAiMSI7Cn0="; "environment" = "Sandbox"; "pod" = "100"; "signing-status" = "0"; iTunes "purchase-info"
  14. { "signature" = "ArGue1kWXahnTFiSxJV8EmqAiPyQhYAJS61C4MaS6nqfGICxQ6JjDc/R+s7uIFAEE7T0NTHdr08Ax7GFiSuBywAqU30FkF GBMDTKunup9Bo5xzSWmSMVeZkAQQW85jNCShhGN7SshNMjLgsThegwRw8xMi3RaR1jJ139e+Lv92O/AAADVzCCA1MwggI7oAMCAQICCBup4+PA hm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW 9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXD TE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTET MBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe 6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZ

    hS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdI wQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJV xVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGT OvA=="; "purchase-info"=" ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTI5IDA5OjE5OjEyIEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInB1cmNoY XNlLWRhdGUtbXMiID0gIjE0MzU1OTU2NTEwMDAiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICI4MDZlZDU0NzBmZjVkM TYwODVjODcwMzcyZWY1Y2I4MTFkY2Q2NjY1IjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3NTg0MiI7CgkiZXhw aXJlcy1kYXRlIiA9ICIxNDM1NTk1OTUxMDAwIjsKCSJ0cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE2MTI3ODg1NiI7Cgkib3JpZ2luYWwtcHV yY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNTU5NDc1MjAwMCI7Cgkid2ViLW9yZGVyLWxpbmUtaXRlbS1pZCIgPSAiMTAwMDAwMDAzMDA0NTY3MiI7Cg kiYnZycyIgPSAiMS4wIjsKCSJ1bmlxdWUtdmVuZG9yLWlkZW50aWZpZXIiID0gIkRFMzcyRDEyLUM4NjYtNEEzRC1BMzU3LTVDMzNGQjgzMkFEQ yI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZC1wc3QiID0gIjIwMTUtMDYtMjkgMDk6Mzk6MTEgQW1lcmljYS9Mb3NfQW5nZWxlcyI7CgkiaXRl bS1pZCIgPSAiMTAxMzk2NTQ1NCI7CgkiZXhwaXJlcy1kYXRlLWZvcm1hdHRlZCIgPSAiMjAxNS0wNi0yOSAxNjozOToxMSBFdGMvR01UIjsKCSJ wcm9kdWN0LWlkIiA9ICJjb20ubXlhcHAuYXBwLmluYXAucGFzcy5tb250aGx5c3Vic2NyaXB0aW9uIjsKCSJwdXJjaGFzZS1kYXRlIiA9ICIyMD E1LTA2LTI5IDE2OjM0OjExIEV0Yy9HTVQiOwoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUiID0gIjIwMTUtMDYtMjkgMTY6MTk6MTIgRXRjL0dNV CI7CgkiYmlkIiA9ICJjb20ubXlhcHAuYXBwIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0yOSAwOTozNDoxMSBBbWVyaWNhL0xv c19BbmdlbGVzIjsKCSJxdWFudGl0eSIgPSAiMSI7Cn0="; "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
  15. { "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
  16. iTunes Trap #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> }
  17. iTunes Trap #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 -l 30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes] 2>&1 | logger -t check_subscriptions_iap
  18. iTunes Trap #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
  19. iTunes Trap #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)
  20. Our backend Google Play token token package name subscription id

    token subscription id package name response with subscription data
  21. Google Play “Better late than never” All very nice at

    first look GET https://www.googleapis.com/androidpublisher/v2/applications/package_name/ purchases/subscriptions/subscription_id/tokens/token Package name: com.myapp.android Subscription id: monthly_sub Token: lppdmbnkljkkgcldbinmhmji.AO-J1OyBx_FJSc6_fZwPnb9urd6u3jOJdaPmonghNlWcFlqG 9hLAIphJia8ETGqY6bIZJNzLVKm226pCc91DjvRPkipLHbhh5IEHniNGNj5yDxnXejuSvd-1wXA-z2HV h49wQAe4fFKs9uRA53TMxHWPLTmMGO5gpB 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)
  22. 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
  23. 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
  24. Google Play Developer payload to the rescue HTTP/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 }
  25. Trap #1: Order IDs (What the users will send to

    your Support team) Base Merchant Order number + recurrence GPA.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
  26. Our backend Google Play base order number token token subscription

    id package name token package name subscription id base order number response with subscription data
  27. 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
  28. Trap #2: Actions outside the app Google Play We already

    know this one! Detect cancellations and renewals { ... "expiryTimeMillis": "1457090213000", "autoRenewing": false, "cancelReason": 0, ... } No reactivations outside \o/ ~ $ crontab -l 30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes, google] 2>&1 | logger -t check_subscriptions_iap
  29. Trap #3: false renewals with payment problems Google Play {

    ... "expiryTimeMillis": "1457090213000", "autoRenewing": true, "paymentState": 0, ... } Get updated every ~24 hours
  30. 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
  31. user ID receipt ID Our backend Amazon RVS user ID

    receipt ID shared secret receipt ID user ID response with subscription data
  32. 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
  33. 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)
  34. 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
  35. 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
  36. Trap #1: Actions outside the app Cancellations: detect expired subscriptions

    and revoke Amazon Appstore Yep, same story HTTP/1.1 200 { ... "cancelDate":1463407610000, ... }
  37. Trap #1: Actions outside the app Cancellations: detect expired subscriptions

    and revoke Amazon Appstore Yep, same story HTTP/1.1 200 { ... "cancelDate":1463407610000, ... } Again, we only notice when it has expired ¬¬ ~ $ crontab -l 30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes, google,amazon] 2>&1 | logger -t check_subscriptions_iap
  38. Trap #1: Actions outside the app Cancellations: detect expired subscriptions

    and revoke Amazon Appstore Yep, same story HTTP/1.1 200 { ... "cancelDate":1463407610000, ... } Again, we only notice when it has expired ¬¬ ~ $ crontab -l 30 10 * * * cd /data/myapp/current && rake myapp:check_subscriptions[itunes, google,amazon] 2>&1 | logger -t check_subscriptions_iap