Skip to content
This repository was archived by the owner on Aug 4, 2026. It is now read-only.

Promotional Offers not Working - Unable to Purchase (Please contact developer for support) #578

Description

@tonytang0114

Relevant issue is below
hyochan/react-native-iap#2715

I have read the documentation on apple developer regarding promotional offer. I have successfully generated signature, nonce, keyId from backend and passed to client and apply to withOffer section.

It returns a dialog "Unable to Purchase (Please contact developer for support)" After clicking the "Purchase button". It does show the correct promotion info from app store dialog side.

I have already done the following checklist.

  1. Create Promotional offer from a base plan
  2. Make sure the sandbox user is an active or elipsed subscriber under the same subscription group
  3. Make sure I am using the In-App-Purchase key with correct key Id.
  4. Make sure everything from the server side and client side are synchronized (signature, offer Id, timestamp, productId, nonce, keyId)
  5. I have double checked the signature I generated. I am using C# web api with ECDSA and SHA256

I am not sure if I have done something wrong or there is a bug here
Do anyone face the same issues here? Thanks

Client code

        var  applePromotionSignature =
                    await subscriptionRepository.getAppleSubscriptionSignature(
                  constants.diamondMemberYearlyPlan,
                  promotionalOffers.first.id,
                );

        var offerId =
            productIOS!.subscriptionInfoIOS!.promotionalOffers!.first.id;

        await sameIAp.requestPurchase(
          RequestPurchaseProps.subs((
            android: null,
            ios: RequestSubscriptionIosProps(
              sku: constants.diamondMemberYearlyPlan,
              appAccountToken: '',
              withOffer: DiscountOfferInputIOS(
                identifier: offerId,
                keyIdentifier: applePromotionSignature!.keyId,
                nonce: applePromotionSignature!.nonce,
                signature: applePromotionSignature!.signature,
                timestamp: applePromotionSignature!.timestamp,
              ),
            ),
            useAlternativeBilling: null,
          )),
        );

Server code in C#

Controller

        [HttpGet("Signature/productId/{productId}/offerId/{offerId}")]
        public async Task<IActionResult> GetAppleSubscriptionSignature(string productId, string offerId)
        {
            var keyFileName = _configuration.GetValue<string>("AppleIAPSubscriptionKeyFileName");
            var bundleId = _configuration.GetValue<string>("AppleClientId");
            var keyId = _configuration.GetValue<string>("AppleIAPPromotionKeyId");
            var nonce = Guid.NewGuid().ToString().ToLowerInvariant();
            // var nowMs = DateTimeOffset.Now.ToUnixTimeSeconds();
            long timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();


            var signature = await _subscriptionService.GenerateSignature(keyFileName, bundleId, keyId, productId, offerId, "", nonce, timestamp);
            if (signature != null)
            {
                var appleSubscriptionSignature = new AppleSubscriptionSignature()
                {
                    offerId = offerId,
                    productId = productId,
                    keyId = keyId,
                    nonce = nonce,
                    signature = signature,
                    timestamp = timestamp
                };
                return Ok(appleSubscriptionSignature);
            }
            return NotFound();
        }

Service class

        public async Task<string> GenerateSignature(
            string keyFileName,
            string bundleId,
            string keyId,
            string productId,
            string offerId,
            string appAccountTokenOrEmpty,
            string nonce,
            long timestampMs)
        {
            // 1) Build the string to sign joined by U+2063 (INVISIBLE SEPARATOR)
            const char SEP = '\u2063';
            var stringToSign = string.Join(SEP, [
                    bundleId,
                    keyId,
                    productId,
                    offerId,
                    appAccountTokenOrEmpty ?? string.Empty,
                    nonce,
                    timestampMs.ToString(CultureInfo.InvariantCulture)
                ]);

            // 2) Load P-256 private key from PKCS#8 PEM
            using var ecdsa = ECDsa.Create();
            var p8PrivateKeyPem = await File.ReadAllTextAsync(keyFileName);
            ImportPkcs8Pem(ecdsa, p8PrivateKeyPem);

            // 3) Sign with ECDSA-SHA256 and return Base64 (DER)
            var data = Encoding.UTF8.GetBytes(stringToSign);
            var derSig = ecdsa.SignData(data, HashAlgorithmName.SHA256);
            return Convert.ToBase64String(derSig);
        }

        private static void ImportPkcs8Pem(ECDsa ecdsa, string pem)
        {

            var pkcs8 = pem
                .Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
                .Replace("-----END PRIVATE KEY-----", string.Empty)
                .Replace("\r", string.Empty)
                .Replace("\n", string.Empty)
                .Trim();
            var keyBytes = Convert.FromBase64String(pkcs8);
            ecdsa.ImportPkcs8PrivateKey(keyBytes, out _);
        }

📱 version

flutter_inapp_purchase: ^7.0.1

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions