Couldn't purchase with Subscription offer
Asked Answered
U

3

12

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;
Ustkamenogorsk answered 15/5, 2019 at 12:6 Comment(10)
What is the SKError and code that you're receiving? It's most likely a problem with the subscription key and/or signing.Cassondra
@Cassondra Where can I get the SKError and code?Ustkamenogorsk
The error in your failure block should be an SKError. if let error = error as? SKError { } then you can print the skerror code and description. developer.apple.com/documentation/storekit/…Cassondra
I just get this line logged Finishing transaction for payment "bundleid.product" with state: failed, it doesn't get to updatedTransactions function.Ustkamenogorsk
Hard to know for sure without seeing the StoreKit error, but I've reproduced that popup when testing with a revoked Subscription Key.Cassondra
not sure if this is helpful but here's a tutorial I wrote on subscription offers w/RevenueCat: revenuecat.com/2019/04/25/signing-ios-subscription-offersCassondra
I replaced the Subscription Key by generating one new. The same issue repeats even then. I will check out your tutorial to get more infoUstkamenogorsk
There is a character limit that is undocumented for application_username, try using only 32 characters or less, and of course make sure you use the same value across the different APIs.Eyre
@Eyre I am just using 11 characters and yes it is same everywhere.Ustkamenogorsk
Hi, I am facing the same issue for a couple of days now. I am able to validate the signature locally, but while redeeming the offer it throws the same error. I am using the same application username to generate the signature and create SKPaymentDiscount. Any help will be greatly appreciated.Contrastive
U
3

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);
Ustkamenogorsk answered 23/5, 2019 at 5:51 Comment(0)
R
3

I ran into the same issue while testing out the new WWDC2019 example Node.js server files they provided. After following the readme, I was able to successfully generate a signature.

To my surprise, however, an invalid signature will look just like a valid one, and it took me a while to realize that my signature was invalid.

My error was the following: I used Alamofire to make a GET request to my server, like so:

AF.request("myserver:3000/offer", parameters: parameters).responseJSON { response in

        var signature: String?
        var keyID: String?
        var timestamp: NSNumber?
        var nonce: UUID?

        switch response.result {
        case let .success(value):
            let json = JSON(value)

            // Get required parameters for creating offer
            signature = json["signature"].stringValue
            keyID = json["keyID"].stringValue
            timestamp = json["timestamp"].numberValue
            nonce = UUID(uuidString: json["nonce"].stringValue)

        case let .failure(error):
            print(error)
            return
        }

        // Create offer
        let discountOffer = SKPaymentDiscount(identifier: offerIdentifier, keyIdentifier: keyID!, nonce: nonce!, signature: signature!, timestamp: timestamp!)

        // Pass offer in completion block
        completion(discountOffer) // this completion is a part of the method where this snippet is running
    }
}

On the files provided in the WWDC2019 Video on Subscription Offers, in the index.js file, they are loading the parameters I passed on my request like so:

const appBundleID = req.body.appBundleID;
const productIdentifier = req.body.productIdentifier;
const subscriptionOfferID = req.body.offerID;
const applicationUsername = req.body.applicationUsername;

However, my alamofire request did not pass the parameters in the body, but rather, as query parameters. Therefore, the server was generating a signature with a null appBundleID as well as other null fields! So I changed the aforementioned section of index.js to the following:

const appBundleID = req.query.appBundleID;
const productIdentifier = req.query.productIdentifier;
const subscriptionOfferID = req.query.offerID;
const applicationUsername = req.query.applicationUsername;

I hope this helps anyone who overlooked this. Pardon my unsafe swift, but I hope you get the point!

Retard answered 14/6, 2019 at 21:44 Comment(1)
Thanks for your answer! I made the same mistake using Apple's default URLSession on the client side. The other thing is that if you are in sandbox, you need to wait for the current valid subscription to expire before you can redeem subscription offers. In product, users can redeem an offer even when an existing subscription is still valid. Good luck for everyone!Algid
H
1

After a couple days of swapping between mine+Apple's WWDC19 node impl, and the one above, I found that my issue was not setting the applicationUsername iOS side to match the applicationUsername used in node. Specifically, the SKMutablePayment object iOS side where the discount is set. Hope some fortunate sufferer sees this after less hours than it took me.

Hamitic answered 5/12, 2019 at 1:5 Comment(1)
Hi Jeremy, I am facing the same issue for a couple of days now. I am able to validate the signature locally, but while redeeming the offer it throws the same error. I am using the same application username to generate the signature and create SKPaymentDiscount. Any help will be greatly appreciated.Contrastive

© 2022 - 2024 — McMap. All rights reserved.