j3k0 / cordova-plugin-purchase

In-App Purchase for Cordova on iOS, Android and Windows
https://purchase.cordova.fovea.cc
1.29k stars 529 forks source link

Has anybody really got subscriptions working? #215

Closed rwillett closed 8 years ago

rwillett commented 8 years ago

After two days of pulling our hair out we still cannot get simple non-auto-renewable subscriptions working correctly.

Short question

Has anybody got subscriptions properly working?

As above but with more background and feeling

We have created a dummy weekly subscription on iTunesConnect and can register and buy this with our app. We can verify with our own web server that we have a correct Apple Transaction Receipt and Store Receipt. This part of the purchasing seems to be OK.

The problems occur afterwards.

Issues we see are:

How do we determine that a subscription is current or has expired?

We have never seen the product.expired() called yet.

We can try and query our own validation server but there's no obvious function to call, you can only use product.verify() when product.state == APPROVED. You can't seem to call any other function.

We can try and look inside the product itself to work out what to try and verify, e.g. we could use the transaction receipt to see if its still valid. But hacking our own function calls to our validation server seems wrong, we should be using the plugin functionality to maintain the internal state model.

We always get an error message saying that "You've Already Purchased This Subscription".

If we press Buy in the modal window (I know its not really), it goes ahead and makes a purchase and checks it against the validation server. If we press Cancel, then the product.State gets changed from OWNED to VALID rather than staying OWNED. This seems like an error to us,

Also if you try to Buy and simply cancel the order the product,state goes from OWNED to VALID rather than staying as OWNED. This looks like an error in the state model handling.

The already purchased problem seems like its a common issue. Anybody resolved it?

Restarting the app

If we stop and restart the app, then nothing calls our validation server to check anything. This really ties into point 1. How do we know what we have subscribed to? I think we can probably pick up one of the receipts and use that as a unique identifier against our validation server but that still doesn't explain how to call the validation server reliably.

Our worry now after two days is that we are having to put more and more workarounds into code to try and fix things or to work out what is happening, especially with the state model. We're starting to write our own app store as we fix things and thats really not sensible, Our own experience is that manually updating internal state models is a really, really dumb idea and that we should do this work through exposed function calls (or the equivalent).

So my question is:

Has anybody actually got subscriptions properly working? If you have and can share some of your insights into the above that would be great. Even if you can't help, simply saying that yes subscriptions work fine and you just need to read more would also help because at this moment we're banging our head against a wall.

Thanks for reading.

Rob

j3k0 commented 8 years ago

Subscription works with the demo app, without workarounds. JS here: https://github.com/Fovea/cordova-plugin-purchase-demo/blob/master/www/js/index.js

rwillett commented 8 years ago

Thats what we are using already.

However we will go back and build an app with that (modified to use our data) and see what happens.

Thanks

Rob.

rwillett commented 8 years ago

We have gone back to basics and are now using the demo app from here. The only things we have changed in the demo app are the URL for the validator and the name of the subscription inApp purchase.

1 - We've built it into a Cordova app and it loads correctly.

2 - We've built the inApp purchases on iTunesConnect with just a single item to be purchased, a non-renewing subscription.

screen shot 2015-06-12 at 15 22 48

3 - We've updated our web server to take the transaction receipt, make it into a sha512_hex string (just to make it readable really), and when the verification function calls the web server, we check to see if the transaction receipt exists. If the transaction receipt doesn't exist in the database, we add the transaction receipt to the database and set the expiration to five mins after the insertion. Simple SQL.

We return the following if the receipt has either been inserted into the database OR is currently valid in the database

{
    'json' => {
                'ok' => 1,
                 'data' => {
                         'appStoreReceipt_sha512' => '<<long SHA512 string>> ',
                         'transactionReceipt_sha512' => '<<different long SHA512 string>>'
                       }
           },
     'text' => 'ok',
     'status' => 200
};

This is Perl and the status is the HTTP code, the text is OK and the JSON return package is as described in #210. Note that Perl does not have true or false. So we return 1 and 0 instead.

If the receipt has expired in our validation database, currently five mins to allow the iTunes sandbox subscription to expire (is this the case?) we return

