react-native-iap icon indicating copy to clipboard operation
react-native-iap copied to clipboard

[iOS] AppAccountToken for App Store Server Notifications

Open AnthonyHaykal opened this issue 2 years ago โ€ข 12 comments

Hello,

iOS has a new JWSTransactionDecodedPayload which contains the following property:

appAccountToken: A UUID that associates the transaction with a user on your own service. If your app doesnโ€™t provide an appAccountToken, this string is empty. For more information, see appAccountToken(_:).

Linked here

I would like to ask you if we can add the appAcountToken property when we purchase and get it from "App Store Server Notifications"

Thank you for all the hard work :).

AnthonyHaykal avatar Nov 22 '21 07:11 AnthonyHaykal

We have the similar requirement as this. We also need to send the a key that will associate the transaction as part of the product purchase option as app account token. Product.PurchaseOption.appAccountToken(UUID)

Not sure if this is already in the roadmap and when could it probably be available?

Thanks.

ManetJun avatar Nov 29 '21 05:11 ManetJun

Hello

We are also facing this issue in our products, it would be a a lot easier if we can set a user identifier token for IOS purchases the way it is feasible for Android

We were wondering if there is any update regarding this feature, and if it will be added anytime soon @hyochan

Thanks for all your efforts :)

Wanes-Tutunjian avatar Jan 27 '22 08:01 Wanes-Tutunjian

Likewise - would be awesome to have this capability added!

CC: @hyochan

mklsk avatar Feb 14 '22 07:02 mklsk

Also +1'ing this.

As I understand this would be blocked by https://github.com/dooboolab/react-native-iap/issues/1403 ?

sampsonjoliver avatar Feb 16 '22 00:02 sampsonjoliver

As I understand it, this would not be blocked by migrating to StoreKit v2.

In the original implementation of StoreKit there is a field in the SKMutablePayment object that allows you to tag a payment to a specific user with an arbitrary string.

If that string happens to be a UUID, then the receipt will include the appAccountToken field with that UUID value.

https://developer.apple.com/documentation/storekit/skmutablepayment/1506088-applicationusername

This would make applicationUserName field analogous to appAccountToken.

kmcginnes avatar Mar 31 '22 18:03 kmcginnes

As I understand it, this would not be blocked by migrating to StoreKit v2.

In the original implementation of StoreKit there is a field in the SKMutablePayment object that allows you to tag a payment to a specific user with an arbitrary string.

If that string happens to be a UUID, then the receipt will include the appAccountToken field with that UUID value.

https://developer.apple.com/documentation/storekit/skmutablepayment/1506088-applicationusername

This would make applicationUserName field analogous to appAccountToken.

thanks for pointing this out... we added the applicationUsername parameter to the request subscription function and patched the package... works like a charm ๐Ÿ‘๐Ÿฟ

Wanes-Tutunjian avatar Jun 30 '22 08:06 Wanes-Tutunjian

Hi @Wanes-Tutunjian, would you mind posting what you changed and how etc? Would love to do this myself. Thanks

He1nr1chK avatar Jun 30 '22 13:06 He1nr1chK

@He1nr1chK modify these files accordingly: RNIapIos.swift

@objc public func buyProduct(
    _ sku:String,
    appUserNameIOS:String, //Add this line
    andDangerouslyFinishTransactionAutomatically: Bool,
    resolve: @escaping RCTPromiseResolveBlock = { _ in },
    reject: @escaping RCTPromiseRejectBlock = { _, _, _ in }
) {

    pendingTransactionWithAutoFinish = andDangerouslyFinishTransactionAutomatically
    var product: SKProduct?
    let lockQueue = DispatchQueue(label: "validProducts")
    lockQueue.sync {
        for p in validProducts {
            if sku == p.productIdentifier {
                product = p
                break
            }
        }
    }
    if let prod = product {
        addPromise(forKey: prod.productIdentifier, resolve: resolve, reject: reject)
        
        let payment = SKMutablePayment(product: prod)
        payment.applicationUsername = appUserNameIOS //Add this line
        SKPaymentQueue.default().add(payment)
    } else{
        if hasListeners {
            let err = [
                "debugMessage" : "Invalid product ID.",
                "code" : "E_DEVELOPER_ERROR",
                "message" : "Invalid product ID.",
                "productId" : sku
            ]
            sendEvent(withName: "purchase-error", body: err)
        }
        reject("E_DEVELOPER_ERROR", "Invalid product ID.", nil)
    }
}

