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