{
         'status' => 200,
         'json' => {
                     'ok' => 0,
                     'data' => {
                                 'error' => {
                                              'message' => 'Expired'
                                            },
                                 'code' => 6778003
                               }
                   },
         'text' => 'ok'
};

4 - We can buy a subscription, we can see the validation server be contacted and can see that the database is updated with a single entry. We've shortened the first column to make it easier to read.

screen shot 2015-06-12 at 15 41 12

5 - We leave the application along for 20 mins. We then refresh the app using the "Refresh Purchase" button, the web server debugging shows the database being queried and the Expired response being sent

Executing select Expiration,datetime('now' , 'localtime') from Receipts where Receipt = 'a6f5fceca3de52ecfb86a8f26ae1fcded1579d6da15fa61155eab7e684edd008694d5ace3e766a735e16720fd5ac91d22f97fec7ef3c5f7fe3e334cf11532dc1'

Is '2015-06-12 15:24:32' > 2015-06-12 15:40:04' ?

Returning $VAR1 = {
         'json' => {
                     'ok' => 0,
                     'data' => {
                                 'error' => {
                                              'message' => 'Expired'
                                            },
                                 'code' => 6778003
                               }
                   },
         'text' => 'ok',
         'status' => 200
       };

6 - The app makes the Subscription 1 item on the test app web page available to purchase and states that we are not subscribed. This looks correct. I don't have a screenshot.

7 - I then select "Subscription 1", which is our only non-renewable subscription InApp purchase, to repurchase it. A Confirmation dialogue pops up asking if I want to buy, I click on Yes and get "You've already Purchased This Subscription"

8 - As far as we know we have expired the subscription in iTunesConnect, the app thinks we have expired the subscription and states we have but we still have the error that it thinks we have already purchased the subscription. This error message comes back so quickly I don't believe its going back to the Apple server to validate this. If its not doing a check with the iTunes server, something local is not being set or the state model is not being updated by something we have done.

9 - The only thing home grown now is the validation server, it is returning what we understand the format for Expired to be, the demo App certainly changes state to unsubscribed when we do, we leave the timeout on the iTunes server for over five mins (is this correct?). Is there another field that needs to be returned from the validation server?

Any suggestions?

Thanks,

Rob

j3k0 commented 8 years ago

Hey, Rob.

This is not an error returned by the plugin, but by the StoreKit SDK.

What if you kill and restart the app? Works? I remember the StoreKit SDK does a lot of caching and may only be refreshing receipts on app restart... If not, try phone reboot...

I checked, calling "refresh" forces a full refresh of the apps' receipts (in turn, an update of products internal state). But who knows, StoreKit may be deciding that it's too early to be hitting Apple's server yet (?)

rwillett commented 8 years ago

I will check and see. From memory we still get the alert but I will have to check later today.

rwillett commented 8 years ago

Jean,

1 - Left it for 30 mins, deleted app, reinstalled app, tried to buy. when the demo app thinks its available, -> Still gets the "You have already purchased this Subscription" error.

Cancelling "You have already purchased this Subscription" dialogue box this puts me back to the beginning and the demo app says I am not subscribed.

2 - Deleted app - Rebooted iPhone - reinstalled app - Same error message "You have already purchased this Subscription"

3 - iPhone asked me to login. Did so and a nonconsumable purchase was updated in the demo app. odd!. Tried to buy the subscription again and same error message "You have already purchased this Subscription" pops up. Decided to buy it and see if we can get things to time out :)

4 - Left for 20 minutes. tried to buy subscription, asked to sign into iTunes Store, pressed confirm to buy and got same error message "You have already purchased this Subscription".

I will leave it overnight but it doesn't look good. Something somewhere thinks that the subscription is still valid. There is nothing in the iTunesConnect to put a time limit on non-renewing subscriptions (AFAIK), rebooting, reinstalling doesn't make a difference, so I am assuming that something from Apple in the receipt itself is telling Storekit that the subscription is still active.

I would still like to know if anybody got non-renewing subscriptions working? If there is a magic way to do this, let me know and we can discuss financial terms. I'm serious, I need to get this working so am prepared to contract with somebody who can fix this.

Thanks,

Rob

rwillett commented 8 years ago

I now have a reproducible error pattern that is easy to demonstrate.