==============================================================

RNIapIos.m

RCT_EXTERN_METHOD(buyProduct:
                  (NSString*)sku
                  appUserNameIOS:(NSString*)appUserNameIOS
                  andDangerouslyFinishTransactionAutomatically:(BOOL)andDangerouslyFinishTransactionAutomatically
                  resolve:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)

==============================================================

iap.d.ts, this file is under src folder

/**
 * Request a purchase for product. This will be received in `PurchaseUpdatedListener`.
 * @param {string} [sku] The product's sku/ID
 * @param {string} [appUserNameIOS] The purchaser's user ID
 * @param {boolean} [andDangerouslyFinishTransactionAutomaticallyIOS] You should set this to false and call finishTransaction manually when you have delivered the purchased goods to the user. It defaults to true to provide backwards compatibility. Will default to false in version 4.0.0.
 * @param {string} [purchaseTokenAndroid] purchaseToken that the user is upgrading or downgrading from (Android).
 * @param {ProrationModesAndroid} [prorationModeAndroid] UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, IMMEDIATE_WITH_TIME_PRORATION, IMMEDIATE_AND_CHARGE_PRORATED_PRICE, IMMEDIATE_WITHOUT_PRORATION, DEFERRED
 * @param {string} [obfuscatedAccountIdAndroid] Specifies an optional obfuscated string that is uniquely associated with the user's account in your app.
 * @param {string} [obfuscatedProfileIdAndroid] Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app.
 * @returns {Promise<SubscriptionPurchase | null>} Promise resolves to null when using proratioModesAndroid=DEFERRED, and to a SubscriptionPurchase otherwise
 */

export declare const requestSubscription: (sku: string, appUserNameIOS?: string | undefined, andDangerouslyFinishTransactionAutomaticallyIOS?: boolean, purchaseTokenAndroid?: string | undefined, prorationModeAndroid?: ProrationModesAndroid, obfuscatedAccountIdAndroid?: string | undefined, obfuscatedProfileIdAndroid?: string | undefined) => Promise<SubscriptionPurchase | null>;

==============================================================

iap.js, this file is also under src folder as well

/**
 * Request a purchase for product. This will be received in `PurchaseUpdatedListener`.
 * @param {string} [sku] The product's sku/ID
 * @param {string} [appUserNameIOS] The purchaser's user ID
 * @param {boolean} [andDangerouslyFinishTransactionAutomaticallyIOS] You should set this to false and call finishTransaction manually when you have delivered the purchased goods to the user. It defaults to true to provide backwards compatibility. Will default to false in version 4.0.0.
 * @param {string} [purchaseTokenAndroid] purchaseToken that the user is upgrading or downgrading from (Android).
 * @param {ProrationModesAndroid} [prorationModeAndroid] UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, IMMEDIATE_WITH_TIME_PRORATION, IMMEDIATE_AND_CHARGE_PRORATED_PRICE, IMMEDIATE_WITHOUT_PRORATION, DEFERRED
 * @param {string} [obfuscatedAccountIdAndroid] Specifies an optional obfuscated string that is uniquely associated with the user's account in your app.
 * @param {string} [obfuscatedProfileIdAndroid] Specifies an optional obfuscated string that is uniquely associated with the user's profile in your app.
 * @returns {Promise<SubscriptionPurchase | null>} Promise resolves to null when using proratioModesAndroid=DEFERRED, and to a SubscriptionPurchase otherwise
 */
