Posts

Post not yet marked as solved
2 Replies
993 Views
Hello, I'm new to apple ios developement and I'm running into a problem generating signature for subscription offers. I took the sample code from this documentation. I changed it a bit to adapt it to serverlessconst express = require('express'); const crypto = require('crypto'); const ECKey = require('ec-key'); const secp256k1 = require('secp256k1'); const uuidv4 = require('uuid/v4'); const KeyEncoder = require('key-encoder').KeyEncoder; exports.handler = (event, context, callback) => { const appBundleID = event.appBundleId; const productIdentifier = event.productIdentifier; const subscriptionOfferID = event.offerId; const applicationUsername = event.applicationUsername; const keyID = event.keyId; const keyString = event.keyString; /* The nonce is a lowercase random UUID string that ensures the payload is unique. The App Store checks the nonce when your app starts a transaction with SKPaymentQueue, to prevent replay attacks. */ const nonce = uuidv4(); /* Get the current time and create a UNIX epoch timestamp in milliseconds. The timestamp ensures the signature was generated recently. The App Store also uses this information help prevent replay attacks. */ const currentDate = new Date(); const timestamp = currentDate.getTime(); /* Combine the parameters into the payload string to be signed. These are the same parameters you provide in SKPaymentDiscount. */ const payload = appBundleID + '\u2063' + keyID + '\u2063' + productIdentifier + '\u2063' + subscriptionOfferID + '\u2063' + applicationUsername + '\u2063' + nonce + '\u2063' + timestamp; // Create an Elliptic Curve Digital Signature Algorithm (ECDSA) object using the private key. const key = new ECKey(keyString, 'pem'); // Set up the cryptographic format used to sign the key with the SHA-256 hashing algorithm. const cryptoSign = key.createSign('SHA256'); // Add the payload string to sign. cryptoSign.update(payload); /* The Node.js crypto library creates a DER-formatted binary value signature, and then base-64 encodes it to create the string that you will use in StoreKit. */ const signature = cryptoSign.sign('base64'); /* Check that the signature passes verification by using the ec-key library. The verification process is similar to creating the signature, except it uses 'createVerify' instead of 'createSign', and after updating it with the payload, it uses `verify` to pass in the signature and encoding, instead of `sign` to get the signature. This step is not required, but it's useful to check when implementing your signature code. This helps debug issues with signing before sending transactions to Apple. If verification succeeds, the next recommended testing step is attempting a purchase in the Sandbox environment. */ const verificationResult = key.createVerify('SHA256').update(payload).verify(signature, 'base64'); console.log("Verification result: " + verificationResult) // Send the response. callback(null, { 'key_identifier': keyID, 'nonce': nonce, 'ts': timestamp, 'base_64_signature': signature }); };I call it from another aws lambda and i pass all the values needed including the key. For testing purposes I have also tried adding the key as a multine string directly into the code const keyString = `my multiline key string`; in line 16 and still it didn't work. To add more context around error 12Error Domain=SKErrorDomain Code=12 "Cannot connect to iTunes Store" UserInfo={NSLocalizedDescription=Cannot connect to iTunes Store}we have created new keys already to rule out the possibility of the the key file being corrupted, the check on line 70 returns true all the time.So I'm trying to figure out if we are missing something here that I've gotten too tunnel vision to see in the code above.we have been testing with offers created for our debug app (not production app from the app store)I'm the backend engineer, the ios engineer has provided me the following code snippets on how they are sending the request to the storekit// Fetch the signature from server to be applied to the offer. // At this point we should know the user, the product the offer is for, and which offer you want to display. func prepareDiscountOffer(forProduct product: SKProduct, withDiscount discountId: String, completion: @escaping (SKPaymentDiscount?) -> Void) { // Get signature from backend, what if the user isn't signed in? self.fetchDiscountSignatureFromBackend(usernameHash: AWSCognitoManager.shared.userId ?? "", productIdentifier: product.productIdentifier, offerIdentifier: discountId) { result in if let discountOfferingSignature = result { // Create an SKPaymentDiscount to be used later when the user initiates the purchase // FIXME: convert timestamp: String to NSNumber guard let uuid = UUID.init(uuidString: discountOfferingSignature.nonce) else { return } let paymentDiscountOffer = SKPaymentDiscount(identifier: discountId, keyIdentifier: discountOfferingSignature.keyIdentifier, nonce: uuid, signature: discountOfferingSignature.base64Signature, timestamp: NSNumber(value: discountOfferingSignature.ts)) // generatedPaymentDiscounts.append(paymentDiscountOffer) completion(paymentDiscountOffer) } else { Analytics.track(event: .failedToGetOfferSignature, properties: ["Product Id": product.productIdentifier, "Offer Id": discountId]) } } // Makes a buy request with a subscription offer attached. public func buyDiscountedProduct(_ product: SKProduct, forUser usernameHash: String, withOffer discountOffer: SKPaymentDiscount) { // The original product being purchased. let payment = SKMutablePayment(product: product) // You must set applicationUsername to be the same as the one used to generate the signature. payment.applicationUsername = usernameHash // Add the offer to the payment. payment.paymentDiscount = discountOffer // Add the payment to the queue for purchase. SKPaymentQueue.default().add(payment) // what happens when we fail? what's the fallback? fallback endpoint, send cisco what things need to be regenerated // free month, for this user }Thanks in advance for your time, we are trying to figure out if the problem is the way we are generating the signature or the way we are sending it to the store kit.Many thanks !
Posted
by cisco5000.
Last updated
.
Post not yet marked as solved
6 Replies
2.7k Views
Hello I'm working on integrating renewable subscriptions statuses from apple into our cloud and I have found some inconsistent behavior that is making very hard to properly create a deserializer for the verification receipt endpoint. I am following the documentation found in here https://developer.apple.com/documentation/appstorereceipts/responsebodyWe are testing only renweables subscriptions right now, nothing else. According to the documentation latest_expired_receipt_info and latest_receipt_info should be an array of receipts, which is sometimes the case, however, sometimes the response from the verification receipt endpoint returns latest_expired_receipt_info and latest_receipt_info not as an array, but as a single receipt object.This is the first time I'm integrating with apple subscription, I'm using golang and this non consistent responses break the deserializer i have for the response, all this behavior is from the Sandbox environment using the endpoint https://sandbox.itunes.apple.com/verifyReceiptto verify the receipts and setting the exclude-old-transactions option to true in the request.I would appreacite it if anyone would point me to the right direction as of why the inconsistent response JSONs.Here is an example where the property is not an array{ "auto_renew_status": 0, "latest_expired_receipt_info": { "original_purchase_date_pst": "2019-11-12 10:01:07 America/Los_Angeles", "quantity": "1", "unique_vendor_identifier": "removed for this post", "bvrs": "removed for this post", "expires_date_formatted": "2019-11-12 22:41:18 Etc/GMT", "is_in_intro_offer_period": "false", "purchase_date_ms": "1573598178000", "expires_date_formatted_pst": "2019-11-12 14:41:18 America/Los_Angeles", "is_trial_period": "false", "item_id": "removed for this post", "unique_identifier": "removed for this post", "original_transaction_id": "removed for this post", "subscription_group_identifier": "removed for this post", "transaction_id": "removed for this post", "bid": "removed for this post", "web_order_line_item_id": "removed for this post", "purchase_date": "2019-11-12 22:36:18 Etc/GMT", "product_id": "removed for this post", "expires_date": "1573598478000", "original_purchase_date": "2019-11-12 18:01:07 Etc/GMT", "purchase_date_pst": "2019-11-12 14:36:18 America/Los_Angeles", "original_purchase_date_ms": "1573581667000" }, "status": 21006, "auto_renew_product_id": "removed for this post", "receipt": { "original_purchase_date_pst": "2019-11-12 10:01:07 America/Los_Angeles", "quantity": "1", "unique_vendor_identifier": "removed for this post", "bvrs": "removed for this post", "expires_date_formatted": "2019-11-12 22:41:18 Etc/GMT", "is_in_intro_offer_period": "false", "purchase_date_ms": "1573598178000", "expires_date_formatted_pst": "2019-11-12 14:41:18 America/Los_Angeles", "is_trial_period": "false", "item_id": "removed for this post", "unique_identifier": "removed for this post", "original_transaction_id": "removed for this post", "subscription_group_identifier": "removed for this post", "transaction_id": "removed for this post", "web_order_line_item_id": "removed for this post", "version_external_identifier": "0", "purchase_date": "2019-11-12 22:36:18 Etc/GMT", "product_id": "removed for this post", "expires_date": "1573598478000", "original_purchase_date": "2019-11-12 18:01:07 Etc/GMT", "purchase_date_pst": "2019-11-12 14:36:18 America/Los_Angeles", "bid": "removed for this post", "original_purchase_date_ms": "1573581667000" }, "expiration_intent": "1", "is_in_billing_retry_period": "0" }However, according to the documentation it should be always returned this way.{ "status": 0, "environment": "Production", "receipt": { "receipt_type": "Production", "adam_id": 0000000000,//changed for this post "app_item_id": 0000000000,//changed for this post "bundle_id": "com.foo.product", "application_version": "000", //changed for this post "download_id": 0000000000,//changed for this post "version_external_identifier": 0000000000,//changed for this post "receipt_creation_date": "2019-11-26 17:01:33 Etc/GMT", "receipt_creation_date_ms": "1574787693000", "receipt_creation_date_pst": "2019-11-26 09:01:33 America/Los_Angeles", "request_date": "2019-12-02 23:09:39 Etc/GMT", "request_date_ms": "1575328179029", "request_date_pst": "2019-12-02 15:09:39 America/Los_Angeles", "original_purchase_date": "2019-11-07 05:57:29 Etc/GMT", "original_purchase_date_ms": "1573106249000", "original_purchase_date_pst": "2019-11-06 21:57:29 America/Los_Angeles", "original_application_version": "99", "in_app": [ { "quantity": "1", "product_id": "com.foo.p", //changed for this post "transaction_id": " 0000000000",//changed for this post ", "original_transaction_id": " 0000000000",//changed for this post , "purchase_date": "2019-11-26 17:01:30 Etc/GMT", "purchase_date_ms": "1574787690000", "purchase_date_pst": "2019-11-26 09:01:30 America/Los_Angeles", "original_purchase_date": "2019-11-26 17:01:31 Etc/GMT", "original_purchase_date_ms": "1574787691000", "original_purchase_date_pst": "2019-11-26 09:01:31 America/Los_Angeles", "expires_date": "2020-11-26 17:01:30 Etc/GMT", "expires_date_ms": "1606410090000", "expires_date_pst": "2020-11-26 09:01:30 America/Los_Angeles", "web_order_line_item_id": "240000240409401", "is_trial_period": "false", "is_in_intro_offer_period": "false" } ] }, "latest_receipt_info": [ { "quantity": "1", "product_id": "com.foo.prod", //changed for this post "transaction_id": "0000000000",//changed for this post "original_transaction_id": "0000000000",//changed for this post "purchase_date": "2019-11-26 17:01:30 Etc/GMT", "purchase_date_ms": "1574787690000", "purchase_date_pst": "2019-11-26 09:01:30 America/Los_Angeles", "original_purchase_date": "2019-11-26 17:01:31 Etc/GMT", "original_purchase_date_ms": "1574787691000", "original_purchase_date_pst": "2019-11-26 09:01:31 America/Los_Angeles", "expires_date": "2020-11-26 17:01:30 Etc/GMT", "expires_date_ms": "1606410090000", "expires_date_pst": "2020-11-26 09:01:30 America/Los_Angeles", "web_order_line_item_id": " 0000000000",//changed for this post "is_trial_period": "false", "is_in_intro_offer_period": "false", "subscription_group_identifier": "0000000000",//changed for this post } ], "latest_receipt": "encoded receipt removed for this post ", "pending_renewal_info": [ { "auto_renew_product_id": "com.fooapp.fooprod", "original_transaction_id": " 0000000000",//changed for this post "product_id": "com.fooapp.fooprod", "auto_renew_status": "1" } ] }So I need to know why it returns the property as an array sometimes and as a single receipt object other times.Thanks in advance.
Posted
by cisco5000.
Last updated
.