1 - Create a new non-renewing subscription in iTunesConnect.

2 - Update the demo app to use the new non-renewing subscription.

3 - Compile and install the app on the iPhone.

4 - Wait for the new subscription to turn up in the demo app.

5 - Buy the subscription. This sends an update to the validation server. I store the transaction receipt (not the app store receipt as thats different each time). I set the database to expire the subscription in five mins.

6 - Wait six mins.

7 - Hit the refresh button on the demo app. This downloads a load of stuff from the App Store AND makers a call to the verification server. My verification server returns expired when a query is made from the demo app.

8 - The subscription is now available to purchase in the app.

9 - Buy the subscription again and this time you will get the "You have already purchased This Subscription" error.

The first time you buy its fine, the second time you buy you have to refresh from the app store. My view and thats all it is, is that something in the App Store receipts has information about the old subscription and thats keeping the subscription 'alive' even through we have expired it in our validation server.

I've recreated this issue twice now by creating a new subscription in the sandboxed App Store.

I'm unsure if this is a bug in the plugin, a misunderstanding of how long things are alive for in the sandbox, the wrong information being sent back from the verification server or some combination of all three or something else altogether different.

I'm going to read more on the transaction and App Store receipts and see if I can break them open to see what is inside them.

Anybody else have any ideas?

Rob

j3k0 commented 8 years ago

Sorry, I was checking a bit more.

Do you get this? "Already Purchased.." with a "Buy" button. Or no "Buy" button?

already purchased

rwillett commented 8 years ago

I get it with the "buy" button.

I was wondering if this was an error just in the the sandbox environment.

j3k0 commented 8 years ago

This is not an error, just the normal StoreKit workflow (with its typically bad user experience).

rwillett commented 8 years ago

Jean,

Whilst this may not be an 'error', its very confusing for the user. Also I've never seen any other app tell me I had already purchased a subscription.

  1. So it appears that you are getting the same window popping up and that this appears to be a reproducible problem? Is this the case? It would be good to know that you are now getting it so I can rule out the problem as something solely in our code. Also does this happen in the production environment? I can't test that as I'm some weeks away from having an app that will pass review. If its something that only happens in Sandbox I can ignore it.
  2. My thinking around this is that the information on the subscriptions must be held in on the two receipts that are returned. Since you aren't displaying the error, something must be, and the only way that information comes in is through the receipts (I think). Whats your view?
  3. I am going to try and unpack the receipts to see what is in them. It may be that the validation server needs to do this anyway to check the receipt and to see if the subscription really has expired and if its not expired then send back the right values. I will also try and do a check of the receipts against Apples sandbox server and see what that brings back. I need to do some reading first though.

My overall feeling is that I can't ask the user to ignore the error window that pops up as users will not want to buy when they say they already have the subscription.

I also cannot believe that I am the first person to find this out. I was really hoping that somebody would chip in and say they have it or don't have it.

Rob

j3k0 commented 8 years ago

AFAIK, this is the workflow for non-renewable subscription on iOS, not a bug or an error. This dialog is the system renewal dialog, you can't get around it.

See here and here

rwillett commented 8 years ago

That has to be one of the worlds worst informational messages :)

If the subscription has expired then it should say so. I'm still going to dig in a bit further to find out more about the receipts, there may be something in there.

I'll also create a renewable subscription and see how that goes.

Thanks,

Rob.

rwillett commented 8 years ago

I have had a dig around in the transaction receipts which was rather interesting.

I'll log what I have done here as it may be useful to somebody else.

The transactionReceipt is returned by the Sandbox as a record of your purchase. It is not the same as the appStoreReceipt which is in a similar format but has far more information within it. I haven't looked at appStoreReceipts in any detail but still may do.

I am not an expert on cryptographic code so some of what I say here may be rubbish. Caveat Emptor.

I took a copy of the transactionReceipt as returned by the system after purchasing a non renewable receipt.

The transactionReceipt is base64 encoded data and is easy to break apart. This was all done on a Mac but would work just as well on a UNIX box. Windows? No idea!

Here's source data, put it in a file called t1

