dooboolab-community / react-native-iap

In App Purchase module for React Native!
https://react-native-iap.dooboolab.com
MIT License
2.77k stars 633 forks source link

Promotional Offers not Working - Unable to Purchase #2715

Open jlafosse opened 5 months ago

jlafosse commented 5 months ago

Description

When attempting to use apple promotional offers for subscriptions I consistently receive the popup that says "Unable to Purchase - Contact the developer for more information.". I have spent the past couple of days trying to figure this out, but I have hit a dead end. Here is the process that I go through in order to receive the error:

  1. I create a brand new sandbox user on AppStoreConnect.
  2. I subscribe to a 1 month subscription that has an introductory offer sku premium_1m_intro_a
  3. I let the subscription renewals run their course - 3 mins each - 12 times typically. The final server notification type is EXPIRED
  4. I then verify that the sandbox subscription is expired on my phone in Settings > App Store > Sandbox Account > Subscriptions
  5. I then verify that the user is no longer eligible for intro offers using: IapIosSk2.isEligibleForIntroOffer(MY_GROUP_ID)
  6. Now when I attempt to subscribe to the (non-intro) subscription sku premium_1m using the premium_1m_20 promotional offer id, I always get the "Unable to Purchase" error.
  7. I have tried restarting my phone & signing out/in of my sandbox user... to no avail.

App Store Connect Subscriptions Setup

Subscription Group: premium
Subscriptions:
    - premium_1m
        - premium_1m_20 (pay-up-front promotional offer)
        - premium_1m_25 (pay-up-front promotional offer)
    - premium_1m_intro_a
    - premium_1m_intro_b
    - premium_6m
        - premium_6m_30 (pay-up-front promotional offer)
        - premium_6m_40 (pay-up-front promotional offer)
    - premium_6m_intro_a
    - premium_6m_intro_b
    - premium_1y

Additional Notes

Code Sample

setup({ storekitMode: 'STOREKIT2_MODE' });
const result = await initConnection();
...
...

const productId = 'premium_1m';
const offerId = 'premium_1m_20';
const userUuid = '268482a9-312d-4f29-97fe-e659b400f504';

const signedOffer = await getSignedOfferFromServer(productId, offerId, userUuid);

/* signedOffer output:
{
    "identifier": "premium_1m_20", 
    "keyIdentifier": "APPLEKEYID", 
    "nonce": "6ffb4558-d5f1-424d-b355-ab2ff11c20fc", 
    "signature": "MEQXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 
    "timestamp": 1711569543000
}
*/

try {
    await requestSubscription({
        'appAccountToken': userUuid, 
        'sku': productId, 
        'withOffer': signedOffer
    });
} catch(err) {
    console.log(err);

    /* Error output:
        [Error: Purchased failed for sku:premium_1m: Unable to Complete Purchase]
    */
}

Environment:

react-native-iap: 12.12.2 react-native: 0.72.6 expo: 49.0.0 Platforms: iOS v17.3.1

Screenshots

jlafosse commented 4 months ago

Upon further testing, I am able to successfully use promotional offers in STOREKIT1_MODE, so this looks like there may be an issue with STOREKIT2_MODE. I have verified that the minimum IOS build version is 15.0 in my IOS/Podfile. I also verified that I am indeed running in SK2 mode by calling isIosStorekit2() - which returns true and using IapIosSk2.isEligibleForIntroOffer() - which does in fact return the correct eligibility based upon whether I have previously used an intro offer or not.

Perhaps someone else who has posted a similar issue with promotional offers could confirm as to whether or not they were using SK1 or SK2 mode. - #2628

jlafosse commented 4 months ago

I think I figured out what is going on. My server-side script that is generating the signature is returning it as base64 encoded. I am not overly familiar with swift but on line 706 of RNIapIosSk2.swift it looks like the signature is expected to be type data which is then converted to utf8...?

Changing the following on line 706 of RNIapIosSk2.swift fixes the issue:

let signature = signature.data(using: .utf8)

Change To:

let signature = Data(base64Encoded: signature)

Is it a correct assumption that the back-end server script should return a base64 encoded signature? The apple docs and demo script seem to imply so...

https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_signature_for_promotional_offers#3342577

https://developer.apple.com/documentation/storekit/in-app_purchase/original_api_for_in-app_purchase/subscriptions_and_offers/generating_a_promotional_offer_signature_on_the_server

niksamoil commented 2 months ago

Hi, are there any updates?

jlafosse commented 2 months ago