export var requestSubscription = function (sku, appUserNameIOS, andDangerouslyFinishTransactionAutomaticallyIOS, purchaseTokenAndroid, prorationModeAndroid, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid) {
    if (andDangerouslyFinishTransactionAutomaticallyIOS === void 0) { andDangerouslyFinishTransactionAutomaticallyIOS = false; }
    if (purchaseTokenAndroid === void 0) { purchaseTokenAndroid = undefined; }
    if (prorationModeAndroid === void 0) { prorationModeAndroid = -1; }
    if (obfuscatedAccountIdAndroid === void 0) { obfuscatedAccountIdAndroid = undefined; }
    if (obfuscatedProfileIdAndroid === void 0) { obfuscatedProfileIdAndroid = undefined; }
    return (Platform.select({
        ios: function () { return __awaiter(void 0, void 0, void 0, function () {
            return __generator(this, function (_a) {
                if (andDangerouslyFinishTransactionAutomaticallyIOS) {
                    // eslint-disable-next-line no-console
                    console.warn(
                    // eslint-disable-next-line max-len
                    'You are dangerously allowing react-native-iap to finish your transaction automatically. You should set andDangerouslyFinishTransactionAutomatically to false when calling requestPurchase and call finishTransaction manually when you have delivered the purchased goods to the user. It defaults to true to provide backwards compatibility. Will default to false in version 4.0.0.');
                }
                return [2 /*return*/, getIosModule().buyProduct(sku, appUserNameIOS, andDangerouslyFinishTransactionAutomaticallyIOS)];
            });
        }); },
        android: function () { return __awaiter(void 0, void 0, void 0, function () {
            return __generator(this, function (_a) {
                return [2 /*return*/, getAndroidModule().buyItemByType(ANDROID_ITEM_TYPE_SUBSCRIPTION, sku, purchaseTokenAndroid, prorationModeAndroid, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid)];
            });
        }); },
    }) || Promise.resolve)();
};

Use it like this: requestSubscription(sku, uuidString);

Please make sure that the applicationUserName input must be in UUID format (i.e. 123e4567-e89b-12d3-a456-426614174000), if not it would not be shown in the App Store Server notification under appAccountToken field.

jeffy2007 avatar Jul 06 '22 14:07 jeffy2007

A little more info here, when the subscription is renew from a expired subscription via App Store Manage Subscription page, the uuid you provided previously would not be included.

jeffy2007 avatar Jul 06 '22 14:07 jeffy2007

Hi @jeffy2007 thanks a lot. Really appreciate that you took the time to help out.

He1nr1chK avatar Jul 07 '22 15:07 He1nr1chK

As I understand it, this would not be blocked by migrating to StoreKit v2. In the original implementation of StoreKit there is a field in the SKMutablePayment object that allows you to tag a payment to a specific user with an arbitrary string. If that string happens to be a UUID, then the receipt will include the appAccountToken field with that UUID value. https://developer.apple.com/documentation/storekit/skmutablepayment/1506088-applicationusername This would make applicationUserName field analogous to appAccountToken.

thanks for pointing this out... we added the applicationUsername parameter to the request subscription function and patched the package... works like a charm ๐Ÿ‘๐Ÿฟ

from which version of RNIap is the applicationUsername included in the subscription function?

MadsonDouglas avatar Aug 02 '22 17:08 MadsonDouglas

As I understand it, this would not be blocked by migrating to StoreKit v2. In the original implementation of StoreKit there is a field in the SKMutablePayment object that allows you to tag a payment to a specific user with an arbitrary string. If that string happens to be a UUID, then the receipt will include the appAccountToken field with that UUID value. https://developer.apple.com/documentation/storekit/skmutablepayment/1506088-applicationusername This would make applicationUserName field analogous to appAccountToken.

thanks for pointing this out... we added the applicationUsername parameter to the request subscription function and patched the package... works like a charm ๐Ÿ‘๐Ÿฟ

from which version of RNIap is the applicationUsername included in the subscription function?

Seems to have been resolved in https://github.com/dooboolab/react-native-iap/pull/1763 which was released in 8.4.0: https://github.com/dooboolab/react-native-iap/releases/tag/8.4.0

Hoping to upgrade and test these changes over the next couple weeks :D

sampsonjoliver avatar Aug 08 '22 07:08 sampsonjoliver

Please help us test 11.0.0-alpha1 (Migration to Storekit 2) Otherwise this has been merged on main. Can be found on the latest 10.x.x

andresesfm avatar Aug 31 '22 21:08 andresesfm