ewoJInNpZ25hdHVyZSIgPSAiQW55bDFSOWRkbzBxN1Z6blFNUE0zbkFnb3RydmhjRGU4cFRvNDg1WE5Xb211SzNHMDVSMnFJdHJoVFpzQjNqdnkvazlnTUtoQitZajR4YlExa0lzYXExSG85T0RkOVgvMjZqNjR1VVllaUdFYjZkOFdmMi9OUjliTDdMbWJmSUJUL0tPVTRxVFFQTFk1ZmtUKysrR3B4dUdYR0FtdUlhKzQxV0dDYUIyNi93UUFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RXpNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdUxnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25iMzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQTFVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hRd3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tkluclp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSjNwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQU0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBMkxURXpJREF3T2pVME9qVTBJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeUlpQTlJQ0l6TW1VMlltVXhaV1UxTXpBMk16Wm1ZVGRtT1Rnek56VmpPV0k1WW1Sa1pqQTVNRGRrTm1aa0lqc0tDU0p2Y21sbmFXNWhiQzEwY21GdWMyRmpkR2x2YmkxcFpDSWdQU0FpTVRBd01EQXdNREUxT1RFMU9ETTRNaUk3Q2draVluWnljeUlnUFNBaU1DNHdMakVpT3dvSkluUnlZVzV6WVdOMGFXOXVMV2xrSWlBOUlDSXhNREF3TURBd01UVTVNVFU0TXpneUlqc0tDU0p4ZFdGdWRHbDBlU0lnUFNBaU1TSTdDZ2tpYjNKcFoybHVZV3d0Y0hWeVkyaGhjMlV0WkdGMFpTMXRjeUlnUFNBaU1UUXpOREU0TWpBNU5ESXdOeUk3Q2draWRXNXBjWFZsTFhabGJtUnZjaTFwWkdWdWRHbG1hV1Z5SWlBOUlDSkNRa1JHUVRrMVJDMDROVUkwTFRSRk16VXRPVVpCUmkwd01qRTVOek5DUWtZNFJEVWlPd29KSW5CeWIyUjFZM1F0YVdRaUlEMGdJbXh2Ym1SdmJpNXFZVzFpZFhOMFpYSXVkR1Z6ZEhOMVluTmpjbWx3ZEdsdmJpNXpkV0p6WTNKcGNIUnBiMjR6SWpzS0NTSnBkR1Z0TFdsa0lpQTlJQ0l4TURBMU9EUTNNalUzSWpzS0NTSmlhV1FpSUQwZ0lteHZibVJ2Ymk1cVlXMWlkWE4wWlhJdWRHVnpkSE4xWW5OamNtbHdkR2x2YmlJN0Nna2ljSFZ5WTJoaGMyVXRaR0YwWlMxdGN5SWdQU0FpTVRRek5ERTRNakE1TkRJd055STdDZ2tpY0hWeVkyaGhjMlV0WkdGMFpTSWdQU0FpTWpBeE5TMHdOaTB4TXlBd056bzFORG8xTkNCRmRHTXZSMDFVSWpzS0NTSndkWEpqYUdGelpTMWtZWFJsTFhCemRDSWdQU0FpTWpBeE5TMHdOaTB4TXlBd01EbzFORG8xTkNCQmJXVnlhV05oTDB4dmMxOUJibWRsYkdWeklqc0tDU0p2Y21sbmFXNWhiQzF3ZFhKamFHRnpaUzFrWVhSbElpQTlJQ0l5TURFMUxUQTJMVEV6SURBM09qVTBPalUwSUVWMFl5OUhUVlFpT3dwOSI7CgkiZW52aXJvbm1lbnQiID0gIlNhbmRib3giOwoJInBvZCIgPSAiMTAwIjsKCSJzaWduaW5nLXN0YXR1cyIgPSAiMCI7Cn0=

You can decode base64 with

base64 --decode < t1 > t2

You can see the output of the file t2 here