Unfortunately no, I have heard nothing from the RNI team on this issue. Although my fix above does "seem" to solve the issue, I decided to simply stick with using STOREKIT1_MODE for now.

sharukhrahman97 commented 1 week ago

Unfortunately no, I have heard nothing from the RNI team on this issue. Although my fix above does "seem" to solve the issue, I decided to simply stick with using STOREKIT1_MODE for now.

Hi im using expo v49 with storekit1 and ios version 13.0, standard payments works fine and subscription upgrade and downgrade works fine.. but cant purchase with promo offers. I have attached the issue below with the code im using.

so for the discount signature generation im using Generating a Promotional Offer Signature on the Server. and these are the params im using for requestSubscription method

{"sku": "m30", "withOffer": {"identifier": "RRFREE30", "keyIdentifier": "<generated key identifier>", "nonce": "8e450914-3a42-4abf-9d88-bab0165ecf1d", "signature": "MEQCIBDMrQwa5asJ8KdoLu4c+rfCA6K+3ocBMMPzbex4IXDlAiA4LXQdlQ/96A4sR5T/DE523nYduxhTCM1CTNsQ57eTWw==", "timestamp": 1723389449700}}

So far everything is working perfectly fine in debug builds and preview build, but not on production.

issue

sharukhrahman97 commented 1 week ago

tried using storekit2 and modified the above signature parsing thing, and added some lines in the appleSk2.ts line 104 to get discounts

var discounts: Discount[] = [] if (subscription?.promotionalOffers) { for (let i = 0; i < subscription.promotionalOffers.length; i++) { discounts.push({ identifier: subscription.promotionalOffers[i].id, type: subscription.promotionalOffers[i].type, numberOfPeriods: subscription.promotionalOffers[i].periodCount.toString(), price: subscription.promotionalOffers[i].price.toString(), localizedPrice: subscription.promotionalOffers[i].displayPrice, paymentMode: subscription.promotionalOffers[i].paymentMode.toUpperCase() as | '' | 'FREETRIAL' | 'PAYASYOUGO' | 'PAYUPFRONT', subscriptionPeriod: subscription.promotionalOffers[i].period.value.toString() }) } } prod.discounts = discounts

Then i get this error Purchase did not return a transaction: Error Domain=ASDServerErrorDomain Code=3904 "Offer Not Available" UserInfo={NSLocalizedFailureReason=Offer Not Available, client-environment-type=Sandbox, AMSServerErrorCode=3904, storefront-country-code=IND}

super frustating. send help :(

jlafosse commented 1 week ago

@sharukhrahman97, you said the offers are working on debug and preview builds but not on production correct? Are these promotional offers or introductory offers? Are you using expo.dev for your builds and how are you testing production? (i.e. testflight).

sharukhrahman97 commented 1 week ago

@sharukhrahman97, you said the offers are working on debug and preview builds but not on production correct? Are these promotional offers or introductory offers? Are you using expo.dev for your builds and how are you testing production? (i.e. testflight).

for storekit 1 the offers are working in debug mode but not in production.. for storekit2 the offer doesnt work on both.

These are promotional offers. And i have used expo(production builds) and testflight builds

The below is the log from xcode inside the RNIapIosSk2.swift image

And this is the code i have modified whether the token is receiving in the code

if let signature = withOffer["signature"],
    let decodedData = Data(base64Encoded: signature) {
        debugMessage(decodedData.base64EncodedString())
} else {
    debugMessage("Invalid signature")
}
if let offerID = offerID, let keyID = keyID, let nonce = nonce, let nonce = UUID(uuidString: nonce), let signature = signature, let signature = Data(base64Encoded: signature), let timestamp = timestamp, let timestamp = Int(timestamp) {
    options.insert(.promotionalOffer(offerID: offerID, keyID: keyID, nonce: nonce, signature: signature, timestamp: timestamp ))
}
if let appAccountToken = appAccountToken, let appAccountToken = UUID(uuidString: appAccountToken) {
    options.insert(.appAccountToken(appAccountToken))
}
debugMessage("Purchase Started")

guard let windowScene = await currentWindow()?.windowScene else {
    reject(IapErrors.E_DEVELOPER_ERROR.rawValue, "Could not find window scene", nil)
    return
}

var result: Product.PurchaseResult?

if #available(iOS 17.0, tvOS 17.0, *) {
    result = try await product.purchase(confirmIn: windowScene, options: options)
} else {
    #if !os(visionOS)
    result = try await product.purchase(options: options)
    #endif
}