Search code examples
iosnode.jsswiftin-app-purchaseauto-renewing

Couldn't purchase with Subscription offer


I am trying to make the In-App Purchase subscription offer work. So I get the encoded signature, nonce, timestamp and key identifier from our server. I create a SKPaymentDiscount object and setting this to paymentDiscount of SKMutablePayment object.

On the first pop it is showing the revised price as expected -> enter the password and continue -> Second pop-up: Confirm subscription : Ok -> Third pop-up: shows the following error Unable to Purchase Contact the developer for more information.

I tried passing a non-applicable offer identifier for a product. Then it threw proper error saying: this cannot be applied to this.

PromoOfferAPI.prepareOffer(usernameHash: "name", productIdentifier: "bundleid.product", offerIdentifier: "TEST10") { (result) in
            switch result {

            case let .success(discount):
                // The original product being purchased.
                let payment = SKMutablePayment(product: option.product)
                // You must set applicationUsername to be the same as the one used to generate the signature.
                payment.applicationUsername = "name"
                // Add the offer to the payment.
                payment.paymentDiscount = discount
                // Add the payment to the queue for purchase.
                SKPaymentQueue.default().add(payment)
                break
            case let .customFail(message):
                print(message)
                break
            case let .failure(error):
                print(error.localizedDescription)
                break
            }
        }

No matter how many times I try, it keeps giving me the same error. Unable to Purchase Contact the developer for more information. What can be done to resolve this issue. Any help is much appreciated.

Thanks In Advance!

Edit 1: It never gets into updatedTransactions function. It just logs Finishing transaction for payment "bundleid.product" with state: failed.

Edit 2: Got the error: code - 12 (invalidSignature). Cannot connect to iTunes Store

Node.JS code that generates the encoded signature.

const UUID = require("uuid-v4");
const microtime = require('microtime');
const express = require('express');
const router = express.Router();
const EC = require("elliptic").ec;
const ec = new EC("secp256k1");
const crypto = require('crypto');

const privateKey = `-----BEGIN PRIVATE KEY-----
key goes here
-----END PRIVATE KEY-----`;
//const key = ec.keyFromPrivate(privateKey,'hex');


router.post('/',(req, res)=>{
    const bundle_id = "bundle.id";
    const key_id = "keyed";
    const nonce = String(UUID()).toLowerCase();// Should be lower case
    const timestamp = microtime.now();

    const product = req.body.product;
    const offer = req.body.offer;
    const application_username = req.body.application_username;

    const payload = bundle_id + '\u2063' + key_id + '\u2063' + product + '\u2063' + offer + '\u2063' + application_username + '\u2063' + String(nonce) + '\u2063' + String(timestamp)
    let shaMsg = crypto.createHash("sha256").update(payload).digest();
    let signature = ec.sign(shaMsg, privateKey, {canonical: true});
    let derSign = signature.toDER();
    let buff = new Buffer(derSign);  
    let base64EncodedSignature = buff.toString('base64');
    let response = {
        "signeture": base64EncodedSignature,
        "nonce": nonce,
        "timestamp": timestamp,
        "keyIdentifier": key_id
    }
    res.type('json').send(response);
});

module.exports = router;

Solution

  • After many trials and errors, figured the issue. Basically it was because of the wrong algorithm and along with minor issues here and there. Here is the complete code in Node.js, hope this helps someone.

      // https://developer.apple.com/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers
      // Step 1
      const appBundleID = req.body.appBundleID
      const keyIdentifier = req.body.keyIdentifier
      const productIdentifier = req.body.productIdentifier
      const offerIdentifier = req.body.offerIdentifier
      const applicationUsername = req.body.applicationUsername
    
      const nonce = uuid4()
      const timestamp = Math.floor(new Date())
    
      // Step 2
      // Combine the parameters into a UTF-8 string with 
      // an invisible separator ('\u2063') between them, 
      // in the order shown:
      // appBundleId + '\u2063' + keyIdentifier + '\u2063' + productIdentifier + 
      // '\u2063' + offerIdentifier + '\u2063' + applicationUsername + '\u2063' + 
      // nonce + '\u2063' + timestamp
    
      let payload = appBundleID + '\u2063' + keyIdentifier + '\u2063' + productIdentifier + '\u2063' + offerIdentifier + '\u2063' + applicationUsername + '\u2063' + nonce+ '\u2063' + timestamp
    
      // Step 3
      // Sign the combined string
      // Private Key - p8 file downloaded
      // Algorithm - ECDSA with SHA-256
    
      const keyPem = fs.readFileSync('file_name.pem', 'ascii');
      // Even though we are specifying "RSA" here, this works with ECDSA
      // keys as well.
      // Step 4
      // Base64-encode the binary signature
      const sign = crypto.createSign('RSA-SHA256')
                       .update(payload)
                       .sign(keyPem, 'base64');
    
      let response1 = {
        "signature": sign,
        "nonce": nonce,
        "timestamp": timestamp,
        "keyIdentifier": keyIdentifier
      }
      res.type('json').send(response1);