{
    "signature" = "Anyl1R9ddo0q7VznQMPM3nAgotrvhcDe8pTo485XNWomuK3G05R2qItrhTZsB3jvy/k9gMKhB+Yj4xbQ1kIsaq1Ho9ODd9X/26j64uUYeiGEb6d8Wf2/NR9bL7LmbfIBT/KOU4qTQPLY5fkT+++GpxuGXGAmuIa+41WGCaB26/wQAAADVzCCA1MwggI7oAMCAQICCBup4+PAhm/LMA0GCSqGSIb3DQEBBQUAMH8xCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEzMDEGA1UEAwwqQXBwbGUgaVR1bmVzIFN0b3JlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE0MDYwNzAwMDIyMVoXDTE2MDUxODE4MzEzMFowZDEjMCEGA1UEAwwaUHVyY2hhc2VSZWNlaXB0Q2VydGlmaWNhdGUxGzAZBgNVBAsMEkFwcGxlIGlUdW5lcyBTdG9yZTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMmTEuLgjimLwRJxy1oEf0esUNDVEIe6wDsnnal14hNBt1v195X6n93YO7gi3orPSux9D554SkMp+Sayg84lTc362UtmYLpWnb34nqyGx9KBVTy5OGV4ljE1OwC+oTnRM+QLRCmeNxMbPZhS47T+eZtDEhVB9usk3+JM2Cogfwo7AgMBAAGjcjBwMB0GA1UdDgQWBBSJaEeNuq9Df6ZfN68Fe+I2u22ssDAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFDYd6OKdgtIBGLUyaw7XQwuRWEM6MA4GA1UdDwEB/wQEAwIHgDAQBgoqhkiG92NkBgUBBAIFADANBgkqhkiG9w0BAQUFAAOCAQEAeaJV2U51rxfcqAAe5C2/fEW8KUl4iO4lMuta7N6XzP1pZIz1NkkCtIIweyNj5URYHK+HjRKSU9RLguNl0nkfxqObiMckwRudKSq69NInrZyCD66R4K77nb9lMTABSSYlsKt8oNtlhgR/1kjSSRQcHktsDcSiQGKMdkSlp4AyXf7vnHPBe4yCwYV2PpSN04kboiJ3pBlxsGwV/ZlL26M2ueYHKYCuXhdqFwxVgm52h3oeJOOt/vY4EcQq7eqHm6m03Z9b7PRzYM2KGXHDmOMk7vDpeMVlLDPSGYz1+U3sDxJzebSpbaJmT7imzUKfggEY7xxf4czfH0yj5wNzSGTOvQ==";
    "purchase-info" = "ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTEzIDAwOjU0OjU0IEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICIzMmU2YmUxZWU1MzA2MzZmYTdmOTgzNzVjOWI5YmRkZjA5MDdkNmZkIjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE1OTE1ODM4MiI7CgkiYnZycyIgPSAiMC4wLjEiOwoJInRyYW5zYWN0aW9uLWlkIiA9ICIxMDAwMDAwMTU5MTU4MzgyIjsKCSJxdWFudGl0eSIgPSAiMSI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNDE4MjA5NDIwNyI7CgkidW5pcXVlLXZlbmRvci1pZGVudGlmaWVyIiA9ICJCQkRGQTk1RC04NUI0LTRFMzUtOUZBRi0wMjE5NzNCQkY4RDUiOwoJInByb2R1Y3QtaWQiID0gImxvbmRvbi5qYW1idXN0ZXIudGVzdHN1YnNjcmlwdGlvbi5zdWJzY3JpcHRpb24zIjsKCSJpdGVtLWlkIiA9ICIxMDA1ODQ3MjU3IjsKCSJiaWQiID0gImxvbmRvbi5qYW1idXN0ZXIudGVzdHN1YnNjcmlwdGlvbiI7CgkicHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNDE4MjA5NDIwNyI7CgkicHVyY2hhc2UtZGF0ZSIgPSAiMjAxNS0wNi0xMyAwNzo1NDo1NCBFdGMvR01UIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0xMyAwMDo1NDo1NCBBbWVyaWNhL0xvc19BbmdlbGVzIjsKCSJvcmlnaW5hbC1wdXJjaGFzZS1kYXRlIiA9ICIyMDE1LTA2LTEzIDA3OjU0OjU0IEV0Yy9HTVQiOwp9";
    "environment" = "Sandbox";
    "pod" = "100";
    "signing-status" = "0";
}

If you manually extract the purchase-info this is also a base64 encoded string. Put it in a file called p1

