How to Let Users Re-Redeem Introductory Offers

Traditionally introductory offers were limited to eligible users who had not previously redeemed an offer for a specific product. With iOS 18.4 Apple is introducing a new API method on Product.PurchaseOption called introductoryOfferEligibility(compactJWS:) which provides more flexibility in offer redemption.

This new API method empowers developers to re-enable introductory offers for users who have previously redeemed them. The mechanism relies on a signed JSON Web Signature (JWS) returned from the developer’s server. By configuring a specific boolean property within the signature, developers can dynamically grant or modify a customer’s eligibility status for introductory offers.

In the following sections, we’ll explore how to configure server-side logic and in-app implementation to leverage the introductoryOfferEligibility(compactJWS:) method effectively.

Prerequisite

Start off by visiting the App Store Server Library GitHub repo and cloning the project.

Create a new file intro_offer_test.js in your Server code

  • Create a new file intro_offer_test.js and add the following code to it in the server library project.
  • Ensure all values in the script are current with your app and App Store Connect account.
    • keyId: The key identifier for the App Store Connect API
    • issuerId: The App Store Connect issuer ID
    • bundleId: The app’s bundle identifier
    • productId: The specific product ID for the subscription/purchase
    • transactionId: The customer’s app transaction ID
    • The private key downloaded from your App Store Connect account
  • Run the script npx ts-node intro_offer_test.ts
  • This script will generate the required JWS needed for the intro offer purchase in your iOS app.
import { IntroductoryOfferEligibilitySignatureCreator } from "@apple/app-store-server-library";
import { readFile } from "fs/promises";
import * as crypto from "crypto";

const filePath = "/Users/alexpaul/Desktop/SubscriptionKey_123456789.p8";
const keyId = "123456789";

// Your App Store Connect issuer id
const issuerId = "15501d40-84b5-4884-a43a-fa18f9e0eecf";

const bundleId = "dev.alexpaul.AppName";
const productId = "your.product.id";

// Here you grant the user access or not to redeem the intro offer
const allowIntroductoryOffer = true;

// Customer's app transaction id
const transactionId = "5000000990901234"; 

async function main() {
    try {
        const encodedKey = await readFile(filePath, "utf8");
        const signatureCreator = new IntroductoryOfferEligibilitySignatureCreator(encodedKey, keyId, issuerId, bundleId);
        const signature = signatureCreator.createSignature(productId, allowIntroductoryOffer, transactionId);
        console.log("transactionId: ", transactionId);
        console.log("productId: ", productId);
        console.log("allowIntroductoryOffer: ", allowIntroductoryOffer);

        // This signature is needed in the iOS app
        // We use this signature in the new `introductoryOfferEligibility(compactJWS: signature)` method
        console.log("signature: ", signature);
    } catch (err) {
        console.error("Error:", err);
    }
}

main().catch(console.error);

Code breakdown:

  • This code is working with Apple’s App Store Server Library to handle introductory offer eligibility for in-app purchases/subscriptions.
  • It reads a private key file from a specific path (in this example it’s using the following path: /Users/alexpaul/Desktop/SubscriptionKey_123456789.p8)
  • The following identifiers are needed by Apple:
    • keyId: The key identifier for the App Store Connect API
    • issuerId: The App Store Connect issuer ID
    • bundleId: The app’s bundle identifier
    • productId: The specific product ID for the subscription/purchase
    • transactionId: The customer’s app transaction ID
  • The main function:
    • Reads the encoded key file asynchronously
    • Creates an IntroductoryOfferEligibilitySignatureCreator instance with the required credentials.
    • Generates a signature using:
      • The product ID
      • A boolean flag allowIntroductoryOffer for determining if the user is eligible for the introductory offer
      • The transaction ID
    • Logs various details including:
      • Transaction ID
      • Product ID
      • Introductory offer allowance status
      • The generated signature

Implement purchase logic in your app

Use the following snippet to add purchase support for introductoryOfferEligibility(compactJWS:) in your app. You will need the signature generated from the script above.

The code snippet below does the following:

  • Creates a Set of Product.PurchaseOption to configure the purchase
  • Two key purchase options are set:
    • appAccountToken()
      • Used to track the transaction across your app
    • introductoryOfferEligibility()
      • Takes a JWS (JSON Web Signature) generated from the server script as a parameter
      • The signature proves the user’s eligibility for an introductory offer
      • The signature is a JWT token containing:
        • Product ID
        • Transaction ID
        • Bundle ID
        • Eligibility status
// purchase code...
let signature = "jws.signed.signature.generated.from.your.server.goes.here"
let options = Set(
    [
        Product.PurchaseOption.appAccountToken(appAccountToken),
        Product.PurchaseOption.introductoryOfferEligibility(compactJWS: signature)
    ]
)

let result = try await subscription.product.purchase(options: options)
// purchase code continued...

Conclusion

By leveraging the introductoryOfferEligibility(compactJWS:) method in iOS 18.4, you can give users the ability to re-redeem introductory offers while maintaining full control over eligibility on your server.

Resources

With this new intro offer API, we now have the potential to acquire, retain, and re-acquire subscribers. Looking forward to seeing Apple’s documentation on subscription offers updated to reflect this change.

Providing subscription offers

2 comments

    • Great question! Promo offers do address offering discounts to former subscribers, but this raises the question—will traditional promo offers gradually become obsolete? This solution is more flexible, as it allows the server to dynamically determine a user’s eligibility for an offer without the sometimes complex configuration of promo offers.

Leave a comment