@idealyst/payments 1.2.108 → 1.2.110
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 +54 -56
- package/package.json +11 -11
- package/src/constants.ts +3 -4
- package/src/errors.ts +53 -21
- package/src/index.native.ts +30 -18
- package/src/index.ts +30 -18
- package/src/index.web.ts +30 -18
- package/src/payments.native.ts +280 -190
- package/src/payments.web.ts +47 -68
- package/src/types.ts +136 -122
- package/src/usePayments.ts +149 -85
package/src/payments.native.ts
CHANGED
|
@@ -1,301 +1,391 @@
|
|
|
1
1
|
// ============================================================================
|
|
2
|
-
// Native
|
|
3
|
-
// Wraps
|
|
2
|
+
// Native IAP Implementation
|
|
3
|
+
// Wraps react-native-iap v14.x for StoreKit 2 (iOS) and Google Play Billing (Android)
|
|
4
|
+
//
|
|
5
|
+
// v14 API changes from earlier versions:
|
|
6
|
+
// - getProducts/getSubscriptions → fetchProducts({ skus, type })
|
|
7
|
+
// - requestPurchase/requestSubscription → requestPurchase({ request, type })
|
|
8
|
+
// - Product fields: id (not productId), displayPrice (not localizedPrice)
|
|
9
|
+
// - Purchase fields: productId, id (transaction ID)
|
|
4
10
|
// ============================================================================
|
|
5
11
|
|
|
6
12
|
import { Platform } from 'react-native';
|
|
7
13
|
import type {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
IAPConfig,
|
|
15
|
+
IAPProviderStatus,
|
|
16
|
+
IAPProduct,
|
|
17
|
+
IAPSubscription,
|
|
18
|
+
IAPPurchase,
|
|
19
|
+
ProductPlatform,
|
|
20
|
+
SubscriptionPeriod,
|
|
21
|
+
SubscriptionPeriodUnit,
|
|
22
|
+
SubscriptionDiscount,
|
|
23
|
+
DiscountPaymentMode,
|
|
14
24
|
} from './types';
|
|
15
25
|
import { INITIAL_PROVIDER_STATUS } from './constants';
|
|
16
|
-
import {
|
|
26
|
+
import { createIAPError, normalizeError } from './errors';
|
|
17
27
|
|
|
18
|
-
// Graceful optional import —
|
|
28
|
+
// Graceful optional import — react-native-iap may not be installed
|
|
19
29
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
-
let
|
|
30
|
+
let RNIap: any = null;
|
|
21
31
|
try {
|
|
22
|
-
|
|
32
|
+
RNIap = require('react-native-iap');
|
|
23
33
|
} catch {
|
|
24
34
|
// Will degrade gracefully when methods are called
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
// Module-level state
|
|
28
|
-
let _status:
|
|
29
|
-
let _config:
|
|
38
|
+
let _status: IAPProviderStatus = { ...INITIAL_PROVIDER_STATUS };
|
|
39
|
+
let _config: IAPConfig = {};
|
|
30
40
|
|
|
31
41
|
/**
|
|
32
|
-
* Get the current
|
|
42
|
+
* Get the current IAP provider status.
|
|
33
43
|
*/
|
|
34
|
-
export function
|
|
44
|
+
export function getIAPStatus(): IAPProviderStatus {
|
|
35
45
|
return { ..._status };
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
/**
|
|
39
|
-
* Initialize the
|
|
49
|
+
* Initialize the IAP connection to the native store.
|
|
40
50
|
*/
|
|
41
|
-
export async function
|
|
42
|
-
|
|
43
|
-
): Promise<void> {
|
|
44
|
-
if (!Stripe) {
|
|
51
|
+
export async function initializeIAP(config?: IAPConfig): Promise<void> {
|
|
52
|
+
if (!RNIap) {
|
|
45
53
|
_status = {
|
|
46
|
-
..._status,
|
|
47
54
|
state: 'error',
|
|
48
|
-
|
|
55
|
+
isStoreAvailable: false,
|
|
56
|
+
error: createIAPError(
|
|
49
57
|
'not_available',
|
|
50
|
-
'
|
|
58
|
+
'react-native-iap is not installed. Run: yarn add react-native-iap',
|
|
51
59
|
),
|
|
52
60
|
};
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
_status = { ..._status, state: 'initializing' };
|
|
57
|
-
_config = config;
|
|
65
|
+
_config = config ?? {};
|
|
58
66
|
|
|
59
67
|
try {
|
|
60
|
-
await
|
|
61
|
-
publishableKey: config.publishableKey,
|
|
62
|
-
merchantIdentifier: config.merchantIdentifier,
|
|
63
|
-
urlScheme: config.urlScheme,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
const availability = await checkPaymentAvailability();
|
|
68
|
+
await RNIap.initConnection();
|
|
67
69
|
|
|
68
70
|
_status = {
|
|
69
71
|
state: 'ready',
|
|
70
|
-
|
|
71
|
-
isPaymentAvailable: availability.some((m) => m.isAvailable),
|
|
72
|
+
isStoreAvailable: true,
|
|
72
73
|
};
|
|
73
74
|
} catch (error) {
|
|
74
75
|
_status = {
|
|
75
|
-
..._status,
|
|
76
76
|
state: 'error',
|
|
77
|
+
isStoreAvailable: false,
|
|
77
78
|
error: normalizeError(error),
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
|
83
|
-
*
|
|
84
|
+
* Fetch products (one-time purchases) from the store by SKU.
|
|
84
85
|
*/
|
|
85
|
-
export async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
unavailableReason: 'Stripe SDK not installed',
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
type: 'google_pay',
|
|
97
|
-
isAvailable: false,
|
|
98
|
-
unavailableReason: 'Stripe SDK not installed',
|
|
99
|
-
},
|
|
100
|
-
{ type: 'card', isAvailable: false, unavailableReason: 'Stripe SDK not installed' },
|
|
101
|
-
];
|
|
86
|
+
export async function getProducts(skus: string[]): Promise<IAPProduct[]> {
|
|
87
|
+
assertReady();
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const products = await RNIap!.fetchProducts({ skus, type: 'in-app' });
|
|
91
|
+
return products.map((p: unknown) => mapProduct(p, 'iap'));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw normalizeError(error);
|
|
102
94
|
}
|
|
95
|
+
}
|
|
103
96
|
|
|
104
|
-
|
|
105
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Fetch subscriptions from the store by SKU.
|
|
99
|
+
*/
|
|
100
|
+
export async function getSubscriptions(
|
|
101
|
+
skus: string[],
|
|
102
|
+
): Promise<IAPSubscription[]> {
|
|
103
|
+
assertReady();
|
|
106
104
|
|
|
107
105
|
try {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
106
|
+
const products = await RNIap!.fetchProducts({ skus, type: 'subs' });
|
|
107
|
+
return products.map(mapSubscription);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw normalizeError(error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
113
112
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
: !isSupported
|
|
120
|
-
? 'Apple Pay is not configured on this device'
|
|
121
|
-
: undefined,
|
|
122
|
-
});
|
|
113
|
+
/**
|
|
114
|
+
* Purchase a one-time product.
|
|
115
|
+
*/
|
|
116
|
+
export async function purchaseProduct(sku: string): Promise<IAPPurchase> {
|
|
117
|
+
assertReady();
|
|
123
118
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
: !isSupported
|
|
130
|
-
? 'Google Pay is not configured on this device'
|
|
131
|
-
: undefined,
|
|
132
|
-
});
|
|
133
|
-
} catch (error) {
|
|
134
|
-
results.push(
|
|
135
|
-
{
|
|
136
|
-
type: 'apple_pay',
|
|
137
|
-
isAvailable: false,
|
|
138
|
-
unavailableReason: String(error),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
type: 'google_pay',
|
|
142
|
-
isAvailable: false,
|
|
143
|
-
unavailableReason: String(error),
|
|
119
|
+
try {
|
|
120
|
+
const purchase = await RNIap!.requestPurchase({
|
|
121
|
+
request: {
|
|
122
|
+
apple: { sku },
|
|
123
|
+
google: { skus: [sku] },
|
|
144
124
|
},
|
|
145
|
-
|
|
146
|
-
|
|
125
|
+
type: 'in-app',
|
|
126
|
+
});
|
|
147
127
|
|
|
148
|
-
|
|
149
|
-
results.push({ type: 'card', isAvailable: true });
|
|
128
|
+
const mapped = mapPurchase(purchase);
|
|
150
129
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
isPaymentAvailable: results.some((m) => m.isAvailable),
|
|
155
|
-
};
|
|
130
|
+
if (_config.autoFinishTransactions) {
|
|
131
|
+
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
132
|
+
}
|
|
156
133
|
|
|
157
|
-
|
|
134
|
+
return mapped;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
throw normalizeError(error);
|
|
137
|
+
}
|
|
158
138
|
}
|
|
159
139
|
|
|
160
140
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
141
|
+
* Purchase a subscription.
|
|
142
|
+
* On Android, an `offerToken` from the subscription's offer details may be required.
|
|
163
143
|
*/
|
|
164
|
-
export async function
|
|
165
|
-
|
|
166
|
-
|
|
144
|
+
export async function purchaseSubscription(
|
|
145
|
+
sku: string,
|
|
146
|
+
offerToken?: string,
|
|
147
|
+
): Promise<IAPPurchase> {
|
|
167
148
|
assertReady();
|
|
168
149
|
|
|
169
|
-
if (!request.clientSecret) {
|
|
170
|
-
throw createPaymentError(
|
|
171
|
-
'invalid_request',
|
|
172
|
-
'clientSecret is required for confirmPayment. Create a PaymentIntent on your server first.',
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
150
|
try {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
if (error) {
|
|
184
|
-
throw error;
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
const googleRequest: any = { skus: [sku] };
|
|
153
|
+
if (offerToken) {
|
|
154
|
+
googleRequest.offerToken = offerToken;
|
|
185
155
|
}
|
|
186
156
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
157
|
+
const purchase = await RNIap!.requestPurchase({
|
|
158
|
+
request: {
|
|
159
|
+
apple: { sku },
|
|
160
|
+
google: googleRequest,
|
|
161
|
+
},
|
|
162
|
+
type: 'subs',
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const mapped = mapPurchase(purchase);
|
|
166
|
+
|
|
167
|
+
if (_config.autoFinishTransactions) {
|
|
168
|
+
await RNIap!.finishTransaction({ purchase, isConsumable: false });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return mapped;
|
|
192
172
|
} catch (error) {
|
|
193
173
|
throw normalizeError(error);
|
|
194
174
|
}
|
|
195
175
|
}
|
|
196
176
|
|
|
197
177
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
178
|
+
* Finish a transaction. Call this after server-side receipt validation.
|
|
179
|
+
*
|
|
180
|
+
* @param purchase The purchase to finish.
|
|
181
|
+
* @param isConsumable Whether the product is consumable (can be purchased again).
|
|
200
182
|
*/
|
|
201
|
-
export async function
|
|
202
|
-
|
|
203
|
-
|
|
183
|
+
export async function finishTransaction(
|
|
184
|
+
purchase: IAPPurchase,
|
|
185
|
+
isConsumable?: boolean,
|
|
186
|
+
): Promise<void> {
|
|
204
187
|
assertReady();
|
|
205
188
|
|
|
206
189
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
190
|
+
// finishTransaction expects the raw purchase object; we reconstruct the
|
|
191
|
+
// minimal shape that react-native-iap needs (id + productId + purchaseToken)
|
|
192
|
+
await RNIap!.finishTransaction({
|
|
193
|
+
purchase: {
|
|
194
|
+
id: purchase.transactionId,
|
|
195
|
+
productId: purchase.sku,
|
|
196
|
+
purchaseToken: purchase.purchaseToken,
|
|
197
|
+
},
|
|
198
|
+
isConsumable: isConsumable ?? false,
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw normalizeError(error);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
219
204
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Restore previous purchases (e.g., after reinstall or on a new device).
|
|
207
|
+
*/
|
|
208
|
+
export async function restorePurchases(): Promise<IAPPurchase[]> {
|
|
209
|
+
assertReady();
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const purchases = await RNIap!.getAvailablePurchases();
|
|
213
|
+
return purchases.map((p: unknown) => mapPurchase(p, 'restored'));
|
|
224
214
|
} catch (error) {
|
|
225
215
|
throw normalizeError(error);
|
|
226
216
|
}
|
|
227
217
|
}
|
|
228
218
|
|
|
219
|
+
/**
|
|
220
|
+
* End the IAP connection. Call on cleanup (e.g., app unmount).
|
|
221
|
+
*/
|
|
222
|
+
export async function endConnection(): Promise<void> {
|
|
223
|
+
if (!RNIap) return;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await RNIap.endConnection();
|
|
227
|
+
_status = { ...INITIAL_PROVIDER_STATUS };
|
|
228
|
+
} catch {
|
|
229
|
+
// Ignore errors during cleanup
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
229
233
|
// ============================================================================
|
|
230
234
|
// Internal Helpers
|
|
231
235
|
// ============================================================================
|
|
232
236
|
|
|
233
237
|
function assertReady(): void {
|
|
234
|
-
if (!
|
|
235
|
-
throw
|
|
238
|
+
if (!RNIap) {
|
|
239
|
+
throw createIAPError(
|
|
236
240
|
'not_available',
|
|
237
|
-
'
|
|
241
|
+
'react-native-iap is not installed',
|
|
238
242
|
);
|
|
239
243
|
}
|
|
240
244
|
if (_status.state !== 'ready') {
|
|
241
|
-
throw
|
|
245
|
+
throw createIAPError(
|
|
242
246
|
'not_initialized',
|
|
243
|
-
'
|
|
247
|
+
'IAP connection not initialized. Call initializeIAP() first.',
|
|
244
248
|
);
|
|
245
249
|
}
|
|
246
250
|
}
|
|
247
251
|
|
|
248
|
-
function
|
|
249
|
-
return Platform.OS === 'ios' ? '
|
|
252
|
+
function getPlatform(): ProductPlatform {
|
|
253
|
+
return Platform.OS === 'ios' ? 'ios' : 'android';
|
|
250
254
|
}
|
|
251
255
|
|
|
252
|
-
|
|
256
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
257
|
+
function mapProduct(raw: any, type: 'iap' | 'sub' = 'iap'): IAPProduct {
|
|
253
258
|
return {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
259
|
+
sku: raw.id ?? raw.productId ?? '',
|
|
260
|
+
title: raw.title ?? raw.displayName ?? '',
|
|
261
|
+
description: raw.description ?? '',
|
|
262
|
+
price: typeof raw.price === 'number' ? raw.price : parseFloat(raw.price ?? '0'),
|
|
263
|
+
priceFormatted: raw.displayPrice ?? raw.localizedPrice ?? String(raw.price ?? ''),
|
|
264
|
+
currency: raw.currency ?? '',
|
|
265
|
+
type,
|
|
266
|
+
platform: getPlatform(),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
271
|
+
function mapSubscription(raw: any): IAPSubscription {
|
|
272
|
+
const base = mapProduct(raw, 'sub');
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
...base,
|
|
276
|
+
subscriptionPeriod: parseSubscriptionPeriod(raw),
|
|
277
|
+
introductoryPrice: raw.introductoryPrice
|
|
278
|
+
? mapDiscount(raw.introductoryPrice, 'introductory')
|
|
268
279
|
: undefined,
|
|
280
|
+
discounts: raw.discounts?.map((d: unknown) =>
|
|
281
|
+
mapDiscount(d, 'promotional'),
|
|
282
|
+
),
|
|
269
283
|
};
|
|
270
284
|
}
|
|
271
285
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
287
|
+
function parseSubscriptionPeriod(raw: any): SubscriptionPeriod {
|
|
288
|
+
// iOS: subscriptionPeriodUnitIOS / subscriptionPeriodNumberIOS
|
|
289
|
+
if (raw.subscriptionPeriodUnitIOS) {
|
|
290
|
+
return {
|
|
291
|
+
unit: mapPeriodUnit(raw.subscriptionPeriodUnitIOS),
|
|
292
|
+
numberOfUnits: parseInt(raw.subscriptionPeriodNumberIOS ?? '1', 10),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Android: subscriptionOfferDetailsAndroid may contain period info
|
|
297
|
+
if (raw.subscriptionOfferDetailsAndroid?.length) {
|
|
298
|
+
const offer = raw.subscriptionOfferDetailsAndroid[0];
|
|
299
|
+
const pricingPhases = offer?.pricingPhases?.pricingPhaseList;
|
|
300
|
+
if (pricingPhases?.length) {
|
|
301
|
+
const billingPeriod = pricingPhases[0]?.billingPeriod;
|
|
302
|
+
if (billingPeriod) {
|
|
303
|
+
return parseISO8601Period(billingPeriod);
|
|
304
|
+
}
|
|
286
305
|
}
|
|
287
306
|
}
|
|
288
307
|
|
|
289
|
-
//
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
308
|
+
// Fallback: subscriptionPeriodAndroid (older format)
|
|
309
|
+
if (raw.subscriptionPeriodAndroid) {
|
|
310
|
+
return parseISO8601Period(raw.subscriptionPeriodAndroid);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { unit: 'month', numberOfUnits: 1 };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function mapPeriodUnit(unit: string): SubscriptionPeriodUnit {
|
|
317
|
+
switch (unit.toUpperCase()) {
|
|
318
|
+
case 'DAY':
|
|
319
|
+
return 'day';
|
|
320
|
+
case 'WEEK':
|
|
321
|
+
return 'week';
|
|
322
|
+
case 'MONTH':
|
|
323
|
+
return 'month';
|
|
324
|
+
case 'YEAR':
|
|
325
|
+
return 'year';
|
|
326
|
+
default:
|
|
327
|
+
return 'month';
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseISO8601Period(period: string): SubscriptionPeriod {
|
|
332
|
+
// Simple ISO 8601 duration parser for P{n}{unit} (e.g., P1M, P3M, P1Y, P7D)
|
|
333
|
+
const match = period.match(/P(\d+)([DWMY])/i);
|
|
334
|
+
if (!match) return { unit: 'month', numberOfUnits: 1 };
|
|
335
|
+
|
|
336
|
+
const num = parseInt(match[1], 10);
|
|
337
|
+
switch (match[2].toUpperCase()) {
|
|
338
|
+
case 'D':
|
|
339
|
+
return { unit: 'day', numberOfUnits: num };
|
|
340
|
+
case 'W':
|
|
341
|
+
return { unit: 'week', numberOfUnits: num };
|
|
342
|
+
case 'M':
|
|
343
|
+
return { unit: 'month', numberOfUnits: num };
|
|
344
|
+
case 'Y':
|
|
345
|
+
return { unit: 'year', numberOfUnits: num };
|
|
346
|
+
default:
|
|
347
|
+
return { unit: 'month', numberOfUnits: num };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
352
|
+
function mapDiscount(raw: any, type: 'introductory' | 'promotional'): SubscriptionDiscount {
|
|
353
|
+
const paymentModeMap: Record<string, DiscountPaymentMode> = {
|
|
354
|
+
FREETRIAL: 'freeTrial',
|
|
355
|
+
FREE_TRIAL: 'freeTrial',
|
|
356
|
+
PAYASYOUGO: 'payAsYouGo',
|
|
357
|
+
PAY_AS_YOU_GO: 'payAsYouGo',
|
|
358
|
+
PAYUPFRONT: 'payUpFront',
|
|
359
|
+
PAY_UP_FRONT: 'payUpFront',
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
identifier: raw.identifier,
|
|
364
|
+
price: parseFloat(raw.price ?? '0'),
|
|
365
|
+
priceFormatted: raw.displayPrice ?? raw.localizedPrice ?? raw.price ?? '',
|
|
366
|
+
period: raw.subscriptionPeriod
|
|
367
|
+
? parseISO8601Period(raw.subscriptionPeriod)
|
|
368
|
+
: { unit: 'month', numberOfUnits: 1 },
|
|
369
|
+
paymentMode:
|
|
370
|
+
paymentModeMap[(raw.paymentMode ?? '').toUpperCase()] ?? 'freeTrial',
|
|
371
|
+
type,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
295
374
|
|
|
375
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
376
|
+
function mapPurchase(raw: any, stateOverride?: IAPPurchase['purchaseState']): IAPPurchase {
|
|
377
|
+
// v14: transaction ID is in `id`, product is in `productId`
|
|
296
378
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
379
|
+
sku: raw.productId ?? raw.id ?? '',
|
|
380
|
+
transactionId: raw.id ?? raw.transactionId ?? '',
|
|
381
|
+
transactionDate: raw.transactionDate
|
|
382
|
+
? (typeof raw.transactionDate === 'number'
|
|
383
|
+
? raw.transactionDate
|
|
384
|
+
: parseInt(raw.transactionDate, 10))
|
|
385
|
+
: Date.now(),
|
|
386
|
+
transactionReceipt: raw.transactionReceipt ?? raw.dataAndroid ?? '',
|
|
387
|
+
purchaseToken: raw.purchaseToken,
|
|
388
|
+
isAcknowledged: raw.isAcknowledgedAndroid,
|
|
389
|
+
purchaseState: stateOverride ?? 'purchased',
|
|
300
390
|
};
|
|
301
391
|
}
|