I wanted to ask if someone has already implemented the new Offers for the inapp-subscription (auto renewal), the difficulty in creating server-side the system to create this signature using the p8 key with php if possible. I found this on the Apple documentation, I'm not sure understanding it: https://developer.apple.com/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers
Here's a walkthrough from RevenueCat: iOS Subscription Offers
The post contains much more detail, but the signature generation is:
import json
import uuid
import time
import hashlib
import base64
from ecdsa import SigningKey
from ecdsa.util import sigencode_der
bundle_id = 'com.myapp'
key_id = 'XWSXTGQVX2'
product = 'com.myapp.product.a'
offer = 'REFERENCE_CODE' # This is the code set in ASC
application_username = 'user_name' # Should be the same you use when
# making purchases
nonce = uuid.uuid4()
timestamp = int(round(time.time() * 1000))
payload = '\u2063'.join([bundle_id,
key_id,
product,
offer,
application_username,
str(nonce), # Should be lower case
str(timestamp)])
# Read the key file
with open('cert.der', 'rb') as myfile:
der = myfile.read()
signing_key = SigningKey.from_der(der)
signature = signing_key.sign(payload.encode('utf-8'),
hashfunc=hashlib.sha256,
sigencode=sigencode_der)
encoded_signature = base64.b64encode(signature)
print(str(encoded_signature, 'utf-8'), str(nonce), str(timestamp), key_id)
This is just a proof of concept. You will want this on your server and perhaps have some logic to determine, for a given user, if the requested offer is appropriate.
Once you’ve generated the signature, nonce and timestamp send these along with the key_id back to your app where you can create an SKPaymentDiscount.
Disclaimer: I work at RevenueCat. We support Subscription Offers out of the box with our SDK, no code-signing required: https://www.revenuecat.com/2019/04/25/signing-ios-subscription-offers
I can confirm that this is working:
<?php
use Ramsey\Uuid\Uuid;
class ItunesSignatureGenerator {
private $appBundleID = 'your.bundle.id';
private $keyIdentifier = 'ZZZZZZZ';
private $itunesPrivateKeyPath = '/path/to/the/file.p8;
/**
* @see https://developer.apple.com/documentation/storekit/in-app_purchase/generating_a_signature_for_subscription_offers
*
* @param $productIdentifier
* @param $offerIdentifier
*
* @return Signature
*/
public function generateSubscriptionOfferSignature($productIdentifier, $offerIdentifier)
{
$nonce = strtolower(Uuid::uuid1()->toString());
$timestamp = time() * 1000;
$applicationUsername = 'username';
$message = implode(
"\u{2063}",
[
$this->appBundleID,
$this->keyIdentifier,
$productIdentifier,
$offerIdentifier,
$applicationUsername,
$nonce,
$timestamp
]
);
$message = $this->sign($message);
return new Signature(
base64_encode($message),
$nonce,
$timestamp,
$this->keyIdentifier
);
}
private function sign($data)
{
$signature = '';
openssl_sign(
$data,
$signature,
openssl_get_privatekey('file://' . $this->itunesPrivateKeyPath),
OPENSSL_ALGO_SHA256
);
return $signature;
}
}
We had a issue on the client side because the device was connected on 2 different itunes account, one regular and one sandbox. It was creating a invalid signature error that didn't make sens. We disconnect the regular account and just use the sandbox account and everything is working.
I used to have problems with subscription offers, but this issue on GitHub helped me to make it work. I installed Sop CryptoBridge library (composer require sop/crypto-bridge) and it finally worked for my iOS app client. Here is my working PHP code:
use Sop\CryptoBridge\Crypto;
use Sop\CryptoEncoding\PEM;
use Sop\CryptoTypes\AlgorithmIdentifier\Hash\SHA256AlgorithmIdentifier;
use Sop\CryptoTypes\AlgorithmIdentifier\Signature\SignatureAlgorithmIdentifierFactory;
use Sop\CryptoTypes\Asymmetric\PrivateKeyInfo;
// you can copy your p8 file contents here or just use file_get_contents()
$privateKeyPem = <<<'PEM'
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
PEM;
// load private key
$privateKeyInfo = PrivateKeyInfo::fromPEM(PEM::fromString($privateKeyPem));
// you can also load p8 file like this
// PrivateKeyInfo::fromPEM(PEM::fromFile($pathToP8));
// combine the parameters
$appBundleId = 'com.github.yaronius'; // iOS app bundle ID
$keyIdentifier = 'A1B2C3D4E5'; // Key ID from AppStore Connect
$productIdentifier = 'product_identifier';
$offerIdentifier = 'offer_identifier';
$applicationUsername = 'username'; // same as in the iOS app
$nonce = 'db6ba7a6-9ec2-4504-bcb9-c0dfbdc3d051'; // some UUID in lowercase
$timestamp = time() * 1000; // milliseconds! as stated in the docs
$dataArray = [
$appBundleId,
$keyIdentifier,
$productIdentifier,
$offerIdentifier,
$applicationUsername,
$nonce,
$timestamp
];
$data = implode("\u{2063}", $dataArray);
// signature algorithm
$algo = SignatureAlgorithmIdentifierFactory::algoForAsymmetricCrypto(
$privateKeyInfo->algorithmIdentifier(),
new SHA256AlgorithmIdentifier()
);
// generate signature
$signature = Crypto::getDefault()->sign($data, $privateKeyInfo, $algo);
// encode as base64 encoded DER
$encodedSignature = base64_encode($signature->toDER());
// send signature to your app
echo $encodedSignature;
Keep in mind a few things:
"\u{2063}". Using '\u2063' did not work for me.$nonce is lowercase UUID $timestamp is in milliseconds (i.e. time() * 1000). And it should work like a charm.