react-native-iap 15.3.6 → 15.4.0-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/README.md +1 -1
- package/android/build.gradle +13 -5
- package/android/src/main/java/com/margelo/nitro/iap/HybridRnIap.kt +16 -3
- package/ios/HybridRnIap.swift +12 -0
- package/lib/module/index.js +56 -16
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.kepler.js +170 -0
- package/lib/module/index.kepler.js.map +1 -0
- package/lib/module/types/amazon-devices-kepler/index.d.js +2 -0
- package/lib/module/types/amazon-devices-kepler/index.d.js.map +1 -0
- package/lib/module/types.js +5 -2
- package/lib/module/types.js.map +1 -1
- package/lib/module/utils/error.js +2 -2
- package/lib/module/utils/error.js.map +1 -1
- package/lib/module/utils/type-bridge.js +4 -1
- package/lib/module/utils/type-bridge.js.map +1 -1
- package/lib/module/vega-adapter.js +981 -0
- package/lib/module/vega-adapter.js.map +1 -0
- package/lib/module/vega.js +10 -0
- package/lib/module/vega.js.map +1 -0
- package/lib/module/vega.kepler.js +18 -0
- package/lib/module/vega.kepler.js.map +1 -0
- package/lib/typescript/plugin/src/withIAP.d.ts +33 -0
- package/lib/typescript/plugin/src/withIAP.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +9 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.kepler.d.ts +55 -0
- package/lib/typescript/src/index.kepler.d.ts.map +1 -0
- package/lib/typescript/src/specs/RnIap.nitro.d.ts +10 -1
- package/lib/typescript/src/specs/RnIap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types.d.ts +16 -3
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/lib/typescript/src/utils/type-bridge.d.ts.map +1 -1
- package/lib/typescript/src/vega-adapter.d.ts +76 -0
- package/lib/typescript/src/vega-adapter.d.ts.map +1 -0
- package/lib/typescript/src/vega.d.ts +4 -0
- package/lib/typescript/src/vega.d.ts.map +1 -0
- package/lib/typescript/src/vega.kepler.d.ts +4 -0
- package/lib/typescript/src/vega.kepler.d.ts.map +1 -0
- package/nitrogen/generated/android/NitroIap+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JHybridRnIapSpec.cpp +5 -0
- package/nitrogen/generated/android/c++/JIapStore.hpp +3 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitAmazonProps.hpp +71 -0
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithIapkitProps.hpp +9 -1
- package/nitrogen/generated/android/c++/JNitroVerifyPurchaseWithProviderProps.hpp +4 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.cpp +26 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.hpp +75 -0
- package/nitrogen/generated/android/c++/JVariant_NullType_NitroVerifyPurchaseWithIapkitProps.hpp +4 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/IapStore.kt +2 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitAmazonProps.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/NitroVerifyPurchaseWithIapkitProps.kt +5 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/iap/Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.kt +53 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Bridge.hpp +47 -0
- package/nitrogen/generated/ios/NitroIap-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridRnIapSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/IapStore.swift +4 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitAmazonProps.swift +103 -0
- package/nitrogen/generated/ios/swift/NitroVerifyPurchaseWithIapkitProps.swift +38 -1
- package/nitrogen/generated/ios/swift/Variant_NullType_NitroVerifyPurchaseWithIapkitAmazonProps.swift +18 -0
- package/nitrogen/generated/shared/c++/IapStore.hpp +4 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitAmazonProps.hpp +94 -0
- package/nitrogen/generated/shared/c++/NitroVerifyPurchaseWithIapkitProps.hpp +8 -1
- package/openiap-versions.json +1 -1
- package/package.json +17 -4
- package/plugin/build/withIAP.d.ts +33 -0
- package/plugin/build/withIAP.js +109 -42
- package/plugin/src/withIAP.ts +185 -55
- package/src/index.kepler.ts +311 -0
- package/src/index.ts +65 -19
- package/src/specs/RnIap.nitro.ts +13 -2
- package/src/types/amazon-devices-kepler/index.d.ts +14 -0
- package/src/types.ts +17 -3
- package/src/utils/error.ts +2 -2
- package/src/utils/type-bridge.ts +4 -1
- package/src/vega-adapter.ts +1503 -0
- package/src/vega.kepler.ts +21 -0
- package/src/vega.ts +10 -0
- package/android/src/test/java/com/margelo/nitro/iap/ListenerThreadSafetyTest.kt +0 -154
- package/android/src/test/java/com/margelo/nitro/iap/OpenIapExceptionTest.kt +0 -93
- package/android/src/test/java/com/margelo/nitro/iap/ProductQueryHelpersTest.kt +0 -140
- package/plugin/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,1503 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IapkitPurchaseState,
|
|
3
|
+
NitroActiveSubscription,
|
|
4
|
+
NitroProduct,
|
|
5
|
+
NitroPurchase,
|
|
6
|
+
NitroPurchaseResult,
|
|
7
|
+
NitroVerifyPurchaseWithProviderProps,
|
|
8
|
+
NitroVerifyPurchaseWithProviderResult,
|
|
9
|
+
RnIap,
|
|
10
|
+
} from './specs/RnIap.nitro';
|
|
11
|
+
import {ErrorCode} from './types';
|
|
12
|
+
|
|
13
|
+
type ResponseOperation =
|
|
14
|
+
| 'product-data'
|
|
15
|
+
| 'purchase'
|
|
16
|
+
| 'purchase-updates'
|
|
17
|
+
| 'user-data'
|
|
18
|
+
| 'notify-fulfillment';
|
|
19
|
+
|
|
20
|
+
const IAPKIT_VERIFY_TIMEOUT_MS = 10_000;
|
|
21
|
+
const MAX_IAPKIT_ERROR_DEPTH = 5;
|
|
22
|
+
const MAX_PRODUCT_DATA_BATCH_SIZE = 100;
|
|
23
|
+
const MAX_PURCHASE_UPDATE_PAGES = 100;
|
|
24
|
+
const NOTIFY_FULFILLMENT_MAX_ATTEMPTS = 15;
|
|
25
|
+
const NOTIFY_FULFILLMENT_RETRY_DELAY_MS = 1_000;
|
|
26
|
+
const PURCHASE_UPDATES_MAX_ATTEMPTS = 5;
|
|
27
|
+
const PURCHASE_UPDATES_RETRY_DELAY_MS = 1_000;
|
|
28
|
+
const PURCHASE_RECOVERY_CLOCK_SKEW_MS = 5_000;
|
|
29
|
+
|
|
30
|
+
interface VegaPrice {
|
|
31
|
+
priceCurrencyCode?: string | null;
|
|
32
|
+
priceStr?: string | null;
|
|
33
|
+
valueInMicros?: bigint | number | string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface VegaProduct {
|
|
37
|
+
description?: string | null;
|
|
38
|
+
freeTrialPeriod?: string | null;
|
|
39
|
+
itemType?: unknown;
|
|
40
|
+
price?: VegaPrice | number | string | null;
|
|
41
|
+
productType?: unknown;
|
|
42
|
+
sku?: string | null;
|
|
43
|
+
subscriptionBase?: string | null;
|
|
44
|
+
subscriptionParent?: string | null;
|
|
45
|
+
subscriptionPeriod?: string | null;
|
|
46
|
+
term?: string | null;
|
|
47
|
+
title?: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface VegaReceipt {
|
|
51
|
+
cancelDate?: Date | number | string | null;
|
|
52
|
+
deferredDate?: Date | number | string | null;
|
|
53
|
+
isCancelled?: boolean | null;
|
|
54
|
+
isDeferred?: boolean | null;
|
|
55
|
+
productType?: unknown;
|
|
56
|
+
purchaseDate?: Date | number | string | null;
|
|
57
|
+
receiptId?: string | null;
|
|
58
|
+
sku?: string | null;
|
|
59
|
+
termSku?: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface VegaUserData {
|
|
63
|
+
countryCode?: string | null;
|
|
64
|
+
marketplace?: string | null;
|
|
65
|
+
userId?: string | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface VegaResponse {
|
|
69
|
+
responseCode?: unknown;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface VegaProductDataResponse extends VegaResponse {
|
|
73
|
+
productData?: Map<string, VegaProduct> | Record<string, VegaProduct> | null;
|
|
74
|
+
unavailableSkus?: string[] | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface VegaPurchaseResponse extends VegaResponse {
|
|
78
|
+
receipt?: VegaReceipt | null;
|
|
79
|
+
userData?: VegaUserData | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface VegaPurchaseUpdatesResponse extends VegaResponse {
|
|
83
|
+
hasMore?: boolean | null;
|
|
84
|
+
receiptList?: VegaReceipt[] | null;
|
|
85
|
+
userData?: VegaUserData | null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface VegaUserDataResponse extends VegaResponse {
|
|
89
|
+
userData?: VegaUserData | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface VegaUserDataRequest {
|
|
93
|
+
fetchUserProfileAccessConsentStatus: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface VegaError extends Error {
|
|
97
|
+
code?: ErrorCode;
|
|
98
|
+
debugMessage?: string;
|
|
99
|
+
platform?: 'android';
|
|
100
|
+
productId?: string;
|
|
101
|
+
responseCode?: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface VegaPurchasingService {
|
|
105
|
+
getProductData(request: {skus: string[]}): Promise<VegaProductDataResponse>;
|
|
106
|
+
getPurchaseUpdates(request: {
|
|
107
|
+
reset: boolean;
|
|
108
|
+
}): Promise<VegaPurchaseUpdatesResponse>;
|
|
109
|
+
getUserData(request: VegaUserDataRequest): Promise<VegaUserDataResponse>;
|
|
110
|
+
notifyFulfillment(request: {
|
|
111
|
+
fulfillmentResult: number;
|
|
112
|
+
receiptId: string;
|
|
113
|
+
}): Promise<VegaResponse>;
|
|
114
|
+
purchase(request: {sku: string}): Promise<VegaPurchaseResponse>;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const PRODUCT_TYPE_SUBSCRIPTION = 3;
|
|
118
|
+
const FULFILLMENT_RESULT_FULFILLED = 1;
|
|
119
|
+
const RESPONSE_SUCCESS = 1;
|
|
120
|
+
const PURCHASE_RESPONSE_SUCCESS = 0;
|
|
121
|
+
const PURCHASE_STATE_PURCHASED = 1;
|
|
122
|
+
const PURCHASE_STATE_PENDING = 2;
|
|
123
|
+
const IAPKIT_DEFAULT_BASE_URL = 'https://kit.openiap.dev';
|
|
124
|
+
const IAPKIT_VERIFY_PATH = '/v1/purchase/verify';
|
|
125
|
+
const VEGA_PARSER_ERROR_MESSAGES = [
|
|
126
|
+
'Cannot convert undefined value to object',
|
|
127
|
+
'userId is not found while parsing Json',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
function createVegaError(
|
|
131
|
+
code: ErrorCode,
|
|
132
|
+
message: string,
|
|
133
|
+
responseCode?: unknown,
|
|
134
|
+
productId?: string,
|
|
135
|
+
): Error {
|
|
136
|
+
const error = new Error(message) as VegaError;
|
|
137
|
+
error.code = code;
|
|
138
|
+
error.debugMessage = message;
|
|
139
|
+
error.platform = 'android';
|
|
140
|
+
error.productId = productId;
|
|
141
|
+
if (typeof responseCode === 'number') {
|
|
142
|
+
error.responseCode = responseCode;
|
|
143
|
+
}
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function parseVegaErrorPayload(error: unknown): Record<string, unknown> {
|
|
148
|
+
if (!(error instanceof Error)) return {};
|
|
149
|
+
const vegaError = error as VegaError;
|
|
150
|
+
if (
|
|
151
|
+
vegaError.code != null ||
|
|
152
|
+
vegaError.debugMessage != null ||
|
|
153
|
+
vegaError.productId != null ||
|
|
154
|
+
vegaError.responseCode != null
|
|
155
|
+
) {
|
|
156
|
+
return {
|
|
157
|
+
code: vegaError.code,
|
|
158
|
+
message: error.message,
|
|
159
|
+
responseCode: vegaError.responseCode,
|
|
160
|
+
debugMessage: vegaError.debugMessage,
|
|
161
|
+
productId: vegaError.productId,
|
|
162
|
+
platform: vegaError.platform,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const parsed = JSON.parse(error.message);
|
|
167
|
+
return parsed && typeof parsed === 'object'
|
|
168
|
+
? (parsed as Record<string, unknown>)
|
|
169
|
+
: {};
|
|
170
|
+
} catch {
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function toPurchaseErrorResult(
|
|
176
|
+
error: unknown,
|
|
177
|
+
fallbackMessage: string,
|
|
178
|
+
): NitroPurchaseResult {
|
|
179
|
+
const parsed = parseVegaErrorPayload(error);
|
|
180
|
+
const responseCode = parsed.responseCode;
|
|
181
|
+
return {
|
|
182
|
+
responseCode: typeof responseCode === 'number' ? responseCode : -1,
|
|
183
|
+
code:
|
|
184
|
+
typeof parsed.code === 'string' ? parsed.code : ErrorCode.PurchaseError,
|
|
185
|
+
message:
|
|
186
|
+
typeof parsed.message === 'string'
|
|
187
|
+
? parsed.message
|
|
188
|
+
: error instanceof Error
|
|
189
|
+
? error.message
|
|
190
|
+
: fallbackMessage,
|
|
191
|
+
debugMessage:
|
|
192
|
+
typeof parsed.debugMessage === 'string' ? parsed.debugMessage : undefined,
|
|
193
|
+
purchaseToken: undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function responseCodeName(responseCode: unknown): string {
|
|
198
|
+
if (typeof responseCode === 'string') {
|
|
199
|
+
return responseCode.toUpperCase();
|
|
200
|
+
}
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function delay(ms: number): Promise<void> {
|
|
205
|
+
return new Promise((resolve) => {
|
|
206
|
+
setTimeout(resolve, ms);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isSuccess(
|
|
211
|
+
operation: ResponseOperation,
|
|
212
|
+
responseCode: unknown,
|
|
213
|
+
): boolean {
|
|
214
|
+
if (typeof responseCode === 'number') {
|
|
215
|
+
return operation === 'purchase'
|
|
216
|
+
? responseCode === PURCHASE_RESPONSE_SUCCESS
|
|
217
|
+
: responseCode === RESPONSE_SUCCESS;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const name = responseCodeName(responseCode);
|
|
221
|
+
return name === 'SUCCESSFUL' || name === 'SUCCESS' || name === 'OK';
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function mapErrorCode(
|
|
225
|
+
operation: ResponseOperation,
|
|
226
|
+
responseCode: unknown,
|
|
227
|
+
): ErrorCode {
|
|
228
|
+
const name = responseCodeName(responseCode);
|
|
229
|
+
if (name.includes('ALREADY_PURCHASED')) return ErrorCode.AlreadyOwned;
|
|
230
|
+
if (name.includes('INVALID_SKU')) return ErrorCode.SkuNotFound;
|
|
231
|
+
if (name.includes('NOT_SUPPORTED')) return ErrorCode.FeatureNotSupported;
|
|
232
|
+
if (name.includes('PENDING')) return ErrorCode.Pending;
|
|
233
|
+
if (name.includes('FAILED')) {
|
|
234
|
+
return operation === 'purchase'
|
|
235
|
+
? ErrorCode.UserCancelled
|
|
236
|
+
: ErrorCode.ServiceError;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (typeof responseCode === 'number') {
|
|
240
|
+
if (operation === 'purchase') {
|
|
241
|
+
if (responseCode === 1) return ErrorCode.AlreadyOwned;
|
|
242
|
+
if (responseCode === 2) return ErrorCode.SkuNotFound;
|
|
243
|
+
if (responseCode === 3) return ErrorCode.FeatureNotSupported;
|
|
244
|
+
if (responseCode === 4) return ErrorCode.UserCancelled;
|
|
245
|
+
}
|
|
246
|
+
if (operation !== 'purchase' && responseCode === 2) {
|
|
247
|
+
return ErrorCode.FeatureNotSupported;
|
|
248
|
+
}
|
|
249
|
+
if (operation !== 'purchase' && responseCode === 3) {
|
|
250
|
+
return ErrorCode.ServiceError;
|
|
251
|
+
}
|
|
252
|
+
if (operation !== 'purchase' && responseCode === 4) {
|
|
253
|
+
return ErrorCode.ServiceError;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (operation === 'product-data') return ErrorCode.QueryProduct;
|
|
258
|
+
if (operation === 'user-data') return ErrorCode.InitConnection;
|
|
259
|
+
return ErrorCode.PurchaseError;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function shouldRetryResponse(
|
|
263
|
+
operation: ResponseOperation,
|
|
264
|
+
responseCode: unknown,
|
|
265
|
+
): boolean {
|
|
266
|
+
if (isSuccess(operation, responseCode)) return false;
|
|
267
|
+
if (operation === 'purchase') return false;
|
|
268
|
+
if (typeof responseCode === 'number') return responseCode === 3;
|
|
269
|
+
return responseCodeName(responseCode).includes('FAILED');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function shouldRecoverPurchaseResponse(responseCode: unknown): boolean {
|
|
273
|
+
if (typeof responseCode === 'number') {
|
|
274
|
+
return responseCode === 1 || responseCode === 4;
|
|
275
|
+
}
|
|
276
|
+
const name = responseCodeName(responseCode);
|
|
277
|
+
return name.includes('ALREADY_PURCHASED') || name.includes('FAILED');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function ensureSuccessful(
|
|
281
|
+
operation: ResponseOperation,
|
|
282
|
+
response: VegaResponse | null | undefined,
|
|
283
|
+
message: string,
|
|
284
|
+
productId?: string,
|
|
285
|
+
): void {
|
|
286
|
+
const responseCode = response?.responseCode;
|
|
287
|
+
if (isSuccess(operation, responseCode)) return;
|
|
288
|
+
|
|
289
|
+
throw createVegaError(
|
|
290
|
+
mapErrorCode(operation, responseCode),
|
|
291
|
+
`${message}. Amazon Vega responseCode=${String(responseCode ?? 'unknown')}`,
|
|
292
|
+
responseCode,
|
|
293
|
+
productId,
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function stringifyJson(value: unknown): string {
|
|
298
|
+
return JSON.stringify(value ?? {});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function toTimestamp(value: unknown): number {
|
|
302
|
+
if (value instanceof Date) return value.getTime();
|
|
303
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
304
|
+
if (typeof value === 'string') {
|
|
305
|
+
const timestamp = Date.parse(value);
|
|
306
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
307
|
+
}
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function priceNumberToMicros(value: number): number {
|
|
312
|
+
return Math.trunc(Math.abs(value) < 10_000 ? value * 1_000_000 : value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function toPriceAmountMicros(value: unknown): string {
|
|
316
|
+
if (typeof value === 'bigint') return value.toString();
|
|
317
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
318
|
+
return Math.trunc(value).toString();
|
|
319
|
+
}
|
|
320
|
+
if (typeof value === 'string' && value.length > 0) return value;
|
|
321
|
+
return '0';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function getPriceObject(product: VegaProduct): VegaPrice {
|
|
325
|
+
return product.price != null &&
|
|
326
|
+
typeof product.price === 'object' &&
|
|
327
|
+
!Array.isArray(product.price)
|
|
328
|
+
? product.price
|
|
329
|
+
: {};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getPriceAmountMicros(product: VegaProduct): unknown {
|
|
333
|
+
if (typeof product.price === 'number' && Number.isFinite(product.price)) {
|
|
334
|
+
return priceNumberToMicros(product.price);
|
|
335
|
+
}
|
|
336
|
+
return getPriceObject(product).valueInMicros;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function microsToPrice(value: unknown): number | null {
|
|
340
|
+
const micros =
|
|
341
|
+
typeof value === 'bigint' ? Number(value) : Number(value ?? Number.NaN);
|
|
342
|
+
if (!Number.isFinite(micros)) return null;
|
|
343
|
+
return micros / 1_000_000;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function getPrice(product: VegaProduct): number | null {
|
|
347
|
+
if (typeof product.price === 'number' && Number.isFinite(product.price)) {
|
|
348
|
+
return Math.abs(product.price) < 10_000
|
|
349
|
+
? product.price
|
|
350
|
+
: product.price / 1_000_000;
|
|
351
|
+
}
|
|
352
|
+
return microsToPrice(getPriceObject(product).valueInMicros);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function getDisplayPrice(product: VegaProduct): string {
|
|
356
|
+
const price = product.price;
|
|
357
|
+
if (typeof price === 'number' && Number.isFinite(price)) {
|
|
358
|
+
const value = Math.abs(price) < 10_000 ? price : price / 1_000_000;
|
|
359
|
+
return value.toFixed(2);
|
|
360
|
+
}
|
|
361
|
+
if (typeof price === 'string') return price;
|
|
362
|
+
return getPriceObject(product).priceStr ?? '';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function getCurrency(product: VegaProduct): string {
|
|
366
|
+
return getPriceObject(product).priceCurrencyCode ?? '';
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function isSubscription(productType: unknown): boolean {
|
|
370
|
+
if (typeof productType === 'number') {
|
|
371
|
+
return productType === PRODUCT_TYPE_SUBSCRIPTION;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (typeof productType === 'string') {
|
|
375
|
+
return productType.toUpperCase().includes('SUBSCRIPTION');
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function productTypeToOpenIap(productType: unknown): 'in-app' | 'subs' {
|
|
382
|
+
return isSubscription(productType) ? 'subs' : 'in-app';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getProductType(product: VegaProduct): unknown {
|
|
386
|
+
return product.productType ?? product.itemType;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function getSubscriptionPeriod(product: VegaProduct): string {
|
|
390
|
+
if (product.subscriptionPeriod) return product.subscriptionPeriod;
|
|
391
|
+
|
|
392
|
+
const term = product.term?.toLowerCase() ?? '';
|
|
393
|
+
if (term.includes('year')) return 'P1Y';
|
|
394
|
+
if (term.includes('month')) return 'P1M';
|
|
395
|
+
if (term.includes('week')) return 'P1W';
|
|
396
|
+
if (term.includes('day')) return 'P1D';
|
|
397
|
+
return '';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function getReceiptSku(receipt: VegaReceipt): string {
|
|
401
|
+
return receipt.sku ?? receipt.termSku ?? '';
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function getCachedProductType(
|
|
405
|
+
receipt: VegaReceipt,
|
|
406
|
+
productTypesBySku: Map<string, unknown>,
|
|
407
|
+
fallbackSku?: string,
|
|
408
|
+
): unknown {
|
|
409
|
+
const sku = getReceiptSku(receipt) || fallbackSku || '';
|
|
410
|
+
return sku ? productTypesBySku.get(sku) : undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function productDataToArray(
|
|
414
|
+
productData?: Map<string, VegaProduct> | Record<string, VegaProduct> | null,
|
|
415
|
+
): VegaProduct[] {
|
|
416
|
+
if (!productData) return [];
|
|
417
|
+
if (productData instanceof Map) {
|
|
418
|
+
return Array.from(productData.entries()).map(([sku, product]) => ({
|
|
419
|
+
...product,
|
|
420
|
+
sku: product.sku ?? sku,
|
|
421
|
+
}));
|
|
422
|
+
}
|
|
423
|
+
return Object.entries(productData).map(([sku, product]) => ({
|
|
424
|
+
...product,
|
|
425
|
+
sku: product.sku ?? sku,
|
|
426
|
+
}));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function chunkArray<T>(items: T[], size: number): T[][] {
|
|
430
|
+
const chunks: T[][] = [];
|
|
431
|
+
for (let index = 0; index < items.length; index += size) {
|
|
432
|
+
chunks.push(items.slice(index, index + size));
|
|
433
|
+
}
|
|
434
|
+
return chunks;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function createUserDataRequest(): VegaUserDataRequest {
|
|
438
|
+
return {
|
|
439
|
+
fetchUserProfileAccessConsentStatus: false,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function isVegaParserError(error: unknown): boolean {
|
|
444
|
+
return (
|
|
445
|
+
error instanceof Error &&
|
|
446
|
+
VEGA_PARSER_ERROR_MESSAGES.some((message) =>
|
|
447
|
+
error.message.includes(message),
|
|
448
|
+
)
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function createPricingPhase(product: VegaProduct) {
|
|
453
|
+
return {
|
|
454
|
+
billingCycleCount: 0,
|
|
455
|
+
billingPeriod: getSubscriptionPeriod(product),
|
|
456
|
+
formattedPrice: getDisplayPrice(product),
|
|
457
|
+
priceAmountMicros: toPriceAmountMicros(getPriceAmountMicros(product)),
|
|
458
|
+
priceCurrencyCode: getCurrency(product),
|
|
459
|
+
recurrenceMode: 1,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function createSubscriptionOffer(product: VegaProduct) {
|
|
464
|
+
const pricingPhase = createPricingPhase(product);
|
|
465
|
+
const sku = product.sku ?? '';
|
|
466
|
+
return {
|
|
467
|
+
basePlanId: sku,
|
|
468
|
+
offerId: null,
|
|
469
|
+
offerTags: [],
|
|
470
|
+
offerToken: '',
|
|
471
|
+
pricingPhases: {
|
|
472
|
+
pricingPhaseList: [pricingPhase],
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function createStandardizedSubscriptionOffer(product: VegaProduct) {
|
|
478
|
+
const pricingPhase = createPricingPhase(product);
|
|
479
|
+
const sku = product.sku ?? '';
|
|
480
|
+
return {
|
|
481
|
+
basePlanIdAndroid: sku,
|
|
482
|
+
currency: getCurrency(product),
|
|
483
|
+
displayPrice: getDisplayPrice(product),
|
|
484
|
+
id: sku,
|
|
485
|
+
offerTagsAndroid: [],
|
|
486
|
+
offerTokenAndroid: '',
|
|
487
|
+
paymentMode: 'pay-as-you-go',
|
|
488
|
+
period: null,
|
|
489
|
+
price: getPrice(product) ?? 0,
|
|
490
|
+
pricingPhasesAndroid: {
|
|
491
|
+
pricingPhaseList: [pricingPhase],
|
|
492
|
+
},
|
|
493
|
+
type: 'introductory',
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function mapProduct(product: VegaProduct): NitroProduct {
|
|
498
|
+
const sku = product.sku ?? '';
|
|
499
|
+
const type = productTypeToOpenIap(getProductType(product));
|
|
500
|
+
const subscriptionOfferDetails =
|
|
501
|
+
type === 'subs' ? [createSubscriptionOffer(product)] : null;
|
|
502
|
+
const subscriptionOffers =
|
|
503
|
+
type === 'subs' ? [createStandardizedSubscriptionOffer(product)] : null;
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
id: sku,
|
|
507
|
+
title: product.title ?? sku,
|
|
508
|
+
description: product.description ?? '',
|
|
509
|
+
type,
|
|
510
|
+
displayName: product.title ?? sku,
|
|
511
|
+
displayPrice: getDisplayPrice(product),
|
|
512
|
+
currency: getCurrency(product),
|
|
513
|
+
price: getPrice(product),
|
|
514
|
+
platform: 'android',
|
|
515
|
+
introductoryPricePaymentModeIOS: 'empty',
|
|
516
|
+
nameAndroid: product.title ?? sku,
|
|
517
|
+
subscriptionPeriodAndroid: getSubscriptionPeriod(product) || null,
|
|
518
|
+
freeTrialPeriodAndroid: product.freeTrialPeriod ?? null,
|
|
519
|
+
subscriptionOfferDetailsAndroid: subscriptionOfferDetails
|
|
520
|
+
? stringifyJson(subscriptionOfferDetails)
|
|
521
|
+
: null,
|
|
522
|
+
subscriptionOffers: subscriptionOffers
|
|
523
|
+
? stringifyJson(subscriptionOffers)
|
|
524
|
+
: null,
|
|
525
|
+
productStatusAndroid: 'ok',
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function mapReceipt(
|
|
530
|
+
receipt: VegaReceipt,
|
|
531
|
+
fallbackProductType?: unknown,
|
|
532
|
+
productIdOverride?: string,
|
|
533
|
+
): NitroPurchase {
|
|
534
|
+
const receiptId = receipt.receiptId ?? '';
|
|
535
|
+
const productId = productIdOverride ?? getReceiptSku(receipt);
|
|
536
|
+
const type = productTypeToOpenIap(receipt.productType ?? fallbackProductType);
|
|
537
|
+
const isPending = Boolean(receipt.isDeferred);
|
|
538
|
+
const isCanceled = Boolean(receipt.isCancelled || receipt.cancelDate);
|
|
539
|
+
const isActive = !isCanceled && !isPending;
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
id: receiptId,
|
|
543
|
+
productId,
|
|
544
|
+
transactionDate: toTimestamp(receipt.purchaseDate),
|
|
545
|
+
purchaseToken: receiptId,
|
|
546
|
+
platform: 'android',
|
|
547
|
+
store: 'amazon',
|
|
548
|
+
quantity: 1,
|
|
549
|
+
purchaseState: isPending ? 'pending' : isActive ? 'purchased' : 'unknown',
|
|
550
|
+
isAutoRenewing: type === 'subs' && isActive,
|
|
551
|
+
purchaseTokenAndroid: receiptId,
|
|
552
|
+
dataAndroid: stringifyJson(receipt),
|
|
553
|
+
signatureAndroid: null,
|
|
554
|
+
autoRenewingAndroid: type === 'subs' && isActive,
|
|
555
|
+
purchaseStateAndroid: isPending
|
|
556
|
+
? PURCHASE_STATE_PENDING
|
|
557
|
+
: isActive
|
|
558
|
+
? PURCHASE_STATE_PURCHASED
|
|
559
|
+
: 0,
|
|
560
|
+
isAcknowledgedAndroid: false,
|
|
561
|
+
isSuspendedAndroid: Boolean(receipt.isDeferred),
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function getSkuFromRequest(request: Parameters<RnIap['requestPurchase']>[0]) {
|
|
566
|
+
const androidRequest = request.google ?? request.android;
|
|
567
|
+
const skus = androidRequest?.skus ?? [];
|
|
568
|
+
if (skus.length !== 1) {
|
|
569
|
+
throw createVegaError(
|
|
570
|
+
ErrorCode.DeveloperError,
|
|
571
|
+
'Amazon Vega purchase expects exactly one SKU per request.',
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
return skus[0]!;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function hasSubscriptionRequestContext(subscriptionOffers: unknown): boolean {
|
|
578
|
+
if (Array.isArray(subscriptionOffers)) return true;
|
|
579
|
+
if (typeof subscriptionOffers === 'string') {
|
|
580
|
+
return subscriptionOffers.trim().length > 0;
|
|
581
|
+
}
|
|
582
|
+
return subscriptionOffers != null;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function throwUnsupportedFeature(feature: string): never {
|
|
586
|
+
throw createVegaError(
|
|
587
|
+
ErrorCode.FeatureNotSupported,
|
|
588
|
+
`${feature} is not supported on Amazon Vega.`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
type VegaRnIapModule = Partial<RnIap> & {
|
|
593
|
+
acknowledgePurchaseAndroid(purchaseToken: string): Promise<boolean>;
|
|
594
|
+
consumePurchaseAndroid(purchaseToken: string): Promise<boolean>;
|
|
595
|
+
restorePurchases(): Promise<void>;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
interface RecoveredNitroPurchases {
|
|
599
|
+
requestedPurchases: NitroPurchase[];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
interface RecoverPurchasesOptions {
|
|
603
|
+
minPurchaseDateMs?: number;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
export function createVegaIapModule(service: VegaPurchasingService): RnIap {
|
|
607
|
+
const productTypesBySku = new Map<string, unknown>();
|
|
608
|
+
const subscriptionBasesBySku = new Map<string, string>();
|
|
609
|
+
const subscriptionParentsBySku = new Map<string, string>();
|
|
610
|
+
const purchaseUpdateListeners = new Map<
|
|
611
|
+
number,
|
|
612
|
+
(purchase: NitroPurchase) => void
|
|
613
|
+
>();
|
|
614
|
+
const purchaseErrorListeners = new Set<
|
|
615
|
+
(error: NitroPurchaseResult) => void
|
|
616
|
+
>();
|
|
617
|
+
let cachedUserData: VegaUserData | null = null;
|
|
618
|
+
let nextPurchaseUpdateListenerToken = 1;
|
|
619
|
+
|
|
620
|
+
const emitPurchaseUpdated = (purchase: NitroPurchase): void => {
|
|
621
|
+
for (const listener of purchaseUpdateListeners.values()) {
|
|
622
|
+
listener(purchase);
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const emitPurchaseError = (error: NitroPurchaseResult): void => {
|
|
627
|
+
for (const listener of purchaseErrorListeners) {
|
|
628
|
+
listener(error);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const cacheProductMetadata = (product: VegaProduct): void => {
|
|
633
|
+
if (!product.sku) return;
|
|
634
|
+
|
|
635
|
+
const productType = getProductType(product);
|
|
636
|
+
productTypesBySku.set(product.sku, productType);
|
|
637
|
+
|
|
638
|
+
if (product.subscriptionBase) {
|
|
639
|
+
subscriptionBasesBySku.set(product.sku, product.subscriptionBase);
|
|
640
|
+
productTypesBySku.set(product.subscriptionBase, productType);
|
|
641
|
+
}
|
|
642
|
+
if (product.subscriptionParent) {
|
|
643
|
+
subscriptionParentsBySku.set(product.sku, product.subscriptionParent);
|
|
644
|
+
productTypesBySku.set(product.subscriptionParent, productType);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
const receiptMatchesRequestedSku = (
|
|
649
|
+
receipt: VegaReceipt,
|
|
650
|
+
sku: string,
|
|
651
|
+
): boolean => {
|
|
652
|
+
const receiptSku = getReceiptSku(receipt);
|
|
653
|
+
if (!receiptSku) return false;
|
|
654
|
+
if (receiptSku === sku || receipt.termSku === sku) return true;
|
|
655
|
+
if (receiptSku === subscriptionBasesBySku.get(sku)) return true;
|
|
656
|
+
if (receiptSku === subscriptionParentsBySku.get(sku)) return true;
|
|
657
|
+
return receiptSku === `${sku}.base`;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const resolveReceiptProductId = (
|
|
661
|
+
receipt: VegaReceipt,
|
|
662
|
+
productIdOverride?: string,
|
|
663
|
+
): string => {
|
|
664
|
+
if (productIdOverride) return productIdOverride;
|
|
665
|
+
|
|
666
|
+
const receiptSku = getReceiptSku(receipt);
|
|
667
|
+
if (!receiptSku) return '';
|
|
668
|
+
|
|
669
|
+
for (const [productSku, subscriptionBase] of subscriptionBasesBySku) {
|
|
670
|
+
if (receiptSku === subscriptionBase) return productSku;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
for (const [productSku, subscriptionParent] of subscriptionParentsBySku) {
|
|
674
|
+
if (receiptSku === subscriptionParent) return productSku;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (receiptSku.endsWith('.base')) {
|
|
678
|
+
const parentSku = receiptSku.slice(0, -'.base'.length);
|
|
679
|
+
if (productTypesBySku.has(parentSku)) return parentSku;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return receiptSku;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const getUserData = async (): Promise<VegaUserData | null> => {
|
|
686
|
+
let response: VegaUserDataResponse;
|
|
687
|
+
try {
|
|
688
|
+
response = await service.getUserData(createUserDataRequest());
|
|
689
|
+
} catch (error) {
|
|
690
|
+
if (isVegaParserError(error)) {
|
|
691
|
+
return null;
|
|
692
|
+
}
|
|
693
|
+
throw error;
|
|
694
|
+
}
|
|
695
|
+
ensureSuccessful('user-data', response, 'Failed to fetch Amazon user data');
|
|
696
|
+
cachedUserData = response.userData ?? null;
|
|
697
|
+
return cachedUserData;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const getStorefront = async (): Promise<string> => {
|
|
701
|
+
await getUserData();
|
|
702
|
+
return cachedUserData?.marketplace ?? cachedUserData?.countryCode ?? '';
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
const getPurchaseUpdateReceipts = async (): Promise<VegaReceipt[]> => {
|
|
706
|
+
const receipts: VegaReceipt[] = [];
|
|
707
|
+
let reset = true;
|
|
708
|
+
let hasMore = false;
|
|
709
|
+
let pageCount = 0;
|
|
710
|
+
|
|
711
|
+
do {
|
|
712
|
+
if (pageCount >= MAX_PURCHASE_UPDATE_PAGES) {
|
|
713
|
+
throw createVegaError(
|
|
714
|
+
ErrorCode.ServiceError,
|
|
715
|
+
'Amazon Vega purchase updates exceeded the pagination limit.',
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
pageCount++;
|
|
719
|
+
|
|
720
|
+
let response: VegaPurchaseUpdatesResponse | null = null;
|
|
721
|
+
for (
|
|
722
|
+
let attempt = 1;
|
|
723
|
+
attempt <= PURCHASE_UPDATES_MAX_ATTEMPTS;
|
|
724
|
+
attempt += 1
|
|
725
|
+
) {
|
|
726
|
+
try {
|
|
727
|
+
response = await service.getPurchaseUpdates({reset});
|
|
728
|
+
} catch (error) {
|
|
729
|
+
if (isVegaParserError(error)) {
|
|
730
|
+
return receipts;
|
|
731
|
+
}
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (
|
|
736
|
+
!response ||
|
|
737
|
+
!shouldRetryResponse('purchase-updates', response.responseCode) ||
|
|
738
|
+
attempt === PURCHASE_UPDATES_MAX_ATTEMPTS
|
|
739
|
+
) {
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
await delay(PURCHASE_UPDATES_RETRY_DELAY_MS);
|
|
743
|
+
}
|
|
744
|
+
if (!response) {
|
|
745
|
+
throw createVegaError(
|
|
746
|
+
ErrorCode.ServiceError,
|
|
747
|
+
'Amazon Vega purchase updates returned no response.',
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
ensureSuccessful(
|
|
751
|
+
'purchase-updates',
|
|
752
|
+
response,
|
|
753
|
+
'Failed to fetch Amazon purchase updates',
|
|
754
|
+
);
|
|
755
|
+
cachedUserData = response.userData ?? cachedUserData;
|
|
756
|
+
receipts.push(...(response.receiptList ?? []));
|
|
757
|
+
hasMore = Boolean(response.hasMore);
|
|
758
|
+
reset = false;
|
|
759
|
+
} while (hasMore);
|
|
760
|
+
|
|
761
|
+
return receipts;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
const getProductData = async (
|
|
765
|
+
skus: string[],
|
|
766
|
+
message: string,
|
|
767
|
+
): Promise<VegaProduct[]> => {
|
|
768
|
+
const products: VegaProduct[] = [];
|
|
769
|
+
for (const batch of chunkArray(skus, MAX_PRODUCT_DATA_BATCH_SIZE)) {
|
|
770
|
+
const response = await service.getProductData({skus: batch});
|
|
771
|
+
ensureSuccessful('product-data', response, message);
|
|
772
|
+
products.push(...productDataToArray(response.productData));
|
|
773
|
+
}
|
|
774
|
+
return products;
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const hydrateProductTypesForReceipts = async (
|
|
778
|
+
receipts: VegaReceipt[],
|
|
779
|
+
): Promise<void> => {
|
|
780
|
+
const missingSkus = new Set<string>();
|
|
781
|
+
|
|
782
|
+
for (const receipt of receipts) {
|
|
783
|
+
const sku = getReceiptSku(receipt);
|
|
784
|
+
if (!sku) continue;
|
|
785
|
+
if (receipt.productType != null) {
|
|
786
|
+
productTypesBySku.set(sku, receipt.productType);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const resolvedSku = resolveReceiptProductId(receipt);
|
|
791
|
+
const resolvedProductType = productTypesBySku.get(resolvedSku);
|
|
792
|
+
if (resolvedProductType != null) {
|
|
793
|
+
productTypesBySku.set(sku, resolvedProductType);
|
|
794
|
+
} else if (!productTypesBySku.has(sku)) {
|
|
795
|
+
missingSkus.add(sku);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (missingSkus.size === 0) return;
|
|
800
|
+
|
|
801
|
+
let products: VegaProduct[];
|
|
802
|
+
try {
|
|
803
|
+
products = await getProductData(
|
|
804
|
+
Array.from(missingSkus),
|
|
805
|
+
'Failed to fetch Amazon Vega product data for purchase updates',
|
|
806
|
+
);
|
|
807
|
+
} catch (error) {
|
|
808
|
+
if (isVegaParserError(error)) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
for (const product of products) {
|
|
815
|
+
cacheProductMetadata(product);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
const getAvailablePurchases = async (
|
|
820
|
+
options?: Parameters<RnIap['getAvailablePurchases']>[0],
|
|
821
|
+
): Promise<NitroPurchase[]> => {
|
|
822
|
+
const requestedType = options?.android?.type;
|
|
823
|
+
const includeSuspended = Boolean(options?.android?.includeSuspended);
|
|
824
|
+
const receipts = await getPurchaseUpdateReceipts();
|
|
825
|
+
await hydrateProductTypesForReceipts(receipts);
|
|
826
|
+
return receipts
|
|
827
|
+
.filter((receipt) => {
|
|
828
|
+
if (receipt.isCancelled || receipt.cancelDate) return false;
|
|
829
|
+
if (!includeSuspended && receipt.isDeferred) return false;
|
|
830
|
+
const openIapType = productTypeToOpenIap(
|
|
831
|
+
receipt.productType ??
|
|
832
|
+
getCachedProductType(receipt, productTypesBySku),
|
|
833
|
+
);
|
|
834
|
+
if (requestedType === 'subs') return openIapType === 'subs';
|
|
835
|
+
if (requestedType === 'inapp') return openIapType === 'in-app';
|
|
836
|
+
return true;
|
|
837
|
+
})
|
|
838
|
+
.map((receipt) =>
|
|
839
|
+
mapReceipt(
|
|
840
|
+
receipt,
|
|
841
|
+
getCachedProductType(receipt, productTypesBySku),
|
|
842
|
+
resolveReceiptProductId(receipt),
|
|
843
|
+
),
|
|
844
|
+
);
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
const finishReceipt = async (
|
|
848
|
+
purchaseToken: string,
|
|
849
|
+
): Promise<NitroPurchaseResult> => {
|
|
850
|
+
if (!purchaseToken) {
|
|
851
|
+
throw createVegaError(
|
|
852
|
+
ErrorCode.DeveloperError,
|
|
853
|
+
'purchaseToken is required to finish an Amazon Vega transaction.',
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
let lastResponse: VegaResponse | null = null;
|
|
858
|
+
for (
|
|
859
|
+
let attempt = 1;
|
|
860
|
+
attempt <= NOTIFY_FULFILLMENT_MAX_ATTEMPTS;
|
|
861
|
+
attempt += 1
|
|
862
|
+
) {
|
|
863
|
+
const response = await service.notifyFulfillment({
|
|
864
|
+
fulfillmentResult: FULFILLMENT_RESULT_FULFILLED,
|
|
865
|
+
receiptId: purchaseToken,
|
|
866
|
+
});
|
|
867
|
+
if (isSuccess('notify-fulfillment', response?.responseCode)) {
|
|
868
|
+
return {
|
|
869
|
+
responseCode: 0,
|
|
870
|
+
code: '',
|
|
871
|
+
message: '',
|
|
872
|
+
purchaseToken,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
lastResponse = response;
|
|
877
|
+
if (attempt < NOTIFY_FULFILLMENT_MAX_ATTEMPTS) {
|
|
878
|
+
await delay(NOTIFY_FULFILLMENT_RETRY_DELAY_MS);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
ensureSuccessful(
|
|
883
|
+
'notify-fulfillment',
|
|
884
|
+
lastResponse,
|
|
885
|
+
'Failed to notify Amazon Vega fulfillment',
|
|
886
|
+
);
|
|
887
|
+
return {
|
|
888
|
+
responseCode: 0,
|
|
889
|
+
code: '',
|
|
890
|
+
message: '',
|
|
891
|
+
purchaseToken,
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const recoverFulfillablePurchases = async (
|
|
896
|
+
sku: string,
|
|
897
|
+
fallbackProductType?: unknown,
|
|
898
|
+
options?: RecoverPurchasesOptions,
|
|
899
|
+
): Promise<RecoveredNitroPurchases> => {
|
|
900
|
+
const receipts = await getPurchaseUpdateReceipts();
|
|
901
|
+
await hydrateProductTypesForReceipts(receipts);
|
|
902
|
+
const requestedPurchases: NitroPurchase[] = [];
|
|
903
|
+
|
|
904
|
+
for (const receipt of receipts) {
|
|
905
|
+
if (receipt.isCancelled || receipt.cancelDate || receipt.isDeferred) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const purchaseTimestamp = toTimestamp(receipt.purchaseDate);
|
|
910
|
+
if (
|
|
911
|
+
options?.minPurchaseDateMs != null &&
|
|
912
|
+
(purchaseTimestamp === 0 || purchaseTimestamp < options.minPurchaseDateMs)
|
|
913
|
+
) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
const matchesRequestedSku = receiptMatchesRequestedSku(receipt, sku);
|
|
918
|
+
const purchase = mapReceipt(
|
|
919
|
+
receipt,
|
|
920
|
+
receipt.productType ??
|
|
921
|
+
getCachedProductType(receipt, productTypesBySku, sku) ??
|
|
922
|
+
(matchesRequestedSku ? fallbackProductType : undefined),
|
|
923
|
+
resolveReceiptProductId(receipt, matchesRequestedSku ? sku : undefined),
|
|
924
|
+
);
|
|
925
|
+
if (matchesRequestedSku) {
|
|
926
|
+
requestedPurchases.push(purchase);
|
|
927
|
+
}
|
|
928
|
+
emitPurchaseUpdated(purchase);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
return {requestedPurchases};
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const verifyWithIapkit = async (
|
|
935
|
+
params: NitroVerifyPurchaseWithProviderProps,
|
|
936
|
+
): Promise<NitroVerifyPurchaseWithProviderResult> => {
|
|
937
|
+
type IapkitEndpointOptions = NonNullable<
|
|
938
|
+
NitroVerifyPurchaseWithProviderProps['iapkit']
|
|
939
|
+
> & {
|
|
940
|
+
baseUrl?: string | null;
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
function iapkitVerifyUrl(
|
|
944
|
+
iapkit: NitroVerifyPurchaseWithProviderProps['iapkit'],
|
|
945
|
+
): string {
|
|
946
|
+
const endpointOptions = iapkit as
|
|
947
|
+
| IapkitEndpointOptions
|
|
948
|
+
| null
|
|
949
|
+
| undefined;
|
|
950
|
+
const baseUrl =
|
|
951
|
+
typeof endpointOptions?.baseUrl === 'string' &&
|
|
952
|
+
endpointOptions.baseUrl.trim().length > 0
|
|
953
|
+
? endpointOptions.baseUrl.trim()
|
|
954
|
+
: IAPKIT_DEFAULT_BASE_URL;
|
|
955
|
+
return `${baseUrl.replace(/\/+$/, '')}${IAPKIT_VERIFY_PATH}`;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function normalizeIapkitState(state: unknown): IapkitPurchaseState {
|
|
959
|
+
const normalized =
|
|
960
|
+
typeof state === 'string'
|
|
961
|
+
? state.toLowerCase().replace(/_/g, '-')
|
|
962
|
+
: 'unknown';
|
|
963
|
+
const states = new Set<IapkitPurchaseState>([
|
|
964
|
+
'entitled',
|
|
965
|
+
'pending-acknowledgment',
|
|
966
|
+
'pending',
|
|
967
|
+
'canceled',
|
|
968
|
+
'expired',
|
|
969
|
+
'ready-to-consume',
|
|
970
|
+
'consumed',
|
|
971
|
+
'unknown',
|
|
972
|
+
'inauthentic',
|
|
973
|
+
]);
|
|
974
|
+
return states.has(normalized as IapkitPurchaseState)
|
|
975
|
+
? (normalized as IapkitPurchaseState)
|
|
976
|
+
: 'unknown';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function extractIapkitErrorMessage(
|
|
980
|
+
json: unknown,
|
|
981
|
+
depth = 0,
|
|
982
|
+
): string | null {
|
|
983
|
+
if (depth > MAX_IAPKIT_ERROR_DEPTH) return null;
|
|
984
|
+
if (!json || typeof json !== 'object') return null;
|
|
985
|
+
const record = json as Record<string, unknown>;
|
|
986
|
+
function extractStringMessage(value: string): string {
|
|
987
|
+
if (depth >= MAX_IAPKIT_ERROR_DEPTH) return value;
|
|
988
|
+
try {
|
|
989
|
+
const parsed = JSON.parse(value);
|
|
990
|
+
return parsed && typeof parsed === 'object'
|
|
991
|
+
? (extractIapkitErrorMessage(parsed, depth + 1) ?? value)
|
|
992
|
+
: value;
|
|
993
|
+
} catch {
|
|
994
|
+
return value;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const details = record.details;
|
|
999
|
+
if (details && typeof details === 'object') {
|
|
1000
|
+
const originalError = (details as Record<string, unknown>)
|
|
1001
|
+
.originalError;
|
|
1002
|
+
if (typeof originalError === 'string') {
|
|
1003
|
+
return extractStringMessage(originalError);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const errors = record.errors;
|
|
1008
|
+
if (Array.isArray(errors) && errors.length > 0) {
|
|
1009
|
+
const firstError = errors[0];
|
|
1010
|
+
return typeof firstError === 'string'
|
|
1011
|
+
? extractStringMessage(firstError)
|
|
1012
|
+
: extractIapkitErrorMessage(firstError, depth + 1);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
if (typeof record.message === 'string') {
|
|
1016
|
+
return extractStringMessage(record.message);
|
|
1017
|
+
}
|
|
1018
|
+
if (typeof record.error === 'string') {
|
|
1019
|
+
return extractStringMessage(record.error);
|
|
1020
|
+
}
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function parseIapkitJsonResponse(text: string): unknown | null {
|
|
1025
|
+
if (!text.trim()) return null;
|
|
1026
|
+
try {
|
|
1027
|
+
return JSON.parse(text);
|
|
1028
|
+
} catch {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function isIapkitResultObject(
|
|
1034
|
+
json: unknown,
|
|
1035
|
+
): json is Record<string, unknown> {
|
|
1036
|
+
return Boolean(json) && typeof json === 'object' && !Array.isArray(json);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function hasIapkitErrors(json: unknown): boolean {
|
|
1040
|
+
if (!isIapkitResultObject(json)) return false;
|
|
1041
|
+
const errors = json.errors;
|
|
1042
|
+
return Array.isArray(errors) && errors.length > 0;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function readIapkitResult(
|
|
1046
|
+
json: Record<string, unknown>,
|
|
1047
|
+
status: number,
|
|
1048
|
+
): {
|
|
1049
|
+
isValid: boolean;
|
|
1050
|
+
state: IapkitPurchaseState;
|
|
1051
|
+
} {
|
|
1052
|
+
if (
|
|
1053
|
+
typeof json.isValid !== 'boolean' ||
|
|
1054
|
+
typeof json.state !== 'string' ||
|
|
1055
|
+
json.store !== 'amazon'
|
|
1056
|
+
) {
|
|
1057
|
+
throw createVegaError(
|
|
1058
|
+
ErrorCode.ReceiptFailed,
|
|
1059
|
+
`IAPKit returned malformed response (HTTP ${status}).`,
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
return {
|
|
1064
|
+
isValid: json.isValid,
|
|
1065
|
+
state: normalizeIapkitState(json.state),
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (params.provider !== 'iapkit') {
|
|
1070
|
+
throw createVegaError(
|
|
1071
|
+
ErrorCode.FeatureNotSupported,
|
|
1072
|
+
`Unsupported purchase verification provider: ${params.provider}.`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
const iapkit = params.iapkit;
|
|
1077
|
+
const payloadCount =
|
|
1078
|
+
Number(Boolean(iapkit?.amazon)) +
|
|
1079
|
+
Number(Boolean(iapkit?.apple)) +
|
|
1080
|
+
Number(Boolean(iapkit?.google));
|
|
1081
|
+
const amazon = iapkit?.amazon;
|
|
1082
|
+
if (payloadCount !== 1 || !amazon) {
|
|
1083
|
+
throw createVegaError(
|
|
1084
|
+
ErrorCode.DeveloperError,
|
|
1085
|
+
'Amazon Vega IAPKit verification requires exactly one amazon payload.',
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const receiptId =
|
|
1090
|
+
typeof amazon.receiptId === 'string' ? amazon.receiptId.trim() : '';
|
|
1091
|
+
if (!receiptId) {
|
|
1092
|
+
throw createVegaError(
|
|
1093
|
+
ErrorCode.DeveloperError,
|
|
1094
|
+
'Amazon Vega IAPKit verification requires amazon.receiptId.',
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
let userId = typeof amazon.userId === 'string' ? amazon.userId.trim() : '';
|
|
1099
|
+
if (!userId) {
|
|
1100
|
+
await getUserData();
|
|
1101
|
+
userId = cachedUserData?.userId?.trim() ?? '';
|
|
1102
|
+
}
|
|
1103
|
+
if (!userId) {
|
|
1104
|
+
throw createVegaError(
|
|
1105
|
+
ErrorCode.DeveloperError,
|
|
1106
|
+
'Amazon Vega IAPKit verification could not resolve userId.',
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
const apiKey =
|
|
1111
|
+
typeof iapkit?.apiKey === 'string' ? iapkit.apiKey.trim() : '';
|
|
1112
|
+
let response: Response;
|
|
1113
|
+
try {
|
|
1114
|
+
const controller = new AbortController();
|
|
1115
|
+
const timeout = setTimeout(
|
|
1116
|
+
() => controller.abort(),
|
|
1117
|
+
IAPKIT_VERIFY_TIMEOUT_MS,
|
|
1118
|
+
);
|
|
1119
|
+
response = await fetch(iapkitVerifyUrl(iapkit), {
|
|
1120
|
+
method: 'POST',
|
|
1121
|
+
headers: {
|
|
1122
|
+
'Content-Type': 'application/json',
|
|
1123
|
+
...(apiKey ? {Authorization: `Bearer ${apiKey}`} : {}),
|
|
1124
|
+
},
|
|
1125
|
+
body: JSON.stringify({
|
|
1126
|
+
store: 'amazon',
|
|
1127
|
+
userId,
|
|
1128
|
+
receiptId,
|
|
1129
|
+
...(amazon.sandbox == null ? {} : {sandbox: amazon.sandbox}),
|
|
1130
|
+
}),
|
|
1131
|
+
signal: controller.signal,
|
|
1132
|
+
}).finally(() => clearTimeout(timeout));
|
|
1133
|
+
} catch (error) {
|
|
1134
|
+
throw createVegaError(
|
|
1135
|
+
ErrorCode.NetworkError,
|
|
1136
|
+
error instanceof Error
|
|
1137
|
+
? error.message
|
|
1138
|
+
: 'Failed to reach IAPKit verification endpoint.',
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
let text: string;
|
|
1142
|
+
try {
|
|
1143
|
+
text = await response.text();
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
throw createVegaError(
|
|
1146
|
+
ErrorCode.NetworkError,
|
|
1147
|
+
error instanceof Error
|
|
1148
|
+
? error.message
|
|
1149
|
+
: 'Failed to read IAPKit verification response.',
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
const json = parseIapkitJsonResponse(text);
|
|
1153
|
+
|
|
1154
|
+
if (!response.ok) {
|
|
1155
|
+
throw createVegaError(
|
|
1156
|
+
ErrorCode.ReceiptFailed,
|
|
1157
|
+
extractIapkitErrorMessage(json) ?? `HTTP ${response.status}`,
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (json === null) {
|
|
1162
|
+
throw createVegaError(
|
|
1163
|
+
ErrorCode.ReceiptFailed,
|
|
1164
|
+
`IAPKit returned non-JSON response (HTTP ${response.status}).`,
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
if (!isIapkitResultObject(json)) {
|
|
1169
|
+
throw createVegaError(
|
|
1170
|
+
ErrorCode.ReceiptFailed,
|
|
1171
|
+
`IAPKit returned malformed response (HTTP ${response.status}).`,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
if (hasIapkitErrors(json)) {
|
|
1176
|
+
throw createVegaError(
|
|
1177
|
+
ErrorCode.ReceiptFailed,
|
|
1178
|
+
extractIapkitErrorMessage(json) ?? 'IAPKit verification failed.',
|
|
1179
|
+
);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const result = readIapkitResult(json, response.status);
|
|
1183
|
+
return {
|
|
1184
|
+
provider: 'iapkit',
|
|
1185
|
+
iapkit: {
|
|
1186
|
+
isValid: result.isValid,
|
|
1187
|
+
state: result.state,
|
|
1188
|
+
store: 'amazon',
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
};
|
|
1192
|
+
|
|
1193
|
+
const module: VegaRnIapModule = {
|
|
1194
|
+
async initConnection(): Promise<boolean> {
|
|
1195
|
+
return true;
|
|
1196
|
+
},
|
|
1197
|
+
async endConnection(): Promise<boolean> {
|
|
1198
|
+
productTypesBySku.clear();
|
|
1199
|
+
subscriptionBasesBySku.clear();
|
|
1200
|
+
subscriptionParentsBySku.clear();
|
|
1201
|
+
cachedUserData = null;
|
|
1202
|
+
return true;
|
|
1203
|
+
},
|
|
1204
|
+
async fetchProducts(skus: string[], type: string): Promise<NitroProduct[]> {
|
|
1205
|
+
if (!Array.isArray(skus) || skus.length === 0) {
|
|
1206
|
+
throw createVegaError(ErrorCode.EmptySkuList, 'No SKUs provided');
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const products = await getProductData(
|
|
1210
|
+
skus,
|
|
1211
|
+
'Failed to fetch Amazon Vega products',
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
return products
|
|
1215
|
+
.filter((product) => {
|
|
1216
|
+
cacheProductMetadata(product);
|
|
1217
|
+
const openIapType = productTypeToOpenIap(getProductType(product));
|
|
1218
|
+
if (type === 'all') return true;
|
|
1219
|
+
if (type === 'subs') return openIapType === 'subs';
|
|
1220
|
+
return openIapType === 'in-app';
|
|
1221
|
+
})
|
|
1222
|
+
.map(mapProduct);
|
|
1223
|
+
},
|
|
1224
|
+
async requestPurchase(
|
|
1225
|
+
request: Parameters<RnIap['requestPurchase']>[0],
|
|
1226
|
+
): Promise<Awaited<ReturnType<RnIap['requestPurchase']>>> {
|
|
1227
|
+
let sku: string | undefined;
|
|
1228
|
+
try {
|
|
1229
|
+
sku = getSkuFromRequest(request);
|
|
1230
|
+
const androidRequest = request.google ?? request.android;
|
|
1231
|
+
const fallbackProductType = hasSubscriptionRequestContext(
|
|
1232
|
+
androidRequest?.subscriptionOffers,
|
|
1233
|
+
)
|
|
1234
|
+
? PRODUCT_TYPE_SUBSCRIPTION
|
|
1235
|
+
: productTypesBySku.get(sku);
|
|
1236
|
+
if (fallbackProductType != null) {
|
|
1237
|
+
productTypesBySku.set(sku, fallbackProductType);
|
|
1238
|
+
}
|
|
1239
|
+
let response: VegaPurchaseResponse;
|
|
1240
|
+
const purchaseStartedAtMs =
|
|
1241
|
+
Date.now() - PURCHASE_RECOVERY_CLOCK_SKEW_MS;
|
|
1242
|
+
try {
|
|
1243
|
+
response = await service.purchase({sku});
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
if (isVegaParserError(error)) {
|
|
1246
|
+
try {
|
|
1247
|
+
const recovered = await recoverFulfillablePurchases(
|
|
1248
|
+
sku,
|
|
1249
|
+
fallbackProductType,
|
|
1250
|
+
{minPurchaseDateMs: purchaseStartedAtMs},
|
|
1251
|
+
);
|
|
1252
|
+
if (recovered.requestedPurchases.length > 0) {
|
|
1253
|
+
return recovered.requestedPurchases;
|
|
1254
|
+
}
|
|
1255
|
+
} catch {
|
|
1256
|
+
// Keep the original parser error as the source of truth.
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
throw error;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
if (
|
|
1263
|
+
!isSuccess('purchase', response.responseCode) &&
|
|
1264
|
+
shouldRecoverPurchaseResponse(response.responseCode)
|
|
1265
|
+
) {
|
|
1266
|
+
try {
|
|
1267
|
+
const recovered = await recoverFulfillablePurchases(
|
|
1268
|
+
sku,
|
|
1269
|
+
fallbackProductType,
|
|
1270
|
+
);
|
|
1271
|
+
if (recovered.requestedPurchases.length > 0) {
|
|
1272
|
+
return recovered.requestedPurchases;
|
|
1273
|
+
}
|
|
1274
|
+
} catch {
|
|
1275
|
+
// Keep the original purchase response as the source of truth.
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
ensureSuccessful(
|
|
1280
|
+
'purchase',
|
|
1281
|
+
response,
|
|
1282
|
+
'Failed to complete Amazon Vega purchase',
|
|
1283
|
+
sku,
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
if (!response.receipt) return [];
|
|
1287
|
+
|
|
1288
|
+
cachedUserData = response.userData ?? cachedUserData;
|
|
1289
|
+
const purchase = mapReceipt(
|
|
1290
|
+
response.receipt,
|
|
1291
|
+
fallbackProductType,
|
|
1292
|
+
sku,
|
|
1293
|
+
);
|
|
1294
|
+
emitPurchaseUpdated(purchase);
|
|
1295
|
+
return [purchase];
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
emitPurchaseError(
|
|
1298
|
+
toPurchaseErrorResult(
|
|
1299
|
+
error,
|
|
1300
|
+
'Failed to complete Amazon Vega purchase',
|
|
1301
|
+
),
|
|
1302
|
+
);
|
|
1303
|
+
throw error;
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
getAvailablePurchases,
|
|
1307
|
+
async getActiveSubscriptions(
|
|
1308
|
+
subscriptionIds?: string[],
|
|
1309
|
+
): Promise<NitroActiveSubscription[]> {
|
|
1310
|
+
const requestedIds = new Set(subscriptionIds ?? []);
|
|
1311
|
+
const purchases = await getAvailablePurchases({android: {type: 'subs'}});
|
|
1312
|
+
return purchases
|
|
1313
|
+
.filter(
|
|
1314
|
+
(purchase) =>
|
|
1315
|
+
purchase.isAutoRenewing &&
|
|
1316
|
+
(requestedIds.size === 0 || requestedIds.has(purchase.productId)),
|
|
1317
|
+
)
|
|
1318
|
+
.map((purchase) => ({
|
|
1319
|
+
productId: purchase.productId,
|
|
1320
|
+
isActive: true,
|
|
1321
|
+
transactionId: purchase.id,
|
|
1322
|
+
purchaseToken: purchase.purchaseToken ?? null,
|
|
1323
|
+
transactionDate: purchase.transactionDate,
|
|
1324
|
+
autoRenewingAndroid: purchase.autoRenewingAndroid ?? true,
|
|
1325
|
+
basePlanIdAndroid: purchase.productId,
|
|
1326
|
+
currentPlanId: purchase.productId,
|
|
1327
|
+
purchaseTokenAndroid: purchase.purchaseTokenAndroid ?? null,
|
|
1328
|
+
}));
|
|
1329
|
+
},
|
|
1330
|
+
async hasActiveSubscriptions(subscriptionIds?: string[]): Promise<boolean> {
|
|
1331
|
+
const subscriptions =
|
|
1332
|
+
await module.getActiveSubscriptions?.(subscriptionIds);
|
|
1333
|
+
return Boolean(subscriptions?.length);
|
|
1334
|
+
},
|
|
1335
|
+
async finishTransaction(
|
|
1336
|
+
params: Parameters<RnIap['finishTransaction']>[0],
|
|
1337
|
+
): Promise<NitroPurchaseResult> {
|
|
1338
|
+
const token = params.android?.purchaseToken;
|
|
1339
|
+
return finishReceipt(token ?? '');
|
|
1340
|
+
},
|
|
1341
|
+
async acknowledgePurchaseAndroid(purchaseToken): Promise<boolean> {
|
|
1342
|
+
await finishReceipt(purchaseToken);
|
|
1343
|
+
return true;
|
|
1344
|
+
},
|
|
1345
|
+
async consumePurchaseAndroid(purchaseToken): Promise<boolean> {
|
|
1346
|
+
await finishReceipt(purchaseToken);
|
|
1347
|
+
return true;
|
|
1348
|
+
},
|
|
1349
|
+
async restorePurchases(): Promise<void> {
|
|
1350
|
+
const purchases = await getAvailablePurchases({
|
|
1351
|
+
android: {includeSuspended: false},
|
|
1352
|
+
});
|
|
1353
|
+
purchases.forEach(emitPurchaseUpdated);
|
|
1354
|
+
},
|
|
1355
|
+
addPurchaseUpdatedListener(listener): number {
|
|
1356
|
+
const token = nextPurchaseUpdateListenerToken++;
|
|
1357
|
+
purchaseUpdateListeners.set(token, listener);
|
|
1358
|
+
return token;
|
|
1359
|
+
},
|
|
1360
|
+
addPurchaseErrorListener(listener): void {
|
|
1361
|
+
purchaseErrorListeners.add(listener);
|
|
1362
|
+
},
|
|
1363
|
+
removePurchaseUpdatedListener(token): void {
|
|
1364
|
+
purchaseUpdateListeners.delete(token);
|
|
1365
|
+
},
|
|
1366
|
+
removePurchaseErrorListener(listener): void {
|
|
1367
|
+
purchaseErrorListeners.delete(listener);
|
|
1368
|
+
},
|
|
1369
|
+
addPromotedProductListenerIOS(): void {},
|
|
1370
|
+
removePromotedProductListenerIOS(): void {},
|
|
1371
|
+
async getAppTransactionIOS(): Promise<null> {
|
|
1372
|
+
return throwUnsupportedFeature('getAppTransactionIOS');
|
|
1373
|
+
},
|
|
1374
|
+
async requestPromotedProductIOS(): Promise<null> {
|
|
1375
|
+
return throwUnsupportedFeature('requestPromotedProductIOS');
|
|
1376
|
+
},
|
|
1377
|
+
async getPromotedProductIOS(): Promise<null> {
|
|
1378
|
+
return throwUnsupportedFeature('getPromotedProductIOS');
|
|
1379
|
+
},
|
|
1380
|
+
async buyPromotedProductIOS(): Promise<void> {
|
|
1381
|
+
return throwUnsupportedFeature('buyPromotedProductIOS');
|
|
1382
|
+
},
|
|
1383
|
+
async presentCodeRedemptionSheetIOS(): Promise<boolean> {
|
|
1384
|
+
return throwUnsupportedFeature('presentCodeRedemptionSheetIOS');
|
|
1385
|
+
},
|
|
1386
|
+
async clearTransactionIOS(): Promise<void> {
|
|
1387
|
+
return throwUnsupportedFeature('clearTransactionIOS');
|
|
1388
|
+
},
|
|
1389
|
+
async beginRefundRequestIOS(): Promise<null> {
|
|
1390
|
+
return throwUnsupportedFeature('beginRefundRequestIOS');
|
|
1391
|
+
},
|
|
1392
|
+
async subscriptionStatusIOS(): Promise<null> {
|
|
1393
|
+
return throwUnsupportedFeature('subscriptionStatusIOS');
|
|
1394
|
+
},
|
|
1395
|
+
async currentEntitlementIOS(): Promise<null> {
|
|
1396
|
+
return throwUnsupportedFeature('currentEntitlementIOS');
|
|
1397
|
+
},
|
|
1398
|
+
async latestTransactionIOS(): Promise<null> {
|
|
1399
|
+
return throwUnsupportedFeature('latestTransactionIOS');
|
|
1400
|
+
},
|
|
1401
|
+
async getPendingTransactionsIOS(): Promise<NitroPurchase[]> {
|
|
1402
|
+
return throwUnsupportedFeature('getPendingTransactionsIOS');
|
|
1403
|
+
},
|
|
1404
|
+
async getAllTransactionsIOS(): Promise<NitroPurchase[]> {
|
|
1405
|
+
return throwUnsupportedFeature('getAllTransactionsIOS');
|
|
1406
|
+
},
|
|
1407
|
+
async syncIOS(): Promise<boolean> {
|
|
1408
|
+
return throwUnsupportedFeature('syncIOS');
|
|
1409
|
+
},
|
|
1410
|
+
async showManageSubscriptionsIOS(): Promise<NitroPurchase[]> {
|
|
1411
|
+
return throwUnsupportedFeature('showManageSubscriptionsIOS');
|
|
1412
|
+
},
|
|
1413
|
+
async deepLinkToSubscriptionsIOS(): Promise<boolean> {
|
|
1414
|
+
return throwUnsupportedFeature('deepLinkToSubscriptionsIOS');
|
|
1415
|
+
},
|
|
1416
|
+
async isEligibleForIntroOfferIOS(): Promise<boolean> {
|
|
1417
|
+
return throwUnsupportedFeature('isEligibleForIntroOfferIOS');
|
|
1418
|
+
},
|
|
1419
|
+
async getReceiptDataIOS(): Promise<string> {
|
|
1420
|
+
return throwUnsupportedFeature('getReceiptDataIOS');
|
|
1421
|
+
},
|
|
1422
|
+
async getReceiptIOS(): Promise<string> {
|
|
1423
|
+
return throwUnsupportedFeature('getReceiptIOS');
|
|
1424
|
+
},
|
|
1425
|
+
async requestReceiptRefreshIOS(): Promise<string> {
|
|
1426
|
+
return throwUnsupportedFeature('requestReceiptRefreshIOS');
|
|
1427
|
+
},
|
|
1428
|
+
async isTransactionVerifiedIOS(): Promise<boolean> {
|
|
1429
|
+
return throwUnsupportedFeature('isTransactionVerifiedIOS');
|
|
1430
|
+
},
|
|
1431
|
+
async getTransactionJwsIOS(): Promise<null> {
|
|
1432
|
+
return throwUnsupportedFeature('getTransactionJwsIOS');
|
|
1433
|
+
},
|
|
1434
|
+
async validateReceipt(): Promise<never> {
|
|
1435
|
+
return throwUnsupportedFeature('validateReceipt');
|
|
1436
|
+
},
|
|
1437
|
+
async getStorefront(): Promise<string> {
|
|
1438
|
+
return getStorefront();
|
|
1439
|
+
},
|
|
1440
|
+
async getStorefrontIOS(): Promise<string> {
|
|
1441
|
+
return getStorefront();
|
|
1442
|
+
},
|
|
1443
|
+
async verifyPurchaseWithProvider(params) {
|
|
1444
|
+
return verifyWithIapkit(params);
|
|
1445
|
+
},
|
|
1446
|
+
async deepLinkToSubscriptionsAndroid(): Promise<void> {
|
|
1447
|
+
return throwUnsupportedFeature('deepLinkToSubscriptionsAndroid');
|
|
1448
|
+
},
|
|
1449
|
+
async checkAlternativeBillingAvailabilityAndroid(): Promise<boolean> {
|
|
1450
|
+
return throwUnsupportedFeature(
|
|
1451
|
+
'checkAlternativeBillingAvailabilityAndroid',
|
|
1452
|
+
);
|
|
1453
|
+
},
|
|
1454
|
+
async showAlternativeBillingDialogAndroid(): Promise<boolean> {
|
|
1455
|
+
return throwUnsupportedFeature('showAlternativeBillingDialogAndroid');
|
|
1456
|
+
},
|
|
1457
|
+
async createAlternativeBillingTokenAndroid(): Promise<null> {
|
|
1458
|
+
return throwUnsupportedFeature('createAlternativeBillingTokenAndroid');
|
|
1459
|
+
},
|
|
1460
|
+
addUserChoiceBillingListenerAndroid(): void {},
|
|
1461
|
+
removeUserChoiceBillingListenerAndroid(): void {},
|
|
1462
|
+
addDeveloperProvidedBillingListenerAndroid(): void {},
|
|
1463
|
+
removeDeveloperProvidedBillingListenerAndroid(): void {},
|
|
1464
|
+
addSubscriptionBillingIssueListener(): void {},
|
|
1465
|
+
removeSubscriptionBillingIssueListener(): void {},
|
|
1466
|
+
enableBillingProgramAndroid(): void {
|
|
1467
|
+
throwUnsupportedFeature('enableBillingProgramAndroid');
|
|
1468
|
+
},
|
|
1469
|
+
async isBillingProgramAvailableAndroid(): Promise<never> {
|
|
1470
|
+
return throwUnsupportedFeature('isBillingProgramAvailableAndroid');
|
|
1471
|
+
},
|
|
1472
|
+
async createBillingProgramReportingDetailsAndroid(): Promise<never> {
|
|
1473
|
+
return throwUnsupportedFeature(
|
|
1474
|
+
'createBillingProgramReportingDetailsAndroid',
|
|
1475
|
+
);
|
|
1476
|
+
},
|
|
1477
|
+
async launchExternalLinkAndroid(): Promise<boolean> {
|
|
1478
|
+
return throwUnsupportedFeature('launchExternalLinkAndroid');
|
|
1479
|
+
},
|
|
1480
|
+
async canPresentExternalPurchaseNoticeIOS(): Promise<boolean> {
|
|
1481
|
+
return throwUnsupportedFeature('canPresentExternalPurchaseNoticeIOS');
|
|
1482
|
+
},
|
|
1483
|
+
async presentExternalPurchaseNoticeSheetIOS(): Promise<never> {
|
|
1484
|
+
return throwUnsupportedFeature('presentExternalPurchaseNoticeSheetIOS');
|
|
1485
|
+
},
|
|
1486
|
+
async presentExternalPurchaseLinkIOS(): Promise<never> {
|
|
1487
|
+
return throwUnsupportedFeature('presentExternalPurchaseLinkIOS');
|
|
1488
|
+
},
|
|
1489
|
+
async isEligibleForExternalPurchaseCustomLinkIOS(): Promise<boolean> {
|
|
1490
|
+
return throwUnsupportedFeature(
|
|
1491
|
+
'isEligibleForExternalPurchaseCustomLinkIOS',
|
|
1492
|
+
);
|
|
1493
|
+
},
|
|
1494
|
+
async getExternalPurchaseCustomLinkTokenIOS(): Promise<never> {
|
|
1495
|
+
return throwUnsupportedFeature('getExternalPurchaseCustomLinkTokenIOS');
|
|
1496
|
+
},
|
|
1497
|
+
async showExternalPurchaseCustomLinkNoticeIOS(): Promise<never> {
|
|
1498
|
+
return throwUnsupportedFeature('showExternalPurchaseCustomLinkNoticeIOS');
|
|
1499
|
+
},
|
|
1500
|
+
};
|
|
1501
|
+
|
|
1502
|
+
return module as RnIap;
|
|
1503
|
+
}
|