cat p1 && echo
ewoJIm9yaWdpbmFsLXB1cmNoYXNlLWRhdGUtcHN0IiA9ICIyMDE1LTA2LTEzIDAwOjU0OjU0IEFtZXJpY2EvTG9zX0FuZ2VsZXMiOwoJInVuaXF1ZS1pZGVudGlmaWVyIiA9ICIzMmU2YmUxZWU1MzA2MzZmYTdmOTgzNzVjOWI5YmRkZjA5MDdkNmZkIjsKCSJvcmlnaW5hbC10cmFuc2FjdGlvbi1pZCIgPSAiMTAwMDAwMDE1OTE1ODM4MiI7CgkiYnZycyIgPSAiMC4wLjEiOwoJInRyYW5zYWN0aW9uLWlkIiA9ICIxMDAwMDAwMTU5MTU4MzgyIjsKCSJxdWFudGl0eSIgPSAiMSI7Cgkib3JpZ2luYWwtcHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNDE4MjA5NDIwNyI7CgkidW5pcXVlLXZlbmRvci1pZGVudGlmaWVyIiA9ICJCQkRGQTk1RC04NUI0LTRFMzUtOUZBRi0wMjE5NzNCQkY4RDUiOwoJInByb2R1Y3QtaWQiID0gImxvbmRvbi5qYW1idXN0ZXIudGVzdHN1YnNjcmlwdGlvbi5zdWJzY3JpcHRpb24zIjsKCSJpdGVtLWlkIiA9ICIxMDA1ODQ3MjU3IjsKCSJiaWQiID0gImxvbmRvbi5qYW1idXN0ZXIudGVzdHN1YnNjcmlwdGlvbiI7CgkicHVyY2hhc2UtZGF0ZS1tcyIgPSAiMTQzNDE4MjA5NDIwNyI7CgkicHVyY2hhc2UtZGF0ZSIgPSAiMjAxNS0wNi0xMyAwNzo1NDo1NCBFdGMvR01UIjsKCSJwdXJjaGFzZS1kYXRlLXBzdCIgPSAiMjAxNS0wNi0xMyAwMDo1NDo1NCBBbWVyaWNhL0xvc19BbmdlbGVzIjsKCSJvcmlnaW5hbC1wdXJjaGFzZS1kYXRlIiA9ICIyMDE1LTA2LTEzIDA3OjU0OjU0IEV0Yy9HTVQiOwp9

The echo at the end of the command is to add a newline after the end of the file output just so it displays nicely on the screen. There is NO newline in the file p1itself.

This is also a base64 encoded string which we can open up

base64 --decode < p1
{
    "original-purchase-date-pst" = "2015-06-13 00:54:54 America/Los_Angeles";
    "unique-identifier" = "32e6be1ee530636fa7f98375c9b9bddf0907d6fd";
    "original-transaction-id" = "1000000159158382";
    "bvrs" = "0.0.1";
    "transaction-id" = "1000000159158382";
    "quantity" = "1";
    "original-purchase-date-ms" = "1434182094207";
    "unique-vendor-identifier" = "BBDFA95D-85B4-4E35-9FAF-021973BBF8D5";
    "product-id" = "london.jambuster.testsubscription.subscription3";
    "item-id" = "1005847257";
    "bid" = "london.jambuster.testsubscription";
    "purchase-date-ms" = "1434182094207";
    "purchase-date" = "2015-06-13 07:54:54 Etc/GMT";
    "purchase-date-pst" = "2015-06-13 00:54:54 America/Los_Angeles";
    "original-purchase-date" = "2015-06-13 07:54:54 Etc/GMT";
}

That was really rather easy indeed. We can see from the contents of the this that purchase_info does not have an end subscription time at all, which is what we expect.

I also did the same for a auto-renewing subscription but the final output of that is different

