expo-iap 2.3.2 → 2.3.4-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/modules/ios.d.ts +4 -1
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +4 -1
- package/build/modules/ios.js.map +1 -1
- package/build/useIap.d.ts +8 -2
- package/build/useIap.d.ts.map +1 -1
- package/build/useIap.js +61 -11
- package/build/useIap.js.map +1 -1
- package/iap.md +20 -38
- package/ios/ExpoIapModule.swift +78 -0
- package/package.json +1 -1
- package/src/modules/ios.ts +4 -1
- package/src/useIap.ts +83 -12
package/build/modules/ios.d.ts
CHANGED
|
@@ -37,7 +37,10 @@ export declare const latestTransaction: (sku: string) => Promise<ProductPurchase
|
|
|
37
37
|
type RefundRequestStatus = 'success' | 'userCancelled';
|
|
38
38
|
export declare const beginRefundRequest: (sku: string) => Promise<RefundRequestStatus>;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* Shows the system UI for managing subscriptions.
|
|
41
|
+
* When the user changes subscription renewal status, the system will emit events to
|
|
42
|
+
* purchaseUpdatedListener and transactionUpdatedIos listeners.
|
|
43
|
+
* @returns {Promise<null>}
|
|
41
44
|
*/
|
|
42
45
|
export declare const showManageSubscriptions: () => Promise<null>;
|
|
43
46
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios.d.ts","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAGhE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC;AAGF,eAAO,MAAM,qBAAqB,aACtB,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,6CAO5C,CAAC;AAGF,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,EACxD,IAAI,EAAE,OAAO,GACZ,IAAI,IAAI,CAAC,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAC,CAO/B;AAGD;;;GAGG;AACH,eAAO,MAAM,IAAI,QAAO,QAAQ,IAAI,CAAyB,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,uBAAuB,YAAa,MAAM,KAAG,QAAQ,OAAO,CACzB,CAAC;AAEjD;;GAEG;AAEH,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,gBAAgB,EAAE,CACpC,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,eAAe,CACjC,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,iBAAiB,QAAS,MAAM,KAAG,QAAQ,eAAe,CACjC,CAAC;AAEvC;;GAEG;AACH,KAAK,mBAAmB,GAAG,SAAS,GAAG,eAAe,CAAC;AACvD,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,mBAAmB,CACrC,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"ios.d.ts","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"AAEA,OAAO,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAChE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAGhE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB,CAAC;AAGF,eAAO,MAAM,qBAAqB,aACtB,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,6CAO5C,CAAC;AAGF,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAC,EACxD,IAAI,EAAE,OAAO,GACZ,IAAI,IAAI,CAAC,GAAG;IAAC,QAAQ,EAAE,KAAK,CAAA;CAAC,CAO/B;AAGD;;;GAGG;AACH,eAAO,MAAM,IAAI,QAAO,QAAQ,IAAI,CAAyB,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,uBAAuB,YAAa,MAAM,KAAG,QAAQ,OAAO,CACzB,CAAC;AAEjD;;GAEG;AAEH,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,gBAAgB,EAAE,CACpC,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,eAAe,CACjC,CAAC;AAExC;;GAEG;AACH,eAAO,MAAM,iBAAiB,QAAS,MAAM,KAAG,QAAQ,eAAe,CACjC,CAAC;AAEvC;;GAEG;AACH,KAAK,mBAAmB,GAAG,SAAS,GAAG,eAAe,CAAC;AACvD,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,QAAQ,mBAAmB,CACrC,CAAC;AAExC;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAO,QAAQ,IAAI,CACd,CAAC"}
|
package/build/modules/ios.js
CHANGED
|
@@ -39,7 +39,10 @@ export const currentEntitlement = (sku) => ExpoIapModule.currentEntitlement(sku)
|
|
|
39
39
|
export const latestTransaction = (sku) => ExpoIapModule.latestTransaction(sku);
|
|
40
40
|
export const beginRefundRequest = (sku) => ExpoIapModule.beginRefundRequest(sku);
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
42
|
+
* Shows the system UI for managing subscriptions.
|
|
43
|
+
* When the user changes subscription renewal status, the system will emit events to
|
|
44
|
+
* purchaseUpdatedListener and transactionUpdatedIos listeners.
|
|
45
|
+
* @returns {Promise<null>}
|
|
43
46
|
*/
|
|
44
47
|
export const showManageSubscriptions = () => ExpoIapModule.showManageSubscriptions();
|
|
45
48
|
//# sourceMappingURL=ios.js.map
|
package/build/modules/ios.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios.js","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AACtC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,IAAI,CAAC;AAGrC,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAO7C,YAAY;AACZ,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,QAA2C,EAC3C,EAAE;IACF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,cAAc;AACd,MAAM,UAAU,YAAY,CAC1B,IAAa;IAEb,OAAO,CACL,IAAI,IAAI,IAAI;QACZ,OAAO,IAAI,KAAK,QAAQ;QACxB,UAAU,IAAI,IAAI;QAClB,IAAI,CAAC,QAAQ,KAAK,KAAK,CACxB,CAAC;AACJ,CAAC;AAED,YAAY;AACZ;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAkB,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;AAE9D;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,OAAe,EAAoB,EAAE,CAC3E,aAAa,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAEjD;;GAEG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA+B,EAAE,CAC7E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA4B,EAAE,CAC1E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAA4B,EAAE,CACzE,aAAa,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAMvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAgC,EAAE,CAC9E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC
|
|
1
|
+
{"version":3,"file":"ios.js","sourceRoot":"","sources":["../../src/modules/ios.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,cAAc,CAAC;AACtC,OAAO,EAAC,OAAO,EAAE,QAAQ,EAAC,MAAM,IAAI,CAAC;AAGrC,OAAO,aAAa,MAAM,kBAAkB,CAAC;AAO7C,YAAY;AACZ,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACnC,QAA2C,EAC3C,EAAE;IACF,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAC1D,CAAC;IAED,OAAO,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC;AACvE,CAAC,CAAC;AAEF,cAAc;AACd,MAAM,UAAU,YAAY,CAC1B,IAAa;IAEb,OAAO,CACL,IAAI,IAAI,IAAI;QACZ,OAAO,IAAI,KAAK,QAAQ;QACxB,UAAU,IAAI,IAAI;QAClB,IAAI,CAAC,QAAQ,KAAK,KAAK,CACxB,CAAC;AACJ,CAAC;AAED,YAAY;AACZ;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,GAAkB,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,CAAC;AAE9D;;GAEG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,OAAe,EAAoB,EAAE,CAC3E,aAAa,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;AAEjD;;GAEG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA+B,EAAE,CAC7E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAA4B,EAAE,CAC1E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,GAAW,EAA4B,EAAE,CACzE,aAAa,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAMvC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAgC,EAAE,CAC9E,aAAa,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC;AAExC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAkB,EAAE,CACzD,aAAa,CAAC,uBAAuB,EAAE,CAAC","sourcesContent":["import {Platform} from 'react-native';\nimport {emitter, IapEvent} from '..';\nimport {ProductPurchase, PurchaseError} from '../ExpoIap.types';\nimport type {ProductStatusIos} from '../types/ExpoIapIos.types';\nimport ExpoIapModule from '../ExpoIapModule';\n\nexport type TransactionEvent = {\n transaction?: ProductPurchase;\n error?: PurchaseError;\n};\n\n// Listeners\nexport const transactionUpdatedIos = (\n listener: (event: TransactionEvent) => void,\n) => {\n if (Platform.OS !== 'ios') {\n throw new Error('This method is only available on iOS');\n }\n\n return emitter.addListener(IapEvent.TransactionIapUpdated, listener);\n};\n\n// Type guards\nexport function isProductIos<T extends {platform?: string}>(\n item: unknown,\n): item is T & {platform: 'ios'} {\n return (\n item != null &&\n typeof item === 'object' &&\n 'platform' in item &&\n item.platform === 'ios'\n );\n}\n\n// Functions\n/**\n * Sync state with Appstore (iOS only)\n * https://developer.apple.com/documentation/storekit/appstore/3791906-sync\n */\nexport const sync = (): Promise<null> => ExpoIapModule.sync();\n\n/**\n *\n */\nexport const isEligibleForIntroOffer = (groupID: string): Promise<boolean> =>\n ExpoIapModule.isEligibleForIntroOffer(groupID);\n\n/**\n *\n */\n\nexport const subscriptionStatus = (sku: string): Promise<ProductStatusIos[]> =>\n ExpoIapModule.subscriptionStatus(sku);\n\n/**\n *\n */\nexport const currentEntitlement = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.currentEntitlement(sku);\n\n/**\n *\n */\nexport const latestTransaction = (sku: string): Promise<ProductPurchase> =>\n ExpoIapModule.latestTransaction(sku);\n\n/**\n *\n */\ntype RefundRequestStatus = 'success' | 'userCancelled';\nexport const beginRefundRequest = (sku: string): Promise<RefundRequestStatus> =>\n ExpoIapModule.beginRefundRequest(sku);\n\n/**\n * Shows the system UI for managing subscriptions.\n * When the user changes subscription renewal status, the system will emit events to \n * purchaseUpdatedListener and transactionUpdatedIos listeners.\n * @returns {Promise<null>}\n */\nexport const showManageSubscriptions = (): Promise<null> =>\n ExpoIapModule.showManageSubscriptions();\n"]}
|
package/build/useIap.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { requestPurchase as requestPurchaseInternal } from './';
|
|
2
|
-
import { Product, ProductPurchase, Purchase, PurchaseError, PurchaseResult, SubscriptionProduct } from './ExpoIap.types';
|
|
2
|
+
import { Product, ProductPurchase, Purchase, PurchaseError, PurchaseResult, SubscriptionProduct, SubscriptionPurchase } from './ExpoIap.types';
|
|
3
3
|
type UseIap = {
|
|
4
4
|
connected: boolean;
|
|
5
5
|
products: Product[];
|
|
@@ -9,6 +9,8 @@ type UseIap = {
|
|
|
9
9
|
availablePurchases: ProductPurchase[];
|
|
10
10
|
currentPurchase?: ProductPurchase;
|
|
11
11
|
currentPurchaseError?: PurchaseError;
|
|
12
|
+
clearCurrentPurchase: () => void;
|
|
13
|
+
clearCurrentPurchaseError: () => void;
|
|
12
14
|
finishTransaction: ({ purchase, isConsumable, }: {
|
|
13
15
|
purchase: Purchase;
|
|
14
16
|
isConsumable?: boolean;
|
|
@@ -19,6 +21,10 @@ type UseIap = {
|
|
|
19
21
|
getSubscriptions: (skus: string[]) => Promise<void>;
|
|
20
22
|
requestPurchase: typeof requestPurchaseInternal;
|
|
21
23
|
};
|
|
22
|
-
export
|
|
24
|
+
export interface UseIAPOptions {
|
|
25
|
+
onPurchaseSuccess?: (purchase: ProductPurchase | SubscriptionPurchase) => void;
|
|
26
|
+
onPurchaseError?: (error: PurchaseError) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare function useIAP(options?: UseIAPOptions): UseIap;
|
|
23
29
|
export {};
|
|
24
30
|
//# sourceMappingURL=useIap.d.ts.map
|
package/build/useIap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIap.d.ts","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,eAAe,IAAI,uBAAuB,
|
|
1
|
+
{"version":3,"file":"useIap.d.ts","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"AAAA,OAAO,EAWL,eAAe,IAAI,uBAAuB,EAE3C,MAAM,IAAI,CAAC;AAEZ,OAAO,EACL,OAAO,EACP,eAAe,EACf,QAAQ,EACR,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,oBAAoB,EACrB,MAAM,iBAAiB,CAAC;AAKzB,KAAK,MAAM,GAAG;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,mBAAmB,EAAE,eAAe,EAAE,CAAC;IACvC,aAAa,EAAE,mBAAmB,EAAE,CAAC;IACrC,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC,kBAAkB,EAAE,eAAe,EAAE,CAAC;IACtC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,oBAAoB,CAAC,EAAE,aAAa,CAAC;IACrC,oBAAoB,EAAE,MAAM,IAAI,CAAC;IACjC,yBAAyB,EAAE,MAAM,IAAI,CAAC;IACtC,iBAAiB,EAAE,CAAC,EAClB,QAAQ,EACR,YAAY,GACb,EAAE;QACD,QAAQ,EAAE,QAAQ,CAAC;QACnB,YAAY,CAAC,EAAE,OAAO,CAAC;KACxB,KAAK,OAAO,CAAC,MAAM,GAAG,OAAO,GAAG,cAAc,GAAG,IAAI,CAAC,CAAC;IACxD,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,oBAAoB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxD,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,eAAe,EAAE,OAAO,uBAAuB,CAAC;CACjD,CAAC;AAEF,MAAM,WAAW,aAAa;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,eAAe,GAAG,oBAAoB,KAAK,IAAI,CAAC;IAC/E,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;CAClD;AAED,wBAAgB,MAAM,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAqMtD"}
|
package/build/useIap.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { endConnection, initConnection, purchaseErrorListener, purchaseUpdatedListener, transactionUpdatedIos, getProducts, getAvailablePurchases, getPurchaseHistory, finishTransaction as finishTransactionInternal, getSubscriptions, requestPurchase as requestPurchaseInternal, } from './';
|
|
1
|
+
import { endConnection, initConnection, purchaseErrorListener, purchaseUpdatedListener, transactionUpdatedIos, getProducts, getAvailablePurchases, getPurchaseHistory, finishTransaction as finishTransactionInternal, getSubscriptions, requestPurchase as requestPurchaseInternal, sync, } from './';
|
|
2
2
|
import { useCallback, useEffect, useState, useRef } from 'react';
|
|
3
3
|
import { Platform } from 'react-native';
|
|
4
|
-
export function useIAP() {
|
|
4
|
+
export function useIAP(options) {
|
|
5
5
|
const [connected, setConnected] = useState(false);
|
|
6
6
|
const [products, setProducts] = useState([]);
|
|
7
7
|
const [promotedProductsIOS, setPromotedProductsIOS] = useState([]);
|
|
@@ -10,7 +10,17 @@ export function useIAP() {
|
|
|
10
10
|
const [availablePurchases, setAvailablePurchases] = useState([]);
|
|
11
11
|
const [currentPurchase, setCurrentPurchase] = useState();
|
|
12
12
|
const [currentPurchaseError, setCurrentPurchaseError] = useState();
|
|
13
|
+
const optionsRef = useRef(options);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
optionsRef.current = options;
|
|
16
|
+
}, [options]);
|
|
13
17
|
const subscriptionsRef = useRef({});
|
|
18
|
+
const clearCurrentPurchase = useCallback(() => {
|
|
19
|
+
setCurrentPurchase(undefined);
|
|
20
|
+
}, []);
|
|
21
|
+
const clearCurrentPurchaseError = useCallback(() => {
|
|
22
|
+
setCurrentPurchaseError(undefined);
|
|
23
|
+
}, []);
|
|
14
24
|
const getProductsInternal = useCallback(async (skus) => {
|
|
15
25
|
setProducts(await getProducts(skus));
|
|
16
26
|
}, []);
|
|
@@ -35,13 +45,39 @@ export function useIAP() {
|
|
|
35
45
|
}
|
|
36
46
|
finally {
|
|
37
47
|
if (purchase.id === currentPurchase?.id) {
|
|
38
|
-
|
|
48
|
+
clearCurrentPurchase();
|
|
39
49
|
}
|
|
40
50
|
if (purchase.id === currentPurchaseError?.productId) {
|
|
41
|
-
|
|
51
|
+
clearCurrentPurchaseError();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}, [currentPurchase?.id, currentPurchaseError?.productId, clearCurrentPurchase, clearCurrentPurchaseError]);
|
|
55
|
+
const requestPurchaseWithReset = useCallback(async (requestObj) => {
|
|
56
|
+
clearCurrentPurchase();
|
|
57
|
+
clearCurrentPurchaseError();
|
|
58
|
+
try {
|
|
59
|
+
return await requestPurchaseInternal(requestObj);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}, [clearCurrentPurchase, clearCurrentPurchaseError]);
|
|
65
|
+
const refreshSubscriptionStatus = useCallback(async (productId) => {
|
|
66
|
+
try {
|
|
67
|
+
if (Platform.OS === 'ios') {
|
|
68
|
+
await sync().catch(() => {
|
|
69
|
+
// Ignore errors as sync might require user password
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (subscriptions.some(sub => sub.id === productId)) {
|
|
73
|
+
await getSubscriptionsInternal([productId]);
|
|
74
|
+
await getAvailablePurchasesInternal();
|
|
42
75
|
}
|
|
43
76
|
}
|
|
44
|
-
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.warn('Failed to refresh subscription status:', error);
|
|
79
|
+
}
|
|
80
|
+
}, [getSubscriptionsInternal, getAvailablePurchasesInternal, subscriptions]);
|
|
45
81
|
const initIapWithSubscriptions = useCallback(async () => {
|
|
46
82
|
const result = await initConnection();
|
|
47
83
|
setConnected(result);
|
|
@@ -49,20 +85,32 @@ export function useIAP() {
|
|
|
49
85
|
subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(async (purchase) => {
|
|
50
86
|
setCurrentPurchaseError(undefined);
|
|
51
87
|
setCurrentPurchase(purchase);
|
|
88
|
+
if ('expirationDateIos' in purchase) {
|
|
89
|
+
await refreshSubscriptionStatus(purchase.id);
|
|
90
|
+
}
|
|
91
|
+
if (optionsRef.current?.onPurchaseSuccess) {
|
|
92
|
+
optionsRef.current.onPurchaseSuccess(purchase);
|
|
93
|
+
}
|
|
52
94
|
});
|
|
53
95
|
subscriptionsRef.current.purchaseError = purchaseErrorListener((error) => {
|
|
54
96
|
setCurrentPurchase(undefined);
|
|
55
97
|
setCurrentPurchaseError(error);
|
|
98
|
+
if (optionsRef.current?.onPurchaseError) {
|
|
99
|
+
optionsRef.current.onPurchaseError(error);
|
|
100
|
+
}
|
|
56
101
|
});
|
|
57
102
|
if (Platform.OS === 'ios') {
|
|
58
|
-
subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos((event) => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
103
|
+
subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos(async (event) => {
|
|
104
|
+
if (event.transaction) {
|
|
105
|
+
setPromotedProductsIOS((prevProducts) => [...prevProducts, event.transaction]);
|
|
106
|
+
if ('expirationDateIos' in event.transaction) {
|
|
107
|
+
await refreshSubscriptionStatus(event.transaction.id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
62
110
|
});
|
|
63
111
|
}
|
|
64
112
|
}
|
|
65
|
-
}, []);
|
|
113
|
+
}, [refreshSubscriptionStatus]);
|
|
66
114
|
useEffect(() => {
|
|
67
115
|
initIapWithSubscriptions();
|
|
68
116
|
const currentSubscriptions = subscriptionsRef.current;
|
|
@@ -84,11 +132,13 @@ export function useIAP() {
|
|
|
84
132
|
availablePurchases,
|
|
85
133
|
currentPurchase,
|
|
86
134
|
currentPurchaseError,
|
|
135
|
+
clearCurrentPurchase,
|
|
136
|
+
clearCurrentPurchaseError,
|
|
87
137
|
getAvailablePurchases: getAvailablePurchasesInternal,
|
|
88
138
|
getPurchaseHistories: getPurchaseHistoriesInternal,
|
|
89
139
|
getProducts: getProductsInternal,
|
|
90
140
|
getSubscriptions: getSubscriptionsInternal,
|
|
91
|
-
requestPurchase:
|
|
141
|
+
requestPurchase: requestPurchaseWithReset,
|
|
92
142
|
};
|
|
93
143
|
}
|
|
94
144
|
//# sourceMappingURL=useIap.js.map
|
package/build/useIap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIap.js","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n endConnection,\n initConnection,\n purchaseErrorListener,\n purchaseUpdatedListener,\n transactionUpdatedIos,\n getProducts,\n getAvailablePurchases,\n getPurchaseHistory,\n finishTransaction as finishTransactionInternal,\n getSubscriptions,\n requestPurchase as requestPurchaseInternal,\n} from './';\nimport {useCallback, useEffect, useState, useRef} from 'react';\nimport {\n Product,\n ProductPurchase,\n Purchase,\n PurchaseError,\n PurchaseResult,\n SubscriptionProduct,\n SubscriptionPurchase,\n} from './ExpoIap.types';\nimport {TransactionEvent} from './modules/ios';\nimport {Subscription} from 'expo-modules-core';\nimport {Platform} from 'react-native';\n\ntype UseIap = {\n connected: boolean;\n products: Product[];\n promotedProductsIOS: ProductPurchase[];\n subscriptions: SubscriptionProduct[];\n purchaseHistories: ProductPurchase[];\n availablePurchases: ProductPurchase[];\n currentPurchase?: ProductPurchase;\n currentPurchaseError?: PurchaseError;\n finishTransaction: ({\n purchase,\n isConsumable,\n }: {\n purchase: Purchase;\n isConsumable?: boolean;\n }) => Promise<string | boolean | PurchaseResult | void>;\n getAvailablePurchases: (skus: string[]) => Promise<void>;\n getPurchaseHistories: (skus: string[]) => Promise<void>;\n getProducts: (skus: string[]) => Promise<void>;\n getSubscriptions: (skus: string[]) => Promise<void>;\n requestPurchase: typeof requestPurchaseInternal;\n};\n\nexport function useIAP(): UseIap {\n const [connected, setConnected] = useState<boolean>(false);\n const [products, setProducts] = useState<Product[]>([]);\n const [promotedProductsIOS, setPromotedProductsIOS] = useState<\n ProductPurchase[]\n >([]);\n const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);\n const [purchaseHistories, setPurchaseHistories] = useState<ProductPurchase[]>(\n [],\n );\n const [availablePurchases, setAvailablePurchases] = useState<\n ProductPurchase[]\n >([]);\n const [currentPurchase, setCurrentPurchase] = useState<ProductPurchase>();\n const [currentPurchaseError, setCurrentPurchaseError] =\n useState<PurchaseError>();\n\n const subscriptionsRef = useRef<{\n purchaseUpdate?: Subscription;\n purchaseError?: Subscription;\n promotedProductsIos?: Subscription;\n }>({});\n\n const getProductsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setProducts(await getProducts(skus));\n },\n [],\n );\n\n const getSubscriptionsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setSubscriptions(await getSubscriptions(skus));\n },\n [],\n );\n\n const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {\n setAvailablePurchases(await getAvailablePurchases());\n }, []);\n\n const getPurchaseHistoriesInternal = useCallback(async (): Promise<void> => {\n setPurchaseHistories(await getPurchaseHistory());\n }, []);\n\n const finishTransaction = useCallback(\n async ({\n purchase,\n isConsumable,\n }: {\n purchase: ProductPurchase;\n isConsumable?: boolean;\n }): Promise<string | boolean | PurchaseResult | void> => {\n try {\n return await finishTransactionInternal({\n purchase,\n isConsumable,\n });\n } catch (err) {\n throw err;\n } finally {\n if (purchase.id === currentPurchase?.id) {\n setCurrentPurchase(undefined);\n }\n if (purchase.id === currentPurchaseError?.productId) {\n setCurrentPurchaseError(undefined);\n }\n }\n },\n [currentPurchase?.id, currentPurchaseError?.productId],\n );\n\n const initIapWithSubscriptions = useCallback(async (): Promise<void> => {\n const result = await initConnection();\n setConnected(result);\n\n if (result) {\n subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(\n async (purchase: Purchase | SubscriptionPurchase) => {\n setCurrentPurchaseError(undefined);\n setCurrentPurchase(purchase);\n },\n );\n\n subscriptionsRef.current.purchaseError = purchaseErrorListener(\n (error: PurchaseError) => {\n setCurrentPurchase(undefined);\n setCurrentPurchaseError(error);\n },\n );\n\n if (Platform.OS === 'ios') {\n subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos(\n (event: TransactionEvent) => {\n setPromotedProductsIOS((prevProducts) =>\n event.transaction\n ? [...prevProducts, event.transaction]\n : prevProducts,\n );\n },\n );\n }\n }\n }, []);\n\n useEffect(() => {\n initIapWithSubscriptions();\n const currentSubscriptions = subscriptionsRef.current;\n\n return () => {\n currentSubscriptions.purchaseUpdate?.remove();\n currentSubscriptions.purchaseError?.remove();\n currentSubscriptions.promotedProductsIos?.remove();\n endConnection();\n setConnected(false);\n };\n }, [initIapWithSubscriptions]);\n\n return {\n connected,\n products,\n promotedProductsIOS,\n subscriptions,\n purchaseHistories,\n finishTransaction,\n availablePurchases,\n currentPurchase,\n currentPurchaseError,\n getAvailablePurchases: getAvailablePurchasesInternal,\n getPurchaseHistories: getPurchaseHistoriesInternal,\n getProducts: getProductsInternal,\n getSubscriptions: getSubscriptionsInternal,\n requestPurchase: requestPurchaseInternal,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"useIap.js","sourceRoot":"","sources":["../src/useIap.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n endConnection,\n initConnection,\n purchaseErrorListener,\n purchaseUpdatedListener,\n transactionUpdatedIos,\n getProducts,\n getAvailablePurchases,\n getPurchaseHistory,\n finishTransaction as finishTransactionInternal,\n getSubscriptions,\n requestPurchase as requestPurchaseInternal,\n sync,\n} from './';\nimport {useCallback, useEffect, useState, useRef} from 'react';\nimport {\n Product,\n ProductPurchase,\n Purchase,\n PurchaseError,\n PurchaseResult,\n SubscriptionProduct,\n SubscriptionPurchase,\n} from './ExpoIap.types';\nimport {TransactionEvent} from './modules/ios';\nimport {Subscription} from 'expo-modules-core';\nimport {Platform} from 'react-native';\n\ntype UseIap = {\n connected: boolean;\n products: Product[];\n promotedProductsIOS: ProductPurchase[];\n subscriptions: SubscriptionProduct[];\n purchaseHistories: ProductPurchase[];\n availablePurchases: ProductPurchase[];\n currentPurchase?: ProductPurchase;\n currentPurchaseError?: PurchaseError;\n clearCurrentPurchase: () => void;\n clearCurrentPurchaseError: () => void;\n finishTransaction: ({\n purchase,\n isConsumable,\n }: {\n purchase: Purchase;\n isConsumable?: boolean;\n }) => Promise<string | boolean | PurchaseResult | void>;\n getAvailablePurchases: (skus: string[]) => Promise<void>;\n getPurchaseHistories: (skus: string[]) => Promise<void>;\n getProducts: (skus: string[]) => Promise<void>;\n getSubscriptions: (skus: string[]) => Promise<void>;\n requestPurchase: typeof requestPurchaseInternal;\n};\n\nexport interface UseIAPOptions {\n onPurchaseSuccess?: (purchase: ProductPurchase | SubscriptionPurchase) => void;\n onPurchaseError?: (error: PurchaseError) => void;\n}\n\nexport function useIAP(options?: UseIAPOptions): UseIap {\n const [connected, setConnected] = useState<boolean>(false);\n const [products, setProducts] = useState<Product[]>([]);\n const [promotedProductsIOS, setPromotedProductsIOS] = useState<\n ProductPurchase[]\n >([]);\n const [subscriptions, setSubscriptions] = useState<SubscriptionProduct[]>([]);\n const [purchaseHistories, setPurchaseHistories] = useState<ProductPurchase[]>(\n [],\n );\n const [availablePurchases, setAvailablePurchases] = useState<\n ProductPurchase[]\n >([]);\n const [currentPurchase, setCurrentPurchase] = useState<ProductPurchase>();\n const [currentPurchaseError, setCurrentPurchaseError] =\n useState<PurchaseError>();\n\n const optionsRef = useRef<UseIAPOptions | undefined>(options);\n\n useEffect(() => {\n optionsRef.current = options;\n }, [options]);\n\n const subscriptionsRef = useRef<{\n purchaseUpdate?: Subscription;\n purchaseError?: Subscription;\n promotedProductsIos?: Subscription;\n }>({});\n\n const clearCurrentPurchase = useCallback(() => {\n setCurrentPurchase(undefined);\n }, []);\n\n const clearCurrentPurchaseError = useCallback(() => {\n setCurrentPurchaseError(undefined);\n }, []);\n\n const getProductsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setProducts(await getProducts(skus));\n },\n [],\n );\n\n const getSubscriptionsInternal = useCallback(\n async (skus: string[]): Promise<void> => {\n setSubscriptions(await getSubscriptions(skus));\n },\n [],\n );\n\n const getAvailablePurchasesInternal = useCallback(async (): Promise<void> => {\n setAvailablePurchases(await getAvailablePurchases());\n }, []);\n\n const getPurchaseHistoriesInternal = useCallback(async (): Promise<void> => {\n setPurchaseHistories(await getPurchaseHistory());\n }, []);\n\n const finishTransaction = useCallback(\n async ({\n purchase,\n isConsumable,\n }: {\n purchase: ProductPurchase;\n isConsumable?: boolean;\n }): Promise<string | boolean | PurchaseResult | void> => {\n try {\n return await finishTransactionInternal({\n purchase,\n isConsumable,\n });\n } catch (err) {\n throw err;\n } finally {\n if (purchase.id === currentPurchase?.id) {\n clearCurrentPurchase();\n }\n if (purchase.id === currentPurchaseError?.productId) {\n clearCurrentPurchaseError();\n }\n }\n },\n [currentPurchase?.id, currentPurchaseError?.productId, clearCurrentPurchase, clearCurrentPurchaseError],\n );\n\n const requestPurchaseWithReset = useCallback(\n async (requestObj: Parameters<typeof requestPurchaseInternal>[0]) => {\n clearCurrentPurchase();\n clearCurrentPurchaseError();\n \n try {\n return await requestPurchaseInternal(requestObj);\n } catch (error) {\n throw error;\n }\n },\n [clearCurrentPurchase, clearCurrentPurchaseError],\n );\n\n const refreshSubscriptionStatus = useCallback(async (productId: string) => {\n try {\n if (Platform.OS === 'ios') {\n await sync().catch(() => {\n // Ignore errors as sync might require user password\n });\n }\n \n if (subscriptions.some(sub => sub.id === productId)) {\n await getSubscriptionsInternal([productId]);\n await getAvailablePurchasesInternal();\n }\n } catch (error) {\n console.warn('Failed to refresh subscription status:', error);\n }\n }, [getSubscriptionsInternal, getAvailablePurchasesInternal, subscriptions]);\n\n const initIapWithSubscriptions = useCallback(async (): Promise<void> => {\n const result = await initConnection();\n setConnected(result);\n\n if (result) {\n subscriptionsRef.current.purchaseUpdate = purchaseUpdatedListener(\n async (purchase: Purchase | SubscriptionPurchase) => {\n setCurrentPurchaseError(undefined);\n setCurrentPurchase(purchase);\n\n if ('expirationDateIos' in purchase) {\n await refreshSubscriptionStatus(purchase.id);\n }\n\n if (optionsRef.current?.onPurchaseSuccess) {\n optionsRef.current.onPurchaseSuccess(purchase);\n }\n },\n );\n\n subscriptionsRef.current.purchaseError = purchaseErrorListener(\n (error: PurchaseError) => {\n setCurrentPurchase(undefined);\n setCurrentPurchaseError(error);\n\n if (optionsRef.current?.onPurchaseError) {\n optionsRef.current.onPurchaseError(error);\n }\n },\n );\n\n if (Platform.OS === 'ios') {\n subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos(\n async (event: TransactionEvent) => {\n if (event.transaction) {\n setPromotedProductsIOS((prevProducts) => \n [...prevProducts, event.transaction!]\n );\n \n if ('expirationDateIos' in event.transaction) {\n await refreshSubscriptionStatus(event.transaction.id);\n }\n }\n },\n );\n }\n }\n }, [refreshSubscriptionStatus]);\n\n useEffect(() => {\n initIapWithSubscriptions();\n const currentSubscriptions = subscriptionsRef.current;\n\n return () => {\n currentSubscriptions.purchaseUpdate?.remove();\n currentSubscriptions.purchaseError?.remove();\n currentSubscriptions.promotedProductsIos?.remove();\n endConnection();\n setConnected(false);\n };\n }, [initIapWithSubscriptions]);\n\n return {\n connected,\n products,\n promotedProductsIOS,\n subscriptions,\n purchaseHistories,\n finishTransaction,\n availablePurchases,\n currentPurchase,\n currentPurchaseError,\n clearCurrentPurchase,\n clearCurrentPurchaseError,\n getAvailablePurchases: getAvailablePurchasesInternal,\n getPurchaseHistories: getPurchaseHistoriesInternal,\n getProducts: getProductsInternal,\n getSubscriptions: getSubscriptionsInternal,\n requestPurchase: requestPurchaseWithReset,\n };\n}\n"]}
|
package/iap.md
CHANGED
|
@@ -209,8 +209,8 @@ Transactions map to `Purchase` or `SubscriptionPurchase` with platform-specific
|
|
|
209
209
|
Below is a simple example of fetching products and making a purchase with `expo-iap` in a managed workflow, updated to use the new `requestPurchase` signature:
|
|
210
210
|
|
|
211
211
|
```tsx
|
|
212
|
-
import {useEffect, useState} from 'react';
|
|
213
|
-
import {Button, Text, View} from 'react-native';
|
|
212
|
+
import { useEffect, useState } from 'react';
|
|
213
|
+
import { Button, Text, View } from 'react-native';
|
|
214
214
|
import {
|
|
215
215
|
initConnection,
|
|
216
216
|
endConnection,
|
|
@@ -237,7 +237,7 @@ export default function SimpleIAP() {
|
|
|
237
237
|
|
|
238
238
|
const purchaseListener = purchaseUpdatedListener(async (purchase) => {
|
|
239
239
|
if (purchase) {
|
|
240
|
-
await finishTransaction({purchase, isConsumable: true});
|
|
240
|
+
await finishTransaction({ purchase, isConsumable: true });
|
|
241
241
|
alert('Purchase completed!');
|
|
242
242
|
}
|
|
243
243
|
});
|
|
@@ -252,12 +252,12 @@ export default function SimpleIAP() {
|
|
|
252
252
|
const buyItem = async () => {
|
|
253
253
|
if (!product) return;
|
|
254
254
|
await requestPurchase({
|
|
255
|
-
request: {skus: [product.id]}, // Android expects 'skus'; iOS would use 'sku'
|
|
255
|
+
request: { skus: [product.id] }, // Android expects 'skus'; iOS would use 'sku'
|
|
256
256
|
});
|
|
257
257
|
};
|
|
258
258
|
|
|
259
259
|
return (
|
|
260
|
-
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
|
|
260
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
261
261
|
<Text>{isConnected ? 'Connected' : 'Connecting...'}</Text>
|
|
262
262
|
{product ? (
|
|
263
263
|
<>
|
|
@@ -277,7 +277,7 @@ export default function SimpleIAP() {
|
|
|
277
277
|
The `useIAP` hook simplifies managing in-app purchases. Below is an example updated to use the new `requestPurchase` signature:
|
|
278
278
|
|
|
279
279
|
```tsx
|
|
280
|
-
import {useEffect, useState} from 'react';
|
|
280
|
+
import { useEffect, useState } from 'react';
|
|
281
281
|
import {
|
|
282
282
|
SafeAreaView,
|
|
283
283
|
ScrollView,
|
|
@@ -289,20 +289,12 @@ import {
|
|
|
289
289
|
InteractionManager,
|
|
290
290
|
Alert,
|
|
291
291
|
} from 'react-native';
|
|
292
|
-
import {useIAP} from 'expo-iap';
|
|
293
|
-
import type {ProductPurchase, SubscriptionProduct} from 'expo-iap';
|
|
292
|
+
import { useIAP } from 'expo-iap';
|
|
293
|
+
import type { ProductPurchase, SubscriptionProduct } from 'expo-iap';
|
|
294
294
|
|
|
295
295
|
// Define SKUs
|
|
296
|
-
const productSkus = [
|
|
297
|
-
|
|
298
|
-
'cpk.points.5000',
|
|
299
|
-
'cpk.points.10000',
|
|
300
|
-
'cpk.points.30000',
|
|
301
|
-
];
|
|
302
|
-
const subscriptionSkus = [
|
|
303
|
-
'cpk.membership.monthly.bronze',
|
|
304
|
-
'cpk.membership.monthly.silver',
|
|
305
|
-
];
|
|
296
|
+
const productSkus = ['cpk.points.1000', 'cpk.points.5000', 'cpk.points.10000', 'cpk.points.30000'];
|
|
297
|
+
const subscriptionSkus = ['cpk.membership.monthly.bronze', 'cpk.membership.monthly.silver'];
|
|
306
298
|
|
|
307
299
|
// Define operations
|
|
308
300
|
const operations = ['getProducts', 'getSubscriptions'] as const;
|
|
@@ -329,10 +321,7 @@ export default function IAPWithHook() {
|
|
|
329
321
|
|
|
330
322
|
const initializeIAP = async () => {
|
|
331
323
|
try {
|
|
332
|
-
await Promise.all([
|
|
333
|
-
getProducts(productSkus),
|
|
334
|
-
getSubscriptions(subscriptionSkus),
|
|
335
|
-
]);
|
|
324
|
+
await Promise.all([getProducts(productSkus), getSubscriptions(subscriptionSkus)]);
|
|
336
325
|
setIsReady(true);
|
|
337
326
|
} catch (error) {
|
|
338
327
|
console.error('Error initializing IAP:', error);
|
|
@@ -400,10 +389,7 @@ export default function IAPWithHook() {
|
|
|
400
389
|
<View style={styles.buttons}>
|
|
401
390
|
<ScrollView contentContainerStyle={styles.buttonsWrapper} horizontal>
|
|
402
391
|
{operations.map((operation) => (
|
|
403
|
-
<Pressable
|
|
404
|
-
key={operation}
|
|
405
|
-
onPress={() => handleOperation(operation)}
|
|
406
|
-
>
|
|
392
|
+
<Pressable key={operation} onPress={() => handleOperation(operation)}>
|
|
407
393
|
<View style={styles.buttonView}>
|
|
408
394
|
<Text>{operation}</Text>
|
|
409
395
|
</View>
|
|
@@ -415,10 +401,10 @@ export default function IAPWithHook() {
|
|
|
415
401
|
{!isReady ? (
|
|
416
402
|
<Text>Loading...</Text>
|
|
417
403
|
) : (
|
|
418
|
-
<View style={{gap: 12}}>
|
|
419
|
-
<Text style={{fontSize: 20}}>Products</Text>
|
|
404
|
+
<View style={{ gap: 12 }}>
|
|
405
|
+
<Text style={{ fontSize: 20 }}>Products</Text>
|
|
420
406
|
{products.map((item) => (
|
|
421
|
-
<View key={item.id} style={{gap: 12}}>
|
|
407
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
422
408
|
<Text>
|
|
423
409
|
{item.title} -{' '}
|
|
424
410
|
{item.platform === 'android'
|
|
@@ -429,24 +415,20 @@ export default function IAPWithHook() {
|
|
|
429
415
|
title="Buy"
|
|
430
416
|
onPress={() =>
|
|
431
417
|
requestPurchase({
|
|
432
|
-
request:
|
|
433
|
-
item.platform === 'android'
|
|
434
|
-
? {skus: [item.id]}
|
|
435
|
-
: {sku: item.id},
|
|
418
|
+
request: item.platform === 'android' ? { skus: [item.id] } : { sku: item.id },
|
|
436
419
|
})
|
|
437
420
|
}
|
|
438
421
|
/>
|
|
439
422
|
</View>
|
|
440
423
|
))}
|
|
441
424
|
|
|
442
|
-
<Text style={{fontSize: 20}}>Subscriptions</Text>
|
|
425
|
+
<Text style={{ fontSize: 20 }}>Subscriptions</Text>
|
|
443
426
|
{subscriptions.map((item) => (
|
|
444
|
-
<View key={item.id} style={{gap: 12}}>
|
|
427
|
+
<View key={item.id} style={{ gap: 12 }}>
|
|
445
428
|
<Text>
|
|
446
429
|
{item.title || item.displayName} -{' '}
|
|
447
430
|
{item.platform === 'android' && item.subscriptionOfferDetails
|
|
448
|
-
? item.subscriptionOfferDetails[0]?.pricingPhases
|
|
449
|
-
.pricingPhaseList[0].formattedPrice
|
|
431
|
+
? item.subscriptionOfferDetails[0]?.pricingPhases.pricingPhaseList[0].formattedPrice
|
|
450
432
|
: item.displayPrice}
|
|
451
433
|
</Text>
|
|
452
434
|
<Button
|
|
@@ -463,7 +445,7 @@ export default function IAPWithHook() {
|
|
|
463
445
|
offerToken: offer.offerToken,
|
|
464
446
|
})) || [],
|
|
465
447
|
}
|
|
466
|
-
: {sku: item.id},
|
|
448
|
+
: { sku: item.id },
|
|
467
449
|
type: 'subs',
|
|
468
450
|
})
|
|
469
451
|
}
|
package/ios/ExpoIapModule.swift
CHANGED
|
@@ -172,6 +172,8 @@ public class ExpoIapModule: Module {
|
|
|
172
172
|
private var productStore: ProductStore?
|
|
173
173
|
private var hasListeners = false
|
|
174
174
|
private var updateListenerTask: Task<Void, Error>?
|
|
175
|
+
private var subscriptionPollingTask: Task<Void, Error>?
|
|
176
|
+
private var pollingSkus: Set<String> = []
|
|
175
177
|
|
|
176
178
|
public func definition() -> ModuleDefinition {
|
|
177
179
|
Name("ExpoIap")
|
|
@@ -560,7 +562,17 @@ public class ExpoIapModule: Module {
|
|
|
560
562
|
"Cannot find window scene or not available on macOS"
|
|
561
563
|
])
|
|
562
564
|
}
|
|
565
|
+
|
|
566
|
+
// Get all subscription products before showing the management UI
|
|
567
|
+
let subscriptionSkus = await self.getAllSubscriptionProductIds()
|
|
568
|
+
self.pollingSkus = Set(subscriptionSkus)
|
|
569
|
+
|
|
570
|
+
// Show the management UI
|
|
563
571
|
try await AppStore.showManageSubscriptions(in: windowScene)
|
|
572
|
+
|
|
573
|
+
// Start polling for status changes
|
|
574
|
+
self.pollForSubscriptionStatusChanges()
|
|
575
|
+
|
|
564
576
|
return true
|
|
565
577
|
#else
|
|
566
578
|
throw NSError(
|
|
@@ -698,4 +710,70 @@ public class ExpoIapModule: Module {
|
|
|
698
710
|
return item
|
|
699
711
|
}
|
|
700
712
|
}
|
|
713
|
+
|
|
714
|
+
private func getAllSubscriptionProductIds() async -> [String] {
|
|
715
|
+
guard let productStore = self.productStore else { return [] }
|
|
716
|
+
let products = await productStore.getAllProducts()
|
|
717
|
+
return products.compactMap { product in
|
|
718
|
+
if product.subscription != nil {
|
|
719
|
+
return product.id
|
|
720
|
+
}
|
|
721
|
+
return nil
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private func pollForSubscriptionStatusChanges() {
|
|
726
|
+
subscriptionPollingTask?.cancel()
|
|
727
|
+
|
|
728
|
+
subscriptionPollingTask = Task {
|
|
729
|
+
try? await Task.sleep(nanoseconds: 1_500_000_000) // 1.5 seconds
|
|
730
|
+
|
|
731
|
+
var previousStatuses: [String: Product.SubscriptionInfo.RenewalState] = [:]
|
|
732
|
+
|
|
733
|
+
for sku in self.pollingSkus {
|
|
734
|
+
guard let product = await self.productStore?.getProduct(productID: sku),
|
|
735
|
+
let status = try? await product.subscription?.status.first else { continue }
|
|
736
|
+
|
|
737
|
+
previousStatuses[sku] = status.renewalInfo.verified?.willAutoRenew == true ?
|
|
738
|
+
.willRenew : .willNotRenew
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
for _ in 1...5 {
|
|
742
|
+
try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds
|
|
743
|
+
|
|
744
|
+
if Task.isCancelled {
|
|
745
|
+
return
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
for sku in self.pollingSkus {
|
|
749
|
+
guard let product = await self.productStore?.getProduct(productID: sku),
|
|
750
|
+
let status = try? await product.subscription?.status.first,
|
|
751
|
+
let transaction = await product.latestTransaction?.verified else { continue }
|
|
752
|
+
|
|
753
|
+
let currentRenewalState = status.renewalInfo.verified?.willAutoRenew == true ?
|
|
754
|
+
Product.SubscriptionInfo.RenewalState.willRenew :
|
|
755
|
+
Product.SubscriptionInfo.RenewalState.willNotRenew
|
|
756
|
+
|
|
757
|
+
if let previousState = previousStatuses[sku],
|
|
758
|
+
previousState != currentRenewalState {
|
|
759
|
+
|
|
760
|
+
var purchaseMap = serializeTransaction(transaction)
|
|
761
|
+
|
|
762
|
+
if let renewalInfo = status.renewalInfo.verified {
|
|
763
|
+
if let renewalInfoDict = serializeRenewalInfo(.verified(renewalInfo)) {
|
|
764
|
+
purchaseMap["renewalInfo"] = renewalInfoDict
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
self.sendEvent(IapEvent.PurchaseUpdated, purchaseMap)
|
|
769
|
+
self.sendEvent(IapEvent.TransactionIapUpdated, ["transaction": purchaseMap])
|
|
770
|
+
|
|
771
|
+
previousStatuses[sku] = currentRenewalState
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
self.pollingSkus.removeAll()
|
|
777
|
+
}
|
|
778
|
+
}
|
|
701
779
|
}
|
package/package.json
CHANGED
package/src/modules/ios.ts
CHANGED
|
@@ -72,7 +72,10 @@ export const beginRefundRequest = (sku: string): Promise<RefundRequestStatus> =>
|
|
|
72
72
|
ExpoIapModule.beginRefundRequest(sku);
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
*
|
|
75
|
+
* Shows the system UI for managing subscriptions.
|
|
76
|
+
* When the user changes subscription renewal status, the system will emit events to
|
|
77
|
+
* purchaseUpdatedListener and transactionUpdatedIos listeners.
|
|
78
|
+
* @returns {Promise<null>}
|
|
76
79
|
*/
|
|
77
80
|
export const showManageSubscriptions = (): Promise<null> =>
|
|
78
81
|
ExpoIapModule.showManageSubscriptions();
|
package/src/useIap.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
finishTransaction as finishTransactionInternal,
|
|
11
11
|
getSubscriptions,
|
|
12
12
|
requestPurchase as requestPurchaseInternal,
|
|
13
|
+
sync,
|
|
13
14
|
} from './';
|
|
14
15
|
import {useCallback, useEffect, useState, useRef} from 'react';
|
|
15
16
|
import {
|
|
@@ -34,6 +35,8 @@ type UseIap = {
|
|
|
34
35
|
availablePurchases: ProductPurchase[];
|
|
35
36
|
currentPurchase?: ProductPurchase;
|
|
36
37
|
currentPurchaseError?: PurchaseError;
|
|
38
|
+
clearCurrentPurchase: () => void;
|
|
39
|
+
clearCurrentPurchaseError: () => void;
|
|
37
40
|
finishTransaction: ({
|
|
38
41
|
purchase,
|
|
39
42
|
isConsumable,
|
|
@@ -48,7 +51,12 @@ type UseIap = {
|
|
|
48
51
|
requestPurchase: typeof requestPurchaseInternal;
|
|
49
52
|
};
|
|
50
53
|
|
|
51
|
-
export
|
|
54
|
+
export interface UseIAPOptions {
|
|
55
|
+
onPurchaseSuccess?: (purchase: ProductPurchase | SubscriptionPurchase) => void;
|
|
56
|
+
onPurchaseError?: (error: PurchaseError) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useIAP(options?: UseIAPOptions): UseIap {
|
|
52
60
|
const [connected, setConnected] = useState<boolean>(false);
|
|
53
61
|
const [products, setProducts] = useState<Product[]>([]);
|
|
54
62
|
const [promotedProductsIOS, setPromotedProductsIOS] = useState<
|
|
@@ -65,12 +73,26 @@ export function useIAP(): UseIap {
|
|
|
65
73
|
const [currentPurchaseError, setCurrentPurchaseError] =
|
|
66
74
|
useState<PurchaseError>();
|
|
67
75
|
|
|
76
|
+
const optionsRef = useRef<UseIAPOptions | undefined>(options);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
optionsRef.current = options;
|
|
80
|
+
}, [options]);
|
|
81
|
+
|
|
68
82
|
const subscriptionsRef = useRef<{
|
|
69
83
|
purchaseUpdate?: Subscription;
|
|
70
84
|
purchaseError?: Subscription;
|
|
71
85
|
promotedProductsIos?: Subscription;
|
|
72
86
|
}>({});
|
|
73
87
|
|
|
88
|
+
const clearCurrentPurchase = useCallback(() => {
|
|
89
|
+
setCurrentPurchase(undefined);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const clearCurrentPurchaseError = useCallback(() => {
|
|
93
|
+
setCurrentPurchaseError(undefined);
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
74
96
|
const getProductsInternal = useCallback(
|
|
75
97
|
async (skus: string[]): Promise<void> => {
|
|
76
98
|
setProducts(await getProducts(skus));
|
|
@@ -110,16 +132,47 @@ export function useIAP(): UseIap {
|
|
|
110
132
|
throw err;
|
|
111
133
|
} finally {
|
|
112
134
|
if (purchase.id === currentPurchase?.id) {
|
|
113
|
-
|
|
135
|
+
clearCurrentPurchase();
|
|
114
136
|
}
|
|
115
137
|
if (purchase.id === currentPurchaseError?.productId) {
|
|
116
|
-
|
|
138
|
+
clearCurrentPurchaseError();
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
141
|
},
|
|
120
|
-
[currentPurchase?.id, currentPurchaseError?.productId],
|
|
142
|
+
[currentPurchase?.id, currentPurchaseError?.productId, clearCurrentPurchase, clearCurrentPurchaseError],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const requestPurchaseWithReset = useCallback(
|
|
146
|
+
async (requestObj: Parameters<typeof requestPurchaseInternal>[0]) => {
|
|
147
|
+
clearCurrentPurchase();
|
|
148
|
+
clearCurrentPurchaseError();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return await requestPurchaseInternal(requestObj);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[clearCurrentPurchase, clearCurrentPurchaseError],
|
|
121
157
|
);
|
|
122
158
|
|
|
159
|
+
const refreshSubscriptionStatus = useCallback(async (productId: string) => {
|
|
160
|
+
try {
|
|
161
|
+
if (Platform.OS === 'ios') {
|
|
162
|
+
await sync().catch(() => {
|
|
163
|
+
// Ignore errors as sync might require user password
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (subscriptions.some(sub => sub.id === productId)) {
|
|
168
|
+
await getSubscriptionsInternal([productId]);
|
|
169
|
+
await getAvailablePurchasesInternal();
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn('Failed to refresh subscription status:', error);
|
|
173
|
+
}
|
|
174
|
+
}, [getSubscriptionsInternal, getAvailablePurchasesInternal, subscriptions]);
|
|
175
|
+
|
|
123
176
|
const initIapWithSubscriptions = useCallback(async (): Promise<void> => {
|
|
124
177
|
const result = await initConnection();
|
|
125
178
|
setConnected(result);
|
|
@@ -129,6 +182,14 @@ export function useIAP(): UseIap {
|
|
|
129
182
|
async (purchase: Purchase | SubscriptionPurchase) => {
|
|
130
183
|
setCurrentPurchaseError(undefined);
|
|
131
184
|
setCurrentPurchase(purchase);
|
|
185
|
+
|
|
186
|
+
if ('expirationDateIos' in purchase) {
|
|
187
|
+
await refreshSubscriptionStatus(purchase.id);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (optionsRef.current?.onPurchaseSuccess) {
|
|
191
|
+
optionsRef.current.onPurchaseSuccess(purchase);
|
|
192
|
+
}
|
|
132
193
|
},
|
|
133
194
|
);
|
|
134
195
|
|
|
@@ -136,22 +197,30 @@ export function useIAP(): UseIap {
|
|
|
136
197
|
(error: PurchaseError) => {
|
|
137
198
|
setCurrentPurchase(undefined);
|
|
138
199
|
setCurrentPurchaseError(error);
|
|
200
|
+
|
|
201
|
+
if (optionsRef.current?.onPurchaseError) {
|
|
202
|
+
optionsRef.current.onPurchaseError(error);
|
|
203
|
+
}
|
|
139
204
|
},
|
|
140
205
|
);
|
|
141
206
|
|
|
142
207
|
if (Platform.OS === 'ios') {
|
|
143
208
|
subscriptionsRef.current.promotedProductsIos = transactionUpdatedIos(
|
|
144
|
-
(event: TransactionEvent) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
209
|
+
async (event: TransactionEvent) => {
|
|
210
|
+
if (event.transaction) {
|
|
211
|
+
setPromotedProductsIOS((prevProducts) =>
|
|
212
|
+
[...prevProducts, event.transaction!]
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
if ('expirationDateIos' in event.transaction) {
|
|
216
|
+
await refreshSubscriptionStatus(event.transaction.id);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
150
219
|
},
|
|
151
220
|
);
|
|
152
221
|
}
|
|
153
222
|
}
|
|
154
|
-
}, []);
|
|
223
|
+
}, [refreshSubscriptionStatus]);
|
|
155
224
|
|
|
156
225
|
useEffect(() => {
|
|
157
226
|
initIapWithSubscriptions();
|
|
@@ -176,10 +245,12 @@ export function useIAP(): UseIap {
|
|
|
176
245
|
availablePurchases,
|
|
177
246
|
currentPurchase,
|
|
178
247
|
currentPurchaseError,
|
|
248
|
+
clearCurrentPurchase,
|
|
249
|
+
clearCurrentPurchaseError,
|
|
179
250
|
getAvailablePurchases: getAvailablePurchasesInternal,
|
|
180
251
|
getPurchaseHistories: getPurchaseHistoriesInternal,
|
|
181
252
|
getProducts: getProductsInternal,
|
|
182
253
|
getSubscriptions: getSubscriptionsInternal,
|
|
183
|
-
requestPurchase:
|
|
254
|
+
requestPurchase: requestPurchaseWithReset,
|
|
184
255
|
};
|
|
185
256
|
}
|