expo-iap 3.0.7 → 3.0.8
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/CLAUDE.md +12 -0
- package/android/build.gradle +1 -1
- package/build/index.d.ts +14 -66
- package/build/index.d.ts.map +1 -1
- package/build/index.js +149 -151
- package/build/index.js.map +1 -1
- package/build/modules/android.d.ts +7 -12
- package/build/modules/android.d.ts.map +1 -1
- package/build/modules/android.js +13 -11
- package/build/modules/android.js.map +1 -1
- package/build/modules/ios.d.ts +19 -35
- package/build/modules/ios.d.ts.map +1 -1
- package/build/modules/ios.js +86 -33
- package/build/modules/ios.js.map +1 -1
- package/build/types.d.ts +99 -76
- package/build/types.d.ts.map +1 -1
- package/build/types.js +1 -0
- package/build/types.js.map +1 -1
- package/build/useIAP.d.ts +6 -11
- package/build/useIAP.d.ts.map +1 -1
- package/build/useIAP.js +44 -15
- package/build/useIAP.js.map +1 -1
- package/build/utils/purchase.d.ts +9 -0
- package/build/utils/purchase.d.ts.map +1 -0
- package/build/utils/purchase.js +34 -0
- package/build/utils/purchase.js.map +1 -0
- package/package.json +2 -2
- package/plugin/build/withIAP.js +3 -3
- package/plugin/src/withIAP.ts +3 -3
- package/src/index.ts +217 -250
- package/src/modules/android.ts +23 -22
- package/src/modules/ios.ts +123 -45
- package/src/types.ts +131 -85
- package/src/useIAP.ts +83 -42
- package/src/utils/purchase.ts +52 -0
- package/.copilot-instructions.md +0 -321
- package/.cursorrules +0 -321
package/CLAUDE.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
- Subject must be imperative, lowercase, without a trailing period, and roughly 50 characters
|
|
9
9
|
- Wrap commit body lines near 72 characters and include footers such as `BREAKING CHANGE:` or `Closes #123` when needed
|
|
10
10
|
|
|
11
|
+
## Tooling & Package Management
|
|
12
|
+
|
|
13
|
+
- **Use Bun exclusively.** Run installs with `bun install`, scripts with `bun run <script>`, add deps via `bun add` / `bun add -d`.
|
|
14
|
+
- Do **not** suggest or create `package-lock.json` or `yarn.lock`; `bun.lock` is the single source of truth.
|
|
15
|
+
|
|
11
16
|
## Expo-Specific Guidelines
|
|
12
17
|
|
|
13
18
|
### iOS Pod Configuration
|
|
@@ -38,6 +43,11 @@ Before committing any changes:
|
|
|
38
43
|
|
|
39
44
|
### Platform-Specific Naming Conventions
|
|
40
45
|
|
|
46
|
+
#### Function Naming
|
|
47
|
+
|
|
48
|
+
- Functions that only operate on one platform must carry the suffix: `nameIOS` or `nameAndroid` (e.g. `getStorefrontIOS`, `deepLinkToSubscriptionsAndroid`).
|
|
49
|
+
- Cross-platform helpers should expose a single name and branch internally via `Platform.select` or equivalent.
|
|
50
|
+
|
|
41
51
|
#### Field Naming
|
|
42
52
|
|
|
43
53
|
- **iOS-related fields**: Use `IOS` suffix (e.g., `displayNameIOS`, `discountsIOS`, `introductoryPriceIOS`)
|
|
@@ -72,6 +82,8 @@ The library follows the OpenIAP type specifications with platform-specific exten
|
|
|
72
82
|
|
|
73
83
|
> **Important:** `src/types.ts` is generated from the OpenIAP schema. Never edit this file manually or commit hand-written changes. After updating any `*.graphql` schema, run `bun run generate:types` (or the equivalent script in your package manager) to refresh the file.
|
|
74
84
|
|
|
85
|
+
- Whenever you need Request/Params/Result types in the JS API surface (`src/index.ts`, hooks, modules, examples), import them directly from the generated `src/types.ts` (e.g., `MutationRequestPurchaseArgs`, `QueryFetchProductsArgs`). Bind exported functions with the generated `QueryField` / `MutationField` helpers so their signatures stay in lockstep with `types.ts` instead of redefining ad-hoc unions like `ProductTypeInput`.
|
|
86
|
+
|
|
75
87
|
### React/JSX Conventions
|
|
76
88
|
|
|
77
89
|
- **Conditional Rendering**: Use ternary operator with null instead of logical AND
|
package/android/build.gradle
CHANGED
|
@@ -58,6 +58,6 @@ dependencies {
|
|
|
58
58
|
implementation project(":openiap-google")
|
|
59
59
|
} else {
|
|
60
60
|
// Fallback to published artifact when local project isn't linked
|
|
61
|
-
implementation "io.github.hyochan.openiap:openiap-google:1.1.
|
|
61
|
+
implementation "io.github.hyochan.openiap:openiap-google:1.1.12"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MutationField, Product, ProductQueryType, Purchase, QueryField } from './types';
|
|
2
2
|
import { PurchaseError } from './purchase-error';
|
|
3
3
|
export * from './types';
|
|
4
4
|
export { ErrorCodeUtils, ErrorCodeMapping } from './purchase-error';
|
|
@@ -28,24 +28,7 @@ export declare const emitter: ExpoIapEmitter;
|
|
|
28
28
|
/**
|
|
29
29
|
* TODO(v3.1.0): Remove legacy 'inapp' alias once downstream apps migrate to 'in-app'.
|
|
30
30
|
*/
|
|
31
|
-
export type ProductTypeInput =
|
|
32
|
-
export type InAppTypeInput = Exclude<ProductTypeInput, 'subs'>;
|
|
33
|
-
type PurchaseRequestInApp = {
|
|
34
|
-
request: RequestPurchasePropsByPlatforms;
|
|
35
|
-
type?: InAppTypeInput;
|
|
36
|
-
};
|
|
37
|
-
type PurchaseRequestSubscription = {
|
|
38
|
-
request: RequestSubscriptionPropsByPlatforms;
|
|
39
|
-
type: 'subs';
|
|
40
|
-
};
|
|
41
|
-
export type PurchaseRequestInput = PurchaseRequestInApp | PurchaseRequestSubscription;
|
|
42
|
-
export type PurchaseRequest = {
|
|
43
|
-
request: RequestPurchaseProps;
|
|
44
|
-
type?: InAppTypeInput;
|
|
45
|
-
} | {
|
|
46
|
-
request: RequestSubscriptionPropsByPlatforms;
|
|
47
|
-
type: 'subs';
|
|
48
|
-
};
|
|
31
|
+
export type ProductTypeInput = ProductQueryType | 'inapp';
|
|
49
32
|
export declare const purchaseUpdatedListener: (listener: (event: Purchase) => void) => {
|
|
50
33
|
remove: () => void;
|
|
51
34
|
};
|
|
@@ -75,38 +58,17 @@ export declare const purchaseErrorListener: (listener: (error: PurchaseError) =>
|
|
|
75
58
|
export declare const promotedProductListenerIOS: (listener: (product: Product) => void) => {
|
|
76
59
|
remove: () => void;
|
|
77
60
|
};
|
|
78
|
-
export declare
|
|
79
|
-
export declare
|
|
61
|
+
export declare const initConnection: MutationField<'initConnection'>;
|
|
62
|
+
export declare const endConnection: MutationField<'endConnection'>;
|
|
80
63
|
/**
|
|
81
64
|
* Fetch products with unified API (v2.7.0+)
|
|
82
65
|
*
|
|
83
|
-
* @param
|
|
84
|
-
* @param
|
|
85
|
-
* @param
|
|
86
|
-
*
|
|
87
|
-
* @example
|
|
88
|
-
* ```typescript
|
|
89
|
-
* // Regular products
|
|
90
|
-
* const products = await fetchProducts({
|
|
91
|
-
* skus: ['product1', 'product2'],
|
|
92
|
-
* type: 'in-app'
|
|
93
|
-
* });
|
|
94
|
-
*
|
|
95
|
-
* // Subscriptions
|
|
96
|
-
* const subscriptions = await fetchProducts({
|
|
97
|
-
* skus: ['sub1', 'sub2'],
|
|
98
|
-
* type: 'subs'
|
|
99
|
-
* });
|
|
100
|
-
* ```
|
|
66
|
+
* @param request - Product fetch configuration
|
|
67
|
+
* @param request.skus - Array of product SKUs to fetch
|
|
68
|
+
* @param request.type - Product query type: 'in-app', 'subs', or 'all'
|
|
101
69
|
*/
|
|
102
|
-
export declare const fetchProducts:
|
|
103
|
-
|
|
104
|
-
type?: ProductTypeInput;
|
|
105
|
-
}) => Promise<Product[] | ProductSubscription[]>;
|
|
106
|
-
export declare const getAvailablePurchases: ({ alsoPublishToEventListenerIOS, onlyIncludeActiveItemsIOS, }?: {
|
|
107
|
-
alsoPublishToEventListenerIOS?: boolean;
|
|
108
|
-
onlyIncludeActiveItemsIOS?: boolean;
|
|
109
|
-
}) => Promise<Purchase[]>;
|
|
70
|
+
export declare const fetchProducts: QueryField<'fetchProducts'>;
|
|
71
|
+
export declare const getAvailablePurchases: QueryField<'getAvailablePurchases'>;
|
|
110
72
|
/**
|
|
111
73
|
* Restore completed transactions (cross-platform behavior)
|
|
112
74
|
*
|
|
@@ -120,10 +82,7 @@ export declare const getAvailablePurchases: ({ alsoPublishToEventListenerIOS, on
|
|
|
120
82
|
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
121
83
|
* @returns Promise resolving to the list of available/restored purchases
|
|
122
84
|
*/
|
|
123
|
-
export declare const restorePurchases:
|
|
124
|
-
alsoPublishToEventListenerIOS?: boolean;
|
|
125
|
-
onlyIncludeActiveItemsIOS?: boolean;
|
|
126
|
-
}) => Promise<Purchase[]>;
|
|
85
|
+
export declare const restorePurchases: MutationField<'restorePurchases'>;
|
|
127
86
|
/**
|
|
128
87
|
* Request a purchase for products or subscriptions.
|
|
129
88
|
*
|
|
@@ -155,11 +114,8 @@ export declare const restorePurchases: (options?: {
|
|
|
155
114
|
* });
|
|
156
115
|
* ```
|
|
157
116
|
*/
|
|
158
|
-
export declare const requestPurchase:
|
|
159
|
-
export declare const finishTransaction:
|
|
160
|
-
purchase: Purchase;
|
|
161
|
-
isConsumable?: boolean;
|
|
162
|
-
}) => Promise<VoidResult | boolean>;
|
|
117
|
+
export declare const requestPurchase: MutationField<'requestPurchase'>;
|
|
118
|
+
export declare const finishTransaction: MutationField<'finishTransaction'>;
|
|
163
119
|
/**
|
|
164
120
|
* Retrieves the current storefront information from iOS App Store
|
|
165
121
|
*
|
|
@@ -191,12 +147,7 @@ export declare const getStorefront: () => Promise<string>;
|
|
|
191
147
|
* - iOS: Send receipt data to Apple's verification endpoint from your server
|
|
192
148
|
* - Android: Use Google Play Developer API with service account credentials
|
|
193
149
|
*/
|
|
194
|
-
export declare const validateReceipt:
|
|
195
|
-
packageName: string;
|
|
196
|
-
productToken: string;
|
|
197
|
-
accessToken: string;
|
|
198
|
-
isSub?: boolean;
|
|
199
|
-
}) => Promise<ReceiptValidationResult>;
|
|
150
|
+
export declare const validateReceipt: MutationField<'validateReceipt'>;
|
|
200
151
|
/**
|
|
201
152
|
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
202
153
|
* @param options.skuAndroid - Required for Android to locate specific subscription (ignored on iOS)
|
|
@@ -215,10 +166,7 @@ export declare const validateReceipt: (sku: string, androidOptions?: {
|
|
|
215
166
|
* packageNameAndroid: 'com.example.app'
|
|
216
167
|
* });
|
|
217
168
|
*/
|
|
218
|
-
export declare const deepLinkToSubscriptions:
|
|
219
|
-
skuAndroid?: string;
|
|
220
|
-
packageNameAndroid?: string;
|
|
221
|
-
}) => Promise<void>;
|
|
169
|
+
export declare const deepLinkToSubscriptions: MutationField<'deepLinkToSubscriptions'>;
|
|
222
170
|
export * from './useIAP';
|
|
223
171
|
export * from './utils/errorMapping';
|
|
224
172
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAmBA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import { Platform } from 'react-native';
|
|
|
5
5
|
import ExpoIapModule from './ExpoIapModule';
|
|
6
6
|
import { isProductIOS, validateReceiptIOS, deepLinkToSubscriptionsIOS, syncIOS, } from './modules/ios';
|
|
7
7
|
import { isProductAndroid, validateReceiptAndroid, deepLinkToSubscriptionsAndroid, } from './modules/android';
|
|
8
|
-
|
|
9
|
-
import { ErrorCode, } from './types';
|
|
8
|
+
import { ErrorCode } from './types';
|
|
10
9
|
import { PurchaseError } from './purchase-error';
|
|
10
|
+
import { normalizePurchaseId, normalizePurchaseList } from './utils/purchase';
|
|
11
11
|
// Export all types
|
|
12
12
|
export * from './types';
|
|
13
13
|
export { ErrorCodeUtils, ErrorCodeMapping } from './purchase-error';
|
|
@@ -45,13 +45,20 @@ const normalizeProductType = (type) => {
|
|
|
45
45
|
native: 'subs',
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
if (type === 'all') {
|
|
49
|
+
return {
|
|
50
|
+
canonical: 'all',
|
|
51
|
+
native: 'all',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
48
54
|
throw new Error(`Unsupported product type: ${type}`);
|
|
49
55
|
};
|
|
50
56
|
export const purchaseUpdatedListener = (listener) => {
|
|
51
57
|
console.log('[JS] Registering purchaseUpdatedListener');
|
|
52
58
|
const wrappedListener = (event) => {
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
const normalized = normalizePurchaseId(event);
|
|
60
|
+
console.log('[JS] purchaseUpdatedListener fired:', normalized);
|
|
61
|
+
listener(normalized);
|
|
55
62
|
};
|
|
56
63
|
const emitterSubscription = emitter.addListener(OpenIapEvent.PurchaseUpdated, wrappedListener);
|
|
57
64
|
console.log('[JS] purchaseUpdatedListener registered successfully');
|
|
@@ -94,81 +101,70 @@ export const promotedProductListenerIOS = (listener) => {
|
|
|
94
101
|
}
|
|
95
102
|
return emitter.addListener(OpenIapEvent.PromotedProductIOS, listener);
|
|
96
103
|
};
|
|
97
|
-
export
|
|
98
|
-
|
|
99
|
-
return Promise.resolve(result);
|
|
100
|
-
}
|
|
101
|
-
export async function endConnection() {
|
|
102
|
-
return ExpoIapModule.endConnection();
|
|
103
|
-
}
|
|
104
|
+
export const initConnection = async () => ExpoIapModule.initConnection();
|
|
105
|
+
export const endConnection = async () => ExpoIapModule.endConnection();
|
|
104
106
|
/**
|
|
105
107
|
* Fetch products with unified API (v2.7.0+)
|
|
106
108
|
*
|
|
107
|
-
* @param
|
|
108
|
-
* @param
|
|
109
|
-
* @param
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* ```typescript
|
|
113
|
-
* // Regular products
|
|
114
|
-
* const products = await fetchProducts({
|
|
115
|
-
* skus: ['product1', 'product2'],
|
|
116
|
-
* type: 'in-app'
|
|
117
|
-
* });
|
|
118
|
-
*
|
|
119
|
-
* // Subscriptions
|
|
120
|
-
* const subscriptions = await fetchProducts({
|
|
121
|
-
* skus: ['sub1', 'sub2'],
|
|
122
|
-
* type: 'subs'
|
|
123
|
-
* });
|
|
124
|
-
* ```
|
|
109
|
+
* @param request - Product fetch configuration
|
|
110
|
+
* @param request.skus - Array of product SKUs to fetch
|
|
111
|
+
* @param request.type - Product query type: 'in-app', 'subs', or 'all'
|
|
125
112
|
*/
|
|
126
|
-
export const fetchProducts = async (
|
|
127
|
-
|
|
113
|
+
export const fetchProducts = async (request) => {
|
|
114
|
+
const { skus, type } = request ?? {};
|
|
115
|
+
if (!Array.isArray(skus) || skus.length === 0) {
|
|
128
116
|
throw new PurchaseError({
|
|
129
117
|
message: 'No SKUs provided',
|
|
130
118
|
code: ErrorCode.EmptySkuList,
|
|
131
119
|
});
|
|
132
120
|
}
|
|
133
121
|
const { canonical, native } = normalizeProductType(type);
|
|
122
|
+
const skuSet = new Set(skus);
|
|
123
|
+
const filterIosItems = (items) => items.filter((item) => {
|
|
124
|
+
if (!isProductIOS(item)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const candidate = item;
|
|
128
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
129
|
+
});
|
|
130
|
+
const filterAndroidItems = (items) => items.filter((item) => {
|
|
131
|
+
if (!isProductAndroid(item)) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const candidate = item;
|
|
135
|
+
return typeof candidate.id === 'string' && skuSet.has(candidate.id);
|
|
136
|
+
});
|
|
137
|
+
const castResult = (items) => {
|
|
138
|
+
if (canonical === 'in-app') {
|
|
139
|
+
return items;
|
|
140
|
+
}
|
|
141
|
+
if (canonical === 'subs') {
|
|
142
|
+
return items;
|
|
143
|
+
}
|
|
144
|
+
return items;
|
|
145
|
+
};
|
|
134
146
|
if (Platform.OS === 'ios') {
|
|
135
147
|
const rawItems = await ExpoIapModule.fetchProducts({ skus, type: native });
|
|
136
|
-
|
|
137
|
-
if (!isProductIOS(item)) {
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
const isValid = typeof item === 'object' &&
|
|
141
|
-
item !== null &&
|
|
142
|
-
'id' in item &&
|
|
143
|
-
typeof item.id === 'string' &&
|
|
144
|
-
skus.includes(item.id);
|
|
145
|
-
return isValid;
|
|
146
|
-
});
|
|
147
|
-
return canonical === 'in-app'
|
|
148
|
-
? filteredItems
|
|
149
|
-
: filteredItems;
|
|
148
|
+
return castResult(filterIosItems(rawItems));
|
|
150
149
|
}
|
|
151
150
|
if (Platform.OS === 'android') {
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
if (!isProductAndroid(item))
|
|
155
|
-
return false;
|
|
156
|
-
return (typeof item === 'object' &&
|
|
157
|
-
item !== null &&
|
|
158
|
-
'id' in item &&
|
|
159
|
-
typeof item.id === 'string' &&
|
|
160
|
-
skus.includes(item.id));
|
|
161
|
-
});
|
|
162
|
-
return canonical === 'in-app'
|
|
163
|
-
? filteredItems
|
|
164
|
-
: filteredItems;
|
|
151
|
+
const rawItems = await ExpoIapModule.fetchProducts(native, skus);
|
|
152
|
+
return castResult(filterAndroidItems(rawItems));
|
|
165
153
|
}
|
|
166
154
|
throw new Error('Unsupported platform');
|
|
167
155
|
};
|
|
168
|
-
export const getAvailablePurchases = (
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
156
|
+
export const getAvailablePurchases = async (options) => {
|
|
157
|
+
const normalizedOptions = {
|
|
158
|
+
alsoPublishToEventListenerIOS: options?.alsoPublishToEventListenerIOS ?? false,
|
|
159
|
+
onlyIncludeActiveItemsIOS: options?.onlyIncludeActiveItemsIOS ?? true,
|
|
160
|
+
};
|
|
161
|
+
const resolvePurchases = Platform.select({
|
|
162
|
+
ios: () => ExpoIapModule.getAvailableItems(normalizedOptions.alsoPublishToEventListenerIOS, normalizedOptions.onlyIncludeActiveItemsIOS),
|
|
163
|
+
android: () => ExpoIapModule.getAvailableItems(),
|
|
164
|
+
}) ?? (() => Promise.resolve([]));
|
|
165
|
+
const purchases = await resolvePurchases();
|
|
166
|
+
return normalizePurchaseList(purchases);
|
|
167
|
+
};
|
|
172
168
|
/**
|
|
173
169
|
* Restore completed transactions (cross-platform behavior)
|
|
174
170
|
*
|
|
@@ -182,17 +178,14 @@ export const getAvailablePurchases = ({ alsoPublishToEventListenerIOS = false, o
|
|
|
182
178
|
* @param options.onlyIncludeActiveItemsIOS - iOS only: whether to only include active items
|
|
183
179
|
* @returns Promise resolving to the list of available/restored purchases
|
|
184
180
|
*/
|
|
185
|
-
export const restorePurchases = async (
|
|
181
|
+
export const restorePurchases = async () => {
|
|
186
182
|
if (Platform.OS === 'ios') {
|
|
187
|
-
// Perform best-effort sync on iOS and ignore sync errors to avoid blocking restore flow
|
|
188
183
|
await syncIOS().catch(() => undefined);
|
|
189
184
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
onlyIncludeActiveItemsIOS: options.onlyIncludeActiveItemsIOS ?? true,
|
|
185
|
+
await getAvailablePurchases({
|
|
186
|
+
alsoPublishToEventListenerIOS: false,
|
|
187
|
+
onlyIncludeActiveItemsIOS: true,
|
|
194
188
|
});
|
|
195
|
-
return purchases;
|
|
196
189
|
};
|
|
197
190
|
const offerToRecordIOS = (offer) => {
|
|
198
191
|
if (!offer)
|
|
@@ -240,8 +233,8 @@ function normalizeRequestProps(request, platform) {
|
|
|
240
233
|
* });
|
|
241
234
|
* ```
|
|
242
235
|
*/
|
|
243
|
-
export const requestPurchase = (
|
|
244
|
-
const { request, type } =
|
|
236
|
+
export const requestPurchase = async (args) => {
|
|
237
|
+
const { request, type } = args;
|
|
245
238
|
const { canonical, native } = normalizeProductType(type);
|
|
246
239
|
const isInAppPurchase = canonical === 'in-app';
|
|
247
240
|
if (Platform.OS === 'ios') {
|
|
@@ -250,17 +243,15 @@ export const requestPurchase = (requestObj) => {
|
|
|
250
243
|
throw new Error('Invalid request for iOS. The `sku` property is required and must be a string.');
|
|
251
244
|
}
|
|
252
245
|
const { sku, andDangerouslyFinishTransactionAutomatically = false, appAccountToken, quantity, withOffer, } = normalizedRequest;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return purchase;
|
|
263
|
-
})();
|
|
246
|
+
const offer = offerToRecordIOS(withOffer ?? undefined);
|
|
247
|
+
const purchase = await ExpoIapModule.requestPurchase({
|
|
248
|
+
sku,
|
|
249
|
+
andDangerouslyFinishTransactionAutomatically,
|
|
250
|
+
appAccountToken,
|
|
251
|
+
quantity,
|
|
252
|
+
withOffer: offer,
|
|
253
|
+
});
|
|
254
|
+
return normalizePurchaseId(purchase);
|
|
264
255
|
}
|
|
265
256
|
if (Platform.OS === 'android') {
|
|
266
257
|
if (isInAppPurchase) {
|
|
@@ -269,18 +260,17 @@ export const requestPurchase = (requestObj) => {
|
|
|
269
260
|
throw new Error('Invalid request for Android. The `skus` property is required and must be a non-empty array.');
|
|
270
261
|
}
|
|
271
262
|
const { skus, obfuscatedAccountIdAndroid, obfuscatedProfileIdAndroid, isOfferPersonalized, } = normalizedRequest;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
})();
|
|
263
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
264
|
+
type: native,
|
|
265
|
+
skuArr: skus,
|
|
266
|
+
purchaseToken: undefined,
|
|
267
|
+
replacementMode: -1,
|
|
268
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
269
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
270
|
+
offerTokenArr: [],
|
|
271
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
272
|
+
}));
|
|
273
|
+
return normalizePurchaseList(result);
|
|
284
274
|
}
|
|
285
275
|
if (canonical === 'subs') {
|
|
286
276
|
const normalizedRequest = normalizeRequestProps(request, 'android');
|
|
@@ -291,52 +281,62 @@ export const requestPurchase = (requestObj) => {
|
|
|
291
281
|
const normalizedOffers = subscriptionOffers ?? [];
|
|
292
282
|
const replacementMode = replacementModeAndroid ?? -1;
|
|
293
283
|
const purchaseToken = purchaseTokenAndroid ?? undefined;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})();
|
|
284
|
+
const result = (await ExpoIapModule.requestPurchase({
|
|
285
|
+
type: native,
|
|
286
|
+
skuArr: skus,
|
|
287
|
+
purchaseToken,
|
|
288
|
+
replacementMode,
|
|
289
|
+
obfuscatedAccountId: obfuscatedAccountIdAndroid,
|
|
290
|
+
obfuscatedProfileId: obfuscatedProfileIdAndroid,
|
|
291
|
+
offerTokenArr: normalizedOffers.map((offer) => offer.offerToken),
|
|
292
|
+
subscriptionOffers: normalizedOffers,
|
|
293
|
+
isOfferPersonalized: isOfferPersonalized ?? false,
|
|
294
|
+
}));
|
|
295
|
+
return normalizePurchaseList(result);
|
|
307
296
|
}
|
|
308
297
|
throw new Error("Invalid request for Android: Expected a valid request object with 'skus' array.");
|
|
309
298
|
}
|
|
310
|
-
|
|
299
|
+
throw new Error('Platform not supported');
|
|
311
300
|
};
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
301
|
+
const toPurchaseInput = (purchase) => ({
|
|
302
|
+
id: purchase.id,
|
|
303
|
+
ids: purchase.ids ?? undefined,
|
|
304
|
+
isAutoRenewing: purchase.isAutoRenewing,
|
|
305
|
+
platform: purchase.platform,
|
|
306
|
+
productId: purchase.productId,
|
|
307
|
+
purchaseState: purchase.purchaseState,
|
|
308
|
+
purchaseToken: purchase.purchaseToken ?? null,
|
|
309
|
+
quantity: purchase.quantity,
|
|
310
|
+
transactionDate: purchase.transactionDate,
|
|
311
|
+
});
|
|
312
|
+
export const finishTransaction = async ({ purchase, isConsumable = false, }) => {
|
|
313
|
+
const normalizedPurchase = toPurchaseInput(purchase);
|
|
314
|
+
if (Platform.OS === 'ios') {
|
|
315
|
+
const transactionId = normalizedPurchase.id;
|
|
316
|
+
if (!transactionId) {
|
|
317
|
+
throw new Error('purchase.id required to finish iOS transaction');
|
|
318
|
+
}
|
|
319
|
+
await ExpoIapModule.finishTransaction(transactionId);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (Platform.OS === 'android') {
|
|
323
|
+
const token = normalizedPurchase.purchaseToken ?? undefined;
|
|
324
|
+
if (!token) {
|
|
325
|
+
throw new PurchaseError({
|
|
326
|
+
message: 'Purchase token is required to finish transaction',
|
|
327
|
+
code: ErrorCode.DeveloperError,
|
|
328
|
+
productId: normalizedPurchase.productId,
|
|
329
|
+
platform: 'android',
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (isConsumable) {
|
|
333
|
+
await ExpoIapModule.consumePurchaseAndroid(token);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
await ExpoIapModule.acknowledgePurchaseAndroid(token);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
throw new Error('Unsupported Platform');
|
|
340
340
|
};
|
|
341
341
|
/**
|
|
342
342
|
* Retrieves the current storefront information from iOS App Store
|
|
@@ -384,28 +384,27 @@ export const getStorefront = () => {
|
|
|
384
384
|
* - iOS: Send receipt data to Apple's verification endpoint from your server
|
|
385
385
|
* - Android: Use Google Play Developer API with service account credentials
|
|
386
386
|
*/
|
|
387
|
-
export const validateReceipt = async (
|
|
387
|
+
export const validateReceipt = async (options) => {
|
|
388
|
+
const { sku, androidOptions } = options;
|
|
388
389
|
if (Platform.OS === 'ios') {
|
|
389
|
-
return
|
|
390
|
+
return validateReceiptIOS({ sku });
|
|
390
391
|
}
|
|
391
|
-
|
|
392
|
+
if (Platform.OS === 'android') {
|
|
392
393
|
if (!androidOptions ||
|
|
393
394
|
!androidOptions.packageName ||
|
|
394
395
|
!androidOptions.productToken ||
|
|
395
396
|
!androidOptions.accessToken) {
|
|
396
397
|
throw new Error('Android validation requires packageName, productToken, and accessToken');
|
|
397
398
|
}
|
|
398
|
-
return
|
|
399
|
+
return validateReceiptAndroid({
|
|
399
400
|
packageName: androidOptions.packageName,
|
|
400
401
|
productId: sku,
|
|
401
402
|
productToken: androidOptions.productToken,
|
|
402
403
|
accessToken: androidOptions.accessToken,
|
|
403
|
-
isSub: androidOptions.isSub,
|
|
404
|
+
isSub: androidOptions.isSub ?? undefined,
|
|
404
405
|
});
|
|
405
406
|
}
|
|
406
|
-
|
|
407
|
-
throw new Error('Platform not supported');
|
|
408
|
-
}
|
|
407
|
+
throw new Error('Platform not supported');
|
|
409
408
|
};
|
|
410
409
|
/**
|
|
411
410
|
* Deeplinks to native interface that allows users to manage their subscriptions
|
|
@@ -425,17 +424,16 @@ export const validateReceipt = async (sku, androidOptions) => {
|
|
|
425
424
|
* packageNameAndroid: 'com.example.app'
|
|
426
425
|
* });
|
|
427
426
|
*/
|
|
428
|
-
export const deepLinkToSubscriptions = (options) => {
|
|
427
|
+
export const deepLinkToSubscriptions = async (options) => {
|
|
429
428
|
if (Platform.OS === 'ios') {
|
|
430
|
-
|
|
429
|
+
await deepLinkToSubscriptionsIOS();
|
|
430
|
+
return;
|
|
431
431
|
}
|
|
432
432
|
if (Platform.OS === 'android') {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
packageName: options?.packageNameAndroid,
|
|
436
|
-
});
|
|
433
|
+
await deepLinkToSubscriptionsAndroid(options ?? null);
|
|
434
|
+
return;
|
|
437
435
|
}
|
|
438
|
-
|
|
436
|
+
throw new Error(`Unsupported platform: ${Platform.OS}`);
|
|
439
437
|
};
|
|
440
438
|
export * from './useIAP';
|
|
441
439
|
export * from './utils/errorMapping';
|