base64 --decode < p1
{
    "original-purchase-date-pst" = "2015-06-13 00:35:28 America/Los_Angeles";
    "purchase-date-ms" = "1434180928000";
    "unique-identifier" = "32e6be1ee530636fa7f98375c9b9bddf0907d6fd";
    "original-transaction-id" = "1000000159158052";
    "expires-date" = "1434181108000";
    "transaction-id" = "1000000159158081";
    "original-purchase-date-ms" = "1434180928000";
    "web-order-line-item-id" = "1000000029931467";
    "bvrs" = "0.0.1";
    "unique-vendor-identifier" = "XXXXXXXXXXXXXXXXXXXXXXXX";
    "expires-date-formatted-pst" = "2015-06-13 00:38:28 America/Los_Angeles";
    "item-id" = "1006053699";
    "expires-date-formatted" = "2015-06-13 07:38:28 Etc/GMT";
    "product-id" = "london.jambuster.testsubscription.subscription4";
    "purchase-date" = "2015-06-13 07:35:28 Etc/GMT";
    "original-purchase-date" = "2015-06-13 07:35:28 Etc/GMT";
    "bid" = "london.jambuster.testsubscription";
    "purchase-date-pst" = "2015-06-13 00:35:28 America/Los_Angeles";
    "quantity" = "1";
}

That certainly has some expiration information within it. I have changed some of the values over to protect the innocent. I don't believe there is anything sensitive in here though as its publicly transmitted over the internet.

The expires-date and purchase-date-ms are UNIX timestamps with milliseconds appended. Simply divide by 1000 and convert from epoch time to local time.

So what has this shown?

  1. Its quite easy to extract out the right information from transactionReceipts for auto-renewing and non-autorenewing subscriptions.
  2. There is no information in the transactionReceipt for non-autorenewing subscriptions.
  3. The message presented by Apple Storekit is crap.
  4. The information in the auto-renewing subscription regarding expiration is correct for the sandbox. It's three mins in the future and this can be easily extracted and shoved in a database.

    "purchase-date" = "2015-06-13 07:35:28 Etc/GMT";
    "expires-date-formatted" = "2015-06-13 07:38:28 Etc/GMT";
  5. I looked at the appStoreReceipt and is a bit similar but you need to extract information using openssl. You need to unpack the appStoreReceipt using base64 and then use the output of that with
openssl pkcs7 -inform dem -in f2 -print_certs -text

The contents look like this. (I have shortened it for brevity)

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            18:59:43:21:72:74:9c:fc
        Signature Algorithm: sha1WithRSAEncryption
        Issuer: C=US, O=Apple Inc., OU=Apple Worldwide Developer Relations, CN=Apple Worldwide Developer Relations Certification Authority
        Validity
            Not Before: Nov 11 21:58:01 2010 GMT
            Not After : Nov 11 21:58:01 2015 GMT
        Subject: CN=Mac App Store Receipt Signing, OU=Apple Worldwide Developer Relations, O=Apple Inc., C=US
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
            RSA Public Key: (2048 bit)
                Modulus (2048 bit):
                    00:b6:93:c2:b7:0f:24:5e:ed:d2:34:48:e8:85:05:
                    e3:33:94:66:5b:e8:27:37:bf:7b:43:49:eb:f9:c9:
                    17:97:33:73:32:49:4a:c8:6f:68:29:14:b8:94:a6:
                    f4:65:4b:3b:47:d7:d1:2c:66:4b:b8:98:d9:bc:f5:
                    12:51:cb:e6:2f:a9:f4:b3:9f:1c:e8:28:fc:52:c0:
                    81:a2:cb:56:62:80:5a:a2:91:ae:4e:40:c3:7d:28:
                    2e:d7:d3:ed:4d:d9:ad:8a:fb:f2:67:48:ec:eb:79:
                    bd:02:6d:04:59:18:ff:8c:37:9f:8a:37:f1:62:ff:
                    bb:a2:03:50:87:0a:d5:92:e0:86:11:5e:23:46:f5:
                    e1:25:63:2b:a2:6a:8c:b2:10:b7:91:23:4d:9a:3f:
                    83:40:f2:64:09:5a:f7:8d:ae:56:5c:d4:f5:b4:6e:
                    03:1b:04:5d:2c:1b:af:00:99:17:d7:a5:fb:49:91:
                    ce:e2:a1:11:31:5e:19:01:c0:da:ce:50:83:5e:c8:
                    eb:49:3b:49:1a:2a:ea:e0:9f:bf:d2:46:49:9c:d8:
                    ab:a1:83:61:6c:0f:c1:fc:b3:ad:99:75:2a:fc:23:
                    9b:ef:22:08:eb:7b:59:14:11:9f:73:34:2d:e6:b9:
                    39:a6:3b:f7:e6:3e:ec:ca:a6:fb:ab:af:26:df:8f:
                    88:81
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier:
                keyid:88:27:17:09:A9:B6:18:60:8B:EC:EB:BA:F6:47:59:C5:52:54:A3:B7

            X509v3 CRL Distribution Points:
                URI:http://developer.apple.com/certificationauthority/wwdrca.crl

            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Subject Key Identifier:
                75:76:24:A2:6B:62:0C:97:34:A1:FA:4E:5E:08:0C:22:BF:73:EF:BE
            X509v3 Certificate Policies:
                Policy: 1.2.840.113635.100.5.6.1
                  User Notice:
                    Explicit Text: Reliance on this certificate by any party assumes acceptance of the then applicable standard terms and conditions of use, certificate policy and certification practice statements.
                  CPS: http://www.apple.com/appleca/

            1.2.840.113635.100.6.11.1:
                ..
    Signature Algorithm: sha1WithRSAEncryption
        a0:3b:f1:87:bc:69:b4:b7:83:7c:19:f4:9f:c4:02:64:df:02:
        c9:8e:31:73:cb:1c:3e:dc:26:07:8b:fd:9e:f3:ed:be:43:d6:
        8d:61:2d:e4:f0:dc:16:73:01:d6:34:a3:69:19:77:14:bf:b9:

We'll look at the error message and work out if we want to go to auto-renewing subscriptions,. That might be the best way to avoid these confusing error messages.

Thanks,

Rob

j3k0 commented 8 years ago

Thanks for the details.

Transactions receipts are deprecated in favor of appStoreReceipt since iOS 7. Still worked in iOS 8, didn't check iOS 9...

Some people use consumable in order to manage their non-renewing subscription, this way no crappy message.

rwillett commented 8 years ago

Rats. All that work for nothing :)

I will now run through the appstorereceipts to get the details.

How do people use consumables for non-renewing auto subscription and get them through approvals? Seems pretty clear that this shouldn't be approved or have I misunderstood?

j3k0 commented 8 years ago

Right, could go through or not.

rwillett commented 8 years ago

OK, lets close this off now. I have been playing with appstorereceipts but thats a lot trickier and more time is needed to decode them,

voneddy commented 8 years ago

Hey is there any further news on this? Did you set a working set up with expiries for non-auto-renewable subscriptions?

Got everything working over here but setting a product as expired so the user can purchase again. It always comes up with the '...this will be restored for free...' dialog...

Just for clarity- i can ascertain if a product is expired as far as im concerned, then make the product purchasable again but rather than bringing up a dialog to pay again i always get the 'restore for free' dialog, what can i do to mark a purchase as expired and let the user buy it again?

Thanks loving the plugin apart from this small issue

JM

jeffleus commented 8 years ago

Wow, @rwillett !!! This is the first place I have seen someone explain what to do w/ the appStoreReceipt after decoding from base64. I have been fiddling about w/ the transaction receipt since it is human-readable after a simple base64 decode. But, what to do w/ the data after the openssl command is run. There is still some work to decode it seems.

I am setting up InApp in my project. And, I finally have everything working w/ approval/verify using a remote validation server. But all I can do is log away the appStoreReceipt for future use. How do I actually inspect the receipts and get transaction/purchase details, like the information provided in the deprecated transactionReceipt?

rwillett commented 8 years ago

@voneddy, @jeffleus

Apologies for taking so long to come back. I completely missed the email alerting me to @voneddy updating this.

We decided not to proceed with using auto subscriptions, indeed we have made our app free to use.

It was so much hassle and never seemed to work correctly we looked at our business model again and decided that

  1. the 30% that Apple would gouge out of our fees,
  2. the fact that virtually nobody wants to pay for anything
  3. that nobody at all wants to have an auto-renewing subscription unless there company is paying for it
  4. we couldn't get the subscriptions to work reliably for us. We put a lot of effort in and it was consuming too much time.
  5. Our business model wants as many as possible end users even if they aren't paying anything
  6. people expect a gold plated support service even if they have only paid 69p.

Based on all this, we decided to abandon subscriptions all together and went with a free service but we will make money elsewhere.

We haven't touched this in six months so can't really help any more.

Rob