framepayments-react-native 2.2.1 → 3.0.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/FrameReactNative.podspec +44 -0
- package/Package.swift +47 -0
- package/README.md +64 -33
- package/android/build.gradle +9 -3
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameCheckoutActivity.kt +18 -3
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameFlowActivity.kt +16 -3
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameGooglePayActivity.kt +0 -3
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameOnboardingActivity.kt +2 -5
- package/android/src/main/java/com/framepayments/reactnativeframe/FrameSDKModule.kt +13 -11
- package/ios/ApplePayPresenter.swift +1 -4
- package/ios/FramePreloader.h +23 -0
- package/ios/FrameSDKBridge.m +17 -17
- package/ios/FrameSDKBridge.swift +109 -54
- package/lib/native.d.ts +12 -2
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +18 -18
- package/lib/types.d.ts +12 -6
- package/lib/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/native.test.ts +60 -74
- package/src/native.ts +37 -34
- package/src/types.ts +12 -6
- package/ios/FrameReactNative.podspec +0 -32
- package/lib/__tests__/native.test.d.ts +0 -32
- package/lib/__tests__/native.test.d.ts.map +0 -1
- package/lib/__tests__/native.test.js +0 -101
- package/lib/components/FrameApplePayButton.d.ts +0 -30
- package/lib/components/FrameApplePayButton.d.ts.map +0 -1
- package/lib/components/FrameApplePayButton.js +0 -42
- package/lib/components/FrameGooglePayButton.d.ts +0 -28
- package/lib/components/FrameGooglePayButton.d.ts.map +0 -1
- package/lib/components/FrameGooglePayButton.js +0 -24
- package/lib/components/index.d.ts +0 -5
- package/lib/components/index.d.ts.map +0 -1
- package/lib/components/index.js +0 -2
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* presentApplePay, presentGooglePay, presentOnboarding). NativeModules.FrameSDK is mocked.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const mockInitialize = jest.fn((_secretKey: string, _publishableKey: string, _debugMode: boolean) => Promise.resolve());
|
|
6
|
+
const mockInitialize = jest.fn((_secretKey: string, _publishableKey: string, _debugMode: boolean, _applePayMerchantId: string | null, _googlePayMerchantId: string | null, _theme: unknown) => Promise.resolve());
|
|
7
7
|
const mockPresentCheckout = jest.fn((_accountId: unknown, _amount: number) => Promise.resolve('tr_1'));
|
|
8
8
|
const mockPresentCart = jest.fn((_accountId: unknown, _items: unknown[], _shipping: number) => Promise.resolve('tr_2'));
|
|
9
|
-
const mockPresentApplePay = jest.fn((_ownerType: string, _ownerId: string, _amount: number, _currency: string
|
|
10
|
-
const mockPresentGooglePay = jest.fn((_amountCents: number, _ownerType: string, _ownerId: string, _currencyCode: string
|
|
11
|
-
const mockPresentOnboarding = jest.fn((_accountId: unknown, _capabilities: unknown[]
|
|
9
|
+
const mockPresentApplePay = jest.fn((_ownerType: string, _ownerId: string, _amount: number, _currency: string) => Promise.resolve('tr_3'));
|
|
10
|
+
const mockPresentGooglePay = jest.fn((_amountCents: number, _ownerType: string, _ownerId: string, _currencyCode: string) => Promise.resolve('tr_4'));
|
|
11
|
+
const mockPresentOnboarding = jest.fn((_accountId: unknown, _capabilities: unknown[]) => Promise.resolve({ status: 'completed', paymentMethodId: 'pm_1' }));
|
|
12
12
|
|
|
13
13
|
const mockPlatform = { OS: 'ios' as 'ios' | 'android' };
|
|
14
14
|
|
|
@@ -27,16 +27,16 @@ jest.mock('react-native', () => ({
|
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
// Re-import after mock so we get the mocked NativeModules
|
|
30
|
-
let initialize: (opts: { secretKey: string; publishableKey: string; debugMode?: boolean }) => Promise<void>;
|
|
30
|
+
let initialize: (opts: { secretKey: string; publishableKey: string; debugMode?: boolean; applePayMerchantId?: string; googlePayMerchantId?: string }) => Promise<void>;
|
|
31
31
|
let presentCheckout: (opts: { accountId: string; amount: number }) => Promise<string>;
|
|
32
32
|
let presentCart: (opts: {
|
|
33
33
|
accountId: string;
|
|
34
34
|
items: Array<{ id: string; title: string; amountInCents: number; imageUrl: string }>;
|
|
35
35
|
shippingAmountInCents: number;
|
|
36
36
|
}) => Promise<string>;
|
|
37
|
-
let presentApplePay: (opts: { amount: number; currency?: string; owner: { type: 'customer' | 'account'; id: string }
|
|
38
|
-
let presentGooglePay: (opts: { amountCents: number; owner: { type: 'customer' | 'account'; id: string }; currencyCode?: string
|
|
39
|
-
let presentOnboarding: (opts: { accountId?: string | null; capabilities?: string[]
|
|
37
|
+
let presentApplePay: (opts: { amount: number; currency?: string; owner: { type: 'customer' | 'account'; id: string } }) => Promise<string>;
|
|
38
|
+
let presentGooglePay: (opts: { amountCents: number; owner: { type: 'customer' | 'account'; id: string }; currencyCode?: string }) => Promise<string>;
|
|
39
|
+
let presentOnboarding: (opts: { accountId?: string | null; capabilities?: string[] }) => Promise<unknown>;
|
|
40
40
|
|
|
41
41
|
beforeEach(() => {
|
|
42
42
|
jest.resetModules();
|
|
@@ -57,15 +57,33 @@ beforeEach(() => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
describe('initialize', () => {
|
|
60
|
-
it('calls native FrameSDK.initialize with
|
|
60
|
+
it('calls native FrameSDK.initialize with all six positional args', () => {
|
|
61
61
|
initialize({ secretKey: 'sk_test_xxx', publishableKey: 'pk_test_xxx', debugMode: true });
|
|
62
62
|
expect(mockInitialize).toHaveBeenCalledTimes(1);
|
|
63
|
-
expect(mockInitialize).toHaveBeenCalledWith('sk_test_xxx', 'pk_test_xxx', true, null);
|
|
63
|
+
expect(mockInitialize).toHaveBeenCalledWith('sk_test_xxx', 'pk_test_xxx', true, null, null, null);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
|
-
it('defaults debugMode to false', () => {
|
|
66
|
+
it('defaults debugMode to false and both merchant IDs to null', () => {
|
|
67
67
|
initialize({ secretKey: 'sk_test_yyy', publishableKey: 'pk_test_yyy' });
|
|
68
|
-
expect(mockInitialize).toHaveBeenCalledWith('sk_test_yyy', 'pk_test_yyy', false, null);
|
|
68
|
+
expect(mockInitialize).toHaveBeenCalledWith('sk_test_yyy', 'pk_test_yyy', false, null, null, null);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('forwards applePayMerchantId to native init', () => {
|
|
72
|
+
initialize({
|
|
73
|
+
secretKey: 'sk_test',
|
|
74
|
+
publishableKey: 'pk_test',
|
|
75
|
+
applePayMerchantId: 'merchant.com.example',
|
|
76
|
+
});
|
|
77
|
+
expect(mockInitialize).toHaveBeenCalledWith('sk_test', 'pk_test', false, 'merchant.com.example', null, null);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('forwards googlePayMerchantId to native init', () => {
|
|
81
|
+
initialize({
|
|
82
|
+
secretKey: 'sk_test',
|
|
83
|
+
publishableKey: 'pk_test',
|
|
84
|
+
googlePayMerchantId: 'BCR2DN4T...',
|
|
85
|
+
});
|
|
86
|
+
expect(mockInitialize).toHaveBeenCalledWith('sk_test', 'pk_test', false, null, 'BCR2DN4T...', null);
|
|
69
87
|
});
|
|
70
88
|
|
|
71
89
|
it('throws if secretKey is missing', () => {
|
|
@@ -153,7 +171,7 @@ describe('presentCart', () => {
|
|
|
153
171
|
describe('presentApplePay', () => {
|
|
154
172
|
it('throws NOT_INITIALIZED if initialize was not called', async () => {
|
|
155
173
|
try {
|
|
156
|
-
await presentApplePay({ amount: 100, owner: { type: 'account', id: 'acct_1' }
|
|
174
|
+
await presentApplePay({ amount: 100, owner: { type: 'account', id: 'acct_1' } });
|
|
157
175
|
expect(true).toBe(false);
|
|
158
176
|
} catch (e: any) {
|
|
159
177
|
expect(e.code).toBe('NOT_INITIALIZED');
|
|
@@ -164,7 +182,7 @@ describe('presentApplePay', () => {
|
|
|
164
182
|
it('throws INVALID_OWNER when owner is missing', async () => {
|
|
165
183
|
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
166
184
|
try {
|
|
167
|
-
await presentApplePay({ amount: 100
|
|
185
|
+
await presentApplePay({ amount: 100 } as any);
|
|
168
186
|
expect(true).toBe(false);
|
|
169
187
|
} catch (e: any) {
|
|
170
188
|
expect(e.code).toBe('INVALID_OWNER');
|
|
@@ -175,7 +193,7 @@ describe('presentApplePay', () => {
|
|
|
175
193
|
it('throws INVALID_OWNER when owner.id is empty', async () => {
|
|
176
194
|
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
177
195
|
try {
|
|
178
|
-
await presentApplePay({ amount: 100, owner: { type: 'account', id: '' }
|
|
196
|
+
await presentApplePay({ amount: 100, owner: { type: 'account', id: '' } });
|
|
179
197
|
expect(true).toBe(false);
|
|
180
198
|
} catch (e: any) {
|
|
181
199
|
expect(e.code).toBe('INVALID_OWNER');
|
|
@@ -183,47 +201,39 @@ describe('presentApplePay', () => {
|
|
|
183
201
|
expect(mockPresentApplePay).not.toHaveBeenCalled();
|
|
184
202
|
});
|
|
185
203
|
|
|
186
|
-
it('throws INVALID_MERCHANT_ID when merchantId is missing', async () => {
|
|
187
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
188
|
-
try {
|
|
189
|
-
await presentApplePay({ amount: 100, owner: { type: 'account', id: 'acct_1' } } as any);
|
|
190
|
-
expect(true).toBe(false);
|
|
191
|
-
} catch (e: any) {
|
|
192
|
-
expect(e.code).toBe('INVALID_MERCHANT_ID');
|
|
193
|
-
}
|
|
194
|
-
expect(mockPresentApplePay).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
204
|
it('forwards account owner; resolves with transfer id string', async () => {
|
|
198
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
205
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', applePayMerchantId: 'merchant.test' });
|
|
199
206
|
const result = await presentApplePay({
|
|
200
207
|
amount: 12345,
|
|
201
208
|
currency: 'usd',
|
|
202
209
|
owner: { type: 'account', id: 'acct_1' },
|
|
203
|
-
merchantId: 'merchant.test',
|
|
204
210
|
});
|
|
205
|
-
expect(mockPresentApplePay).toHaveBeenCalledWith('account', 'acct_1', 12345, 'usd'
|
|
211
|
+
expect(mockPresentApplePay).toHaveBeenCalledWith('account', 'acct_1', 12345, 'usd');
|
|
206
212
|
expect(result).toBe('tr_3');
|
|
207
213
|
});
|
|
208
214
|
|
|
209
215
|
it('forwards customer owner; resolves with charge intent id string', async () => {
|
|
210
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
216
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', applePayMerchantId: 'merchant.test' });
|
|
211
217
|
await presentApplePay({
|
|
212
218
|
amount: 9999,
|
|
213
219
|
owner: { type: 'customer', id: 'cus_1' },
|
|
214
|
-
merchantId: 'merchant.test',
|
|
215
220
|
});
|
|
216
|
-
expect(mockPresentApplePay).toHaveBeenCalledWith('customer', 'cus_1', 9999, 'usd'
|
|
221
|
+
expect(mockPresentApplePay).toHaveBeenCalledWith('customer', 'cus_1', 9999, 'usd');
|
|
217
222
|
});
|
|
218
223
|
|
|
219
224
|
it('defaults currency to usd', async () => {
|
|
220
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
221
|
-
await presentApplePay({ amount: 100, owner: { type: 'account', id: 'acct_1' }
|
|
222
|
-
expect(mockPresentApplePay).toHaveBeenCalledWith('account', 'acct_1', 100, 'usd'
|
|
225
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', applePayMerchantId: 'merchant.test' });
|
|
226
|
+
await presentApplePay({ amount: 100, owner: { type: 'account', id: 'acct_1' } });
|
|
227
|
+
expect(mockPresentApplePay).toHaveBeenCalledWith('account', 'acct_1', 100, 'usd');
|
|
223
228
|
});
|
|
224
229
|
});
|
|
225
230
|
|
|
226
231
|
describe('presentGooglePay', () => {
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
// presentGooglePay is Android-only; tests run on Android except where noted.
|
|
234
|
+
mockPlatform.OS = 'android';
|
|
235
|
+
});
|
|
236
|
+
|
|
227
237
|
it('throws NOT_INITIALIZED if initialize was not called', async () => {
|
|
228
238
|
try {
|
|
229
239
|
await presentGooglePay({ amountCents: 100, owner: { type: 'account', id: 'acct_1' } });
|
|
@@ -257,30 +267,29 @@ describe('presentGooglePay', () => {
|
|
|
257
267
|
});
|
|
258
268
|
|
|
259
269
|
it('forwards account owner; resolves with transfer id string', async () => {
|
|
260
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
270
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', googlePayMerchantId: 'BCR2DN4T...' });
|
|
261
271
|
const result = await presentGooglePay({
|
|
262
272
|
amountCents: 9999,
|
|
263
273
|
owner: { type: 'account', id: 'acct_1' },
|
|
264
274
|
currencyCode: 'EUR',
|
|
265
|
-
googlePayMerchantId: 'BCR2DN4T...',
|
|
266
275
|
});
|
|
267
|
-
expect(mockPresentGooglePay).toHaveBeenCalledWith(9999, 'account', 'acct_1', 'EUR'
|
|
276
|
+
expect(mockPresentGooglePay).toHaveBeenCalledWith(9999, 'account', 'acct_1', 'EUR');
|
|
268
277
|
expect(result).toBe('tr_4');
|
|
269
278
|
});
|
|
270
279
|
|
|
271
280
|
it('forwards customer owner; resolves with charge intent id string', async () => {
|
|
272
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
281
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', googlePayMerchantId: 'BCR2DN4T...' });
|
|
273
282
|
await presentGooglePay({
|
|
274
283
|
amountCents: 4242,
|
|
275
284
|
owner: { type: 'customer', id: 'cus_1' },
|
|
276
285
|
});
|
|
277
|
-
expect(mockPresentGooglePay).toHaveBeenCalledWith(4242, 'customer', 'cus_1', 'USD'
|
|
286
|
+
expect(mockPresentGooglePay).toHaveBeenCalledWith(4242, 'customer', 'cus_1', 'USD');
|
|
278
287
|
});
|
|
279
288
|
|
|
280
|
-
it('defaults currencyCode to USD
|
|
281
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
289
|
+
it('defaults currencyCode to USD', async () => {
|
|
290
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', googlePayMerchantId: 'BCR2DN4T...' });
|
|
282
291
|
await presentGooglePay({ amountCents: 100, owner: { type: 'account', id: 'acct_1' } });
|
|
283
|
-
expect(mockPresentGooglePay).toHaveBeenCalledWith(100, 'account', 'acct_1', 'USD'
|
|
292
|
+
expect(mockPresentGooglePay).toHaveBeenCalledWith(100, 'account', 'acct_1', 'USD');
|
|
284
293
|
});
|
|
285
294
|
});
|
|
286
295
|
|
|
@@ -296,46 +305,23 @@ describe('presentOnboarding', () => {
|
|
|
296
305
|
expect(mockPresentOnboarding).not.toHaveBeenCalled();
|
|
297
306
|
});
|
|
298
307
|
|
|
299
|
-
it('calls native presentOnboarding with accountId
|
|
300
|
-
mockPlatform.OS = 'ios';
|
|
308
|
+
it('calls native presentOnboarding with accountId and capabilities only — no merchant params', async () => {
|
|
301
309
|
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
302
310
|
const result = await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc', 'bank_account_verification'] });
|
|
303
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc', 'bank_account_verification']
|
|
311
|
+
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc', 'bank_account_verification']);
|
|
304
312
|
expect(result).toEqual({ status: 'completed', paymentMethodId: 'pm_1' });
|
|
305
313
|
});
|
|
306
314
|
|
|
307
|
-
it('passes null
|
|
308
|
-
mockPlatform.OS = 'ios';
|
|
315
|
+
it('passes null accountId and empty array for capabilities when not provided', async () => {
|
|
309
316
|
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
310
317
|
await presentOnboarding({});
|
|
311
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith(null, []
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('forwards applePayMerchantId on iOS', async () => {
|
|
315
|
-
mockPlatform.OS = 'ios';
|
|
316
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
317
|
-
await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc'], applePayMerchantId: 'merchant.com.example' });
|
|
318
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc'], 'merchant.com.example');
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('ignores applePayMerchantId on Android', async () => {
|
|
322
|
-
mockPlatform.OS = 'android';
|
|
323
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
324
|
-
await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc'], applePayMerchantId: 'merchant.com.example' });
|
|
325
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc'], null);
|
|
318
|
+
expect(mockPresentOnboarding).toHaveBeenCalledWith(null, []);
|
|
326
319
|
});
|
|
327
320
|
|
|
328
|
-
it('
|
|
321
|
+
it('behaves the same on Android — merchant IDs are init-only across both platforms', async () => {
|
|
329
322
|
mockPlatform.OS = 'android';
|
|
330
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
331
|
-
await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc']
|
|
332
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc']
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
it('ignores googlePayMerchantId on iOS', async () => {
|
|
336
|
-
mockPlatform.OS = 'ios';
|
|
337
|
-
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx' });
|
|
338
|
-
await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc'], googlePayMerchantId: 'BCR2DN4T...' });
|
|
339
|
-
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc'], null);
|
|
323
|
+
await initialize({ secretKey: 'sk_xxx', publishableKey: 'pk_xxx', googlePayMerchantId: 'BCR2DN4T...' });
|
|
324
|
+
await presentOnboarding({ accountId: 'acct_1', capabilities: ['kyc'] });
|
|
325
|
+
expect(mockPresentOnboarding).toHaveBeenCalledWith('acct_1', ['kyc']);
|
|
340
326
|
});
|
|
341
327
|
});
|
package/src/native.ts
CHANGED
|
@@ -13,20 +13,6 @@ import type {
|
|
|
13
13
|
} from './types';
|
|
14
14
|
import { ErrorCodes } from './errors';
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Throw a coded error from synchronous JS validation. Mirrors the `code`/`message`
|
|
18
|
-
* shape that native rejections produce so consumers can catch `e.code === 'INVALID_*'`
|
|
19
|
-
* uniformly.
|
|
20
|
-
*/
|
|
21
|
-
function throwCoded(code: string, message: string): never {
|
|
22
|
-
const err = new Error(message) as Error & { code: string };
|
|
23
|
-
err.code = code;
|
|
24
|
-
throw err;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// theme is iOS-only today: frame-android does not yet have a matching theme API,
|
|
28
|
-
// so the field is accepted on both platforms but ignored on Android until it does.
|
|
29
|
-
|
|
30
16
|
const LINKING_ERROR =
|
|
31
17
|
`The package 'framepayments-react-native' doesn't seem to be linked. Make sure you have run 'pod install' (iOS) or rebuilt the app (Android).`;
|
|
32
18
|
|
|
@@ -41,12 +27,38 @@ const FrameSDK = NativeModules.FrameSDK
|
|
|
41
27
|
}
|
|
42
28
|
);
|
|
43
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Throw a coded error from synchronous JS validation. Mirrors the `code`/`message`
|
|
32
|
+
* shape that native rejections produce so consumers can catch `e.code === 'INVALID_*'`
|
|
33
|
+
* uniformly.
|
|
34
|
+
*/
|
|
35
|
+
function throwCoded(code: string, message: string): never {
|
|
36
|
+
const err = new Error(message) as Error & { code: string };
|
|
37
|
+
err.code = code;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// theme is iOS-only today: frame-android does not yet have a matching theme API,
|
|
42
|
+
// so the field is accepted on both platforms but ignored on Android until it does.
|
|
43
|
+
|
|
44
44
|
let isInitialized = false;
|
|
45
45
|
|
|
46
46
|
export function initialize(options: {
|
|
47
47
|
secretKey: string;
|
|
48
48
|
publishableKey: string;
|
|
49
49
|
debugMode?: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Apple Pay merchant ID configured in your Apple Developer account. Applied to every
|
|
52
|
+
* Apple Pay surface (presentApplePay, the bundled checkout's wallet row, the
|
|
53
|
+
* onboarding wallet attach button). iOS-only — ignored on Android.
|
|
54
|
+
*/
|
|
55
|
+
applePayMerchantId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Google Pay merchant ID from the Google Pay & Wallet Console. Applied to every
|
|
58
|
+
* Google Pay surface (presentGooglePay, the bundled checkout's wallet row, the
|
|
59
|
+
* onboarding wallet attach button). Android-only — ignored on iOS.
|
|
60
|
+
*/
|
|
61
|
+
googlePayMerchantId?: string;
|
|
50
62
|
/**
|
|
51
63
|
* Optional theme applied SDK-wide to Frame's reusable iOS components
|
|
52
64
|
* (checkout, cart, onboarding). Pass any subset — unspecified tokens fall
|
|
@@ -69,6 +81,8 @@ export function initialize(options: {
|
|
|
69
81
|
options.secretKey,
|
|
70
82
|
options.publishableKey,
|
|
71
83
|
options.debugMode ?? false,
|
|
84
|
+
options.applePayMerchantId ?? null,
|
|
85
|
+
options.googlePayMerchantId ?? null,
|
|
72
86
|
options.theme ?? null
|
|
73
87
|
)
|
|
74
88
|
).then(() => {
|
|
@@ -147,24 +161,12 @@ export function presentCart(options: {
|
|
|
147
161
|
export function presentOnboarding(options: {
|
|
148
162
|
accountId?: string | null;
|
|
149
163
|
capabilities?: OnboardingCapability[];
|
|
150
|
-
applePayMerchantId?: string | null;
|
|
151
|
-
googlePayMerchantId?: string | null;
|
|
152
164
|
}): Promise<OnboardingResult> {
|
|
153
165
|
guardInitialized();
|
|
154
|
-
if (Platform.OS === 'ios') {
|
|
155
|
-
return wrapPromise(
|
|
156
|
-
FrameSDK.presentOnboarding(
|
|
157
|
-
options.accountId ?? null,
|
|
158
|
-
options.capabilities ?? [],
|
|
159
|
-
options.applePayMerchantId ?? null
|
|
160
|
-
)
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
166
|
return wrapPromise(
|
|
164
167
|
FrameSDK.presentOnboarding(
|
|
165
168
|
options.accountId ?? null,
|
|
166
|
-
options.capabilities ?? []
|
|
167
|
-
options.googlePayMerchantId ?? null
|
|
169
|
+
options.capabilities ?? []
|
|
168
170
|
)
|
|
169
171
|
);
|
|
170
172
|
}
|
|
@@ -179,22 +181,21 @@ export function presentOnboarding(options: {
|
|
|
179
181
|
*/
|
|
180
182
|
export function presentApplePay(options: PresentApplePayOptions): Promise<string> {
|
|
181
183
|
guardInitialized();
|
|
184
|
+
if (Platform.OS !== 'ios') {
|
|
185
|
+
throwCoded('PLATFORM_UNSUPPORTED', 'Frame.presentApplePay is iOS-only; use presentGooglePay on Android.');
|
|
186
|
+
}
|
|
182
187
|
if (!options?.owner || (options.owner.type !== 'customer' && options.owner.type !== 'account')) {
|
|
183
188
|
throwCoded(ErrorCodes.INVALID_OWNER, 'Frame.presentApplePay requires owner: { type: "customer" | "account", id: string }');
|
|
184
189
|
}
|
|
185
190
|
if (!options.owner.id) {
|
|
186
191
|
throwCoded(ErrorCodes.INVALID_OWNER, 'Frame.presentApplePay requires owner.id');
|
|
187
192
|
}
|
|
188
|
-
if (!options.merchantId) {
|
|
189
|
-
throwCoded(ErrorCodes.INVALID_MERCHANT_ID, 'Frame.presentApplePay requires merchantId');
|
|
190
|
-
}
|
|
191
193
|
return wrapPromise(
|
|
192
194
|
FrameSDK.presentApplePay(
|
|
193
195
|
options.owner.type,
|
|
194
196
|
options.owner.id,
|
|
195
197
|
options.amount,
|
|
196
|
-
options.currency ?? 'usd'
|
|
197
|
-
options.merchantId
|
|
198
|
+
options.currency ?? 'usd'
|
|
198
199
|
)
|
|
199
200
|
);
|
|
200
201
|
}
|
|
@@ -208,6 +209,9 @@ export function presentApplePay(options: PresentApplePayOptions): Promise<string
|
|
|
208
209
|
*/
|
|
209
210
|
export function presentGooglePay(options: PresentGooglePayOptions): Promise<string> {
|
|
210
211
|
guardInitialized();
|
|
212
|
+
if (Platform.OS !== 'android') {
|
|
213
|
+
throwCoded('PLATFORM_UNSUPPORTED', 'Frame.presentGooglePay is Android-only; use presentApplePay on iOS.');
|
|
214
|
+
}
|
|
211
215
|
if (!options?.owner || (options.owner.type !== 'customer' && options.owner.type !== 'account')) {
|
|
212
216
|
throwCoded(ErrorCodes.INVALID_OWNER, 'Frame.presentGooglePay requires owner: { type: "customer" | "account", id: string }');
|
|
213
217
|
}
|
|
@@ -219,8 +223,7 @@ export function presentGooglePay(options: PresentGooglePayOptions): Promise<stri
|
|
|
219
223
|
options.amountCents,
|
|
220
224
|
options.owner.type,
|
|
221
225
|
options.owner.id,
|
|
222
|
-
options.currencyCode ?? 'USD'
|
|
223
|
-
options.googlePayMerchantId ?? null
|
|
226
|
+
options.currencyCode ?? 'USD'
|
|
224
227
|
)
|
|
225
228
|
);
|
|
226
229
|
}
|
package/src/types.ts
CHANGED
|
@@ -100,7 +100,12 @@ export type WalletOwner =
|
|
|
100
100
|
/** @deprecated Use {@link WalletOwner}. Retained as an alias for source compatibility. */
|
|
101
101
|
export type ApplePayOwner = WalletOwner;
|
|
102
102
|
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Options for Frame.presentApplePay.
|
|
105
|
+
*
|
|
106
|
+
* The Apple Pay merchant ID is set once at `Frame.initialize({ applePayMerchantId })`. There is
|
|
107
|
+
* no per-call merchant ID parameter — the SDK reads it from the singleton at present time.
|
|
108
|
+
*/
|
|
104
109
|
export interface PresentApplePayOptions {
|
|
105
110
|
/** Payment amount in cents. */
|
|
106
111
|
amount: number;
|
|
@@ -108,11 +113,14 @@ export interface PresentApplePayOptions {
|
|
|
108
113
|
currency?: string;
|
|
109
114
|
/** Customer or account that owns the resulting payment method and charge. */
|
|
110
115
|
owner: WalletOwner;
|
|
111
|
-
/** Apple Pay merchant ID configured in your Apple Developer account. */
|
|
112
|
-
merchantId: string;
|
|
113
116
|
}
|
|
114
117
|
|
|
115
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Options for Frame.presentGooglePay.
|
|
120
|
+
*
|
|
121
|
+
* The Google Pay merchant ID is set once at `Frame.initialize({ googlePayMerchantId })`. There
|
|
122
|
+
* is no per-call merchant ID parameter — the SDK reads it from the singleton at present time.
|
|
123
|
+
*/
|
|
116
124
|
export interface PresentGooglePayOptions {
|
|
117
125
|
/** Payment amount in cents. */
|
|
118
126
|
amountCents: number;
|
|
@@ -120,8 +128,6 @@ export interface PresentGooglePayOptions {
|
|
|
120
128
|
owner: WalletOwner;
|
|
121
129
|
/** ISO 4217 currency code. Defaults to 'USD'. */
|
|
122
130
|
currencyCode?: string;
|
|
123
|
-
/** Optional override for the Google Pay merchant ID. */
|
|
124
|
-
googlePayMerchantId?: string;
|
|
125
131
|
}
|
|
126
132
|
|
|
127
133
|
/**
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
|
|
3
|
-
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
-
|
|
5
|
-
Pod::Spec.new do |s|
|
|
6
|
-
s.name = 'FrameReactNative'
|
|
7
|
-
s.version = package['version']
|
|
8
|
-
s.summary = package['description']
|
|
9
|
-
s.homepage = package['homepage']
|
|
10
|
-
s.license = package['license']
|
|
11
|
-
s.authors = package['author']
|
|
12
|
-
s.platforms = { :ios => '17.0' }
|
|
13
|
-
s.source = { :git => package['repository']['url'], :tag => "#{s.version}" }
|
|
14
|
-
s.source_files = '**/*.{h,m,mm,swift}'
|
|
15
|
-
s.requires_arc = true
|
|
16
|
-
|
|
17
|
-
s.dependency 'React-Core'
|
|
18
|
-
|
|
19
|
-
# Frame-iOS SDK (Frame + FrameOnboarding) must be available to the app.
|
|
20
|
-
#
|
|
21
|
-
# SPM users (RN >= 0.73): add framepayments-react-native via Swift Package Manager.
|
|
22
|
-
# Frame and FrameOnboarding are declared as dependencies in Package.swift and resolve
|
|
23
|
-
# automatically — no manual step required.
|
|
24
|
-
#
|
|
25
|
-
# CocoaPods users: add frame-ios manually via Xcode:
|
|
26
|
-
# File → Add Package Dependencies → https://github.com/Frame-Payments/frame-ios
|
|
27
|
-
# Add both the "Frame-iOS" and "Frame-Onboarding" products to your app target.
|
|
28
|
-
# If Frame-iOS is published as a pod in the future, replace the above with:
|
|
29
|
-
# s.dependency 'Frame-iOS'
|
|
30
|
-
|
|
31
|
-
install_modules_dependencies(s) if respond_to?(:install_modules_dependencies)
|
|
32
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for the native module bridge (initialize, presentCheckout, presentCart).
|
|
3
|
-
* NativeModules.FrameSDK is mocked.
|
|
4
|
-
*/
|
|
5
|
-
declare const mockInitialize: jest.Mock<Promise<void>, [_apiKey: string, _debugMode: boolean], any>;
|
|
6
|
-
declare const mockPresentCheckout: jest.Mock<Promise<{
|
|
7
|
-
id: string;
|
|
8
|
-
amount: number;
|
|
9
|
-
}>, [_customerId: unknown, _amount: number], any>;
|
|
10
|
-
declare const mockPresentCart: jest.Mock<Promise<{
|
|
11
|
-
id: string;
|
|
12
|
-
amount: number;
|
|
13
|
-
}>, [_customerId: unknown, _items: unknown[], _shipping: number], any>;
|
|
14
|
-
declare let initialize: (opts: {
|
|
15
|
-
apiKey: string;
|
|
16
|
-
debugMode?: boolean;
|
|
17
|
-
}) => void;
|
|
18
|
-
declare let presentCheckout: (opts: {
|
|
19
|
-
customerId?: string | null;
|
|
20
|
-
amount: number;
|
|
21
|
-
}) => Promise<unknown>;
|
|
22
|
-
declare let presentCart: (opts: {
|
|
23
|
-
customerId?: string | null;
|
|
24
|
-
items: Array<{
|
|
25
|
-
id: string;
|
|
26
|
-
title: string;
|
|
27
|
-
amountInCents: number;
|
|
28
|
-
imageUrl: string;
|
|
29
|
-
}>;
|
|
30
|
-
shippingAmountInCents: number;
|
|
31
|
-
}) => Promise<unknown>;
|
|
32
|
-
//# sourceMappingURL=native.test.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"native.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/native.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,QAAA,MAAM,cAAc,uEAAuE,CAAC;AAC5F,QAAA,MAAM,mBAAmB;;;iDAAqG,CAAC;AAC/H,QAAA,MAAM,eAAe;;;sEAA0H,CAAC;AAahJ,QAAA,IAAI,UAAU,EAAE,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,CAAC;AACxE,QAAA,IAAI,eAAe,EAAE,CAAC,IAAI,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAChG,QAAA,IAAI,WAAW,EAAE,CAAC,IAAI,EAAE;IACtB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACrF,qBAAqB,EAAE,MAAM,CAAC;CAC/B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC"}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the native module bridge (initialize, presentCheckout, presentCart).
|
|
4
|
-
* NativeModules.FrameSDK is mocked.
|
|
5
|
-
*/
|
|
6
|
-
const mockInitialize = jest.fn((_apiKey, _debugMode) => Promise.resolve());
|
|
7
|
-
const mockPresentCheckout = jest.fn((_customerId, _amount) => Promise.resolve({ id: 'ci_1', amount: 10000 }));
|
|
8
|
-
const mockPresentCart = jest.fn((_customerId, _items, _shipping) => Promise.resolve({ id: 'ci_2', amount: 15000 }));
|
|
9
|
-
jest.mock('react-native', () => ({
|
|
10
|
-
NativeModules: {
|
|
11
|
-
FrameSDK: {
|
|
12
|
-
initialize: mockInitialize,
|
|
13
|
-
presentCheckout: mockPresentCheckout,
|
|
14
|
-
presentCart: mockPresentCart,
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
// Re-import after mock so we get the mocked NativeModules
|
|
19
|
-
let initialize;
|
|
20
|
-
let presentCheckout;
|
|
21
|
-
let presentCart;
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
jest.resetModules();
|
|
24
|
-
mockInitialize.mockClear();
|
|
25
|
-
mockPresentCheckout.mockClear();
|
|
26
|
-
mockPresentCart.mockClear();
|
|
27
|
-
const native = require('../native');
|
|
28
|
-
initialize = native.initialize;
|
|
29
|
-
presentCheckout = native.presentCheckout;
|
|
30
|
-
presentCart = native.presentCart;
|
|
31
|
-
});
|
|
32
|
-
describe('initialize', () => {
|
|
33
|
-
it('calls native FrameSDK.initialize with apiKey and debugMode', () => {
|
|
34
|
-
initialize({ apiKey: 'sk_test_xxx', debugMode: true });
|
|
35
|
-
expect(mockInitialize).toHaveBeenCalledTimes(1);
|
|
36
|
-
expect(mockInitialize).toHaveBeenCalledWith('sk_test_xxx', true);
|
|
37
|
-
});
|
|
38
|
-
it('defaults debugMode to false', () => {
|
|
39
|
-
initialize({ apiKey: 'sk_test_yyy' });
|
|
40
|
-
expect(mockInitialize).toHaveBeenCalledWith('sk_test_yyy', false);
|
|
41
|
-
});
|
|
42
|
-
it('throws if apiKey is missing', () => {
|
|
43
|
-
expect(() => initialize({ apiKey: '' })).toThrow();
|
|
44
|
-
expect(() => initialize({})).toThrow();
|
|
45
|
-
expect(mockInitialize).not.toHaveBeenCalled();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('presentCheckout', () => {
|
|
49
|
-
it('throws NOT_INITIALIZED if initialize was not called', async () => {
|
|
50
|
-
try {
|
|
51
|
-
await presentCheckout({ amount: 10000 });
|
|
52
|
-
expect(true).toBe(false);
|
|
53
|
-
}
|
|
54
|
-
catch (e) {
|
|
55
|
-
expect(e.code).toBe('NOT_INITIALIZED');
|
|
56
|
-
expect(e.message).toContain('initialized');
|
|
57
|
-
}
|
|
58
|
-
expect(mockPresentCheckout).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
it('calls native presentCheckout with customerId and amount after initialize', async () => {
|
|
61
|
-
initialize({ apiKey: 'sk_xxx' });
|
|
62
|
-
const result = await presentCheckout({ customerId: 'cus_1', amount: 10000 });
|
|
63
|
-
expect(mockPresentCheckout).toHaveBeenCalledWith('cus_1', 10000);
|
|
64
|
-
expect(result).toEqual({ id: 'ci_1', amount: 10000 });
|
|
65
|
-
});
|
|
66
|
-
it('passes null for customerId when not provided', async () => {
|
|
67
|
-
initialize({ apiKey: 'sk_xxx' });
|
|
68
|
-
await presentCheckout({ amount: 5000 });
|
|
69
|
-
expect(mockPresentCheckout).toHaveBeenCalledWith(null, 5000);
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe('presentCart', () => {
|
|
73
|
-
const items = [
|
|
74
|
-
{ id: '1', title: 'Item A', amountInCents: 1000, imageUrl: 'https://example.com/a.jpg' },
|
|
75
|
-
];
|
|
76
|
-
it('throws NOT_INITIALIZED if initialize was not called', async () => {
|
|
77
|
-
try {
|
|
78
|
-
await presentCart({ items, shippingAmountInCents: 500 });
|
|
79
|
-
expect(true).toBe(false);
|
|
80
|
-
}
|
|
81
|
-
catch (e) {
|
|
82
|
-
expect(e.code).toBe('NOT_INITIALIZED');
|
|
83
|
-
}
|
|
84
|
-
expect(mockPresentCart).not.toHaveBeenCalled();
|
|
85
|
-
});
|
|
86
|
-
it('calls native presentCart with customerId, items, shipping after initialize', async () => {
|
|
87
|
-
initialize({ apiKey: 'sk_xxx' });
|
|
88
|
-
const result = await presentCart({
|
|
89
|
-
customerId: 'cus_2',
|
|
90
|
-
items,
|
|
91
|
-
shippingAmountInCents: 500,
|
|
92
|
-
});
|
|
93
|
-
expect(mockPresentCart).toHaveBeenCalledWith('cus_2', items, 500);
|
|
94
|
-
expect(result).toEqual({ id: 'ci_2', amount: 15000 });
|
|
95
|
-
});
|
|
96
|
-
it('passes null for customerId when not provided', async () => {
|
|
97
|
-
initialize({ apiKey: 'sk_xxx' });
|
|
98
|
-
await presentCart({ items, shippingAmountInCents: 0 });
|
|
99
|
-
expect(mockPresentCart).toHaveBeenCalledWith(null, items, 0);
|
|
100
|
-
});
|
|
101
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <FrameApplePayButton /> — iOS-only React Native view that wraps Frame iOS SDK's
|
|
3
|
-
* native FrameApplePayButton (PassKit + automatic device attestation).
|
|
4
|
-
*
|
|
5
|
-
* On non-iOS platforms this component renders nothing, so consumers can place it
|
|
6
|
-
* unconditionally in their JSX.
|
|
7
|
-
*/
|
|
8
|
-
import * as React from 'react';
|
|
9
|
-
import { type NativeSyntheticEvent, type ViewProps } from 'react-native';
|
|
10
|
-
import type { ApplePayButtonStyle, ApplePayButtonType, ApplePayOwner, FrameApplePayResultEvent } from '../types';
|
|
11
|
-
export interface FrameApplePayButtonProps extends ViewProps {
|
|
12
|
-
/** Payment amount in cents. */
|
|
13
|
-
amount: number;
|
|
14
|
-
/** ISO 4217 currency code. Defaults to 'usd'. */
|
|
15
|
-
currency?: string;
|
|
16
|
-
/** Customer or account that owns the resulting payment method. */
|
|
17
|
-
owner: ApplePayOwner;
|
|
18
|
-
/** Apple Pay merchant ID configured in your Apple Developer account. */
|
|
19
|
-
merchantId: string;
|
|
20
|
-
/** When true, the button renders a "Or" divider beneath itself for inline checkout layouts. */
|
|
21
|
-
addCheckoutDivider?: boolean;
|
|
22
|
-
/** PassKit button type. Defaults to 'buy'. */
|
|
23
|
-
buttonType?: ApplePayButtonType;
|
|
24
|
-
/** PassKit button style. Defaults to 'black'. */
|
|
25
|
-
buttonStyle?: ApplePayButtonStyle;
|
|
26
|
-
/** Fired when payment completes, fails, or the button reports an error. */
|
|
27
|
-
onResult: (event: NativeSyntheticEvent<FrameApplePayResultEvent>) => void;
|
|
28
|
-
}
|
|
29
|
-
export declare const FrameApplePayButton: React.FC<FrameApplePayButtonProps>;
|
|
30
|
-
//# sourceMappingURL=FrameApplePayButton.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FrameApplePayButton.d.ts","sourceRoot":"","sources":["../../src/components/FrameApplePayButton.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EAClB,aAAa,EACb,wBAAwB,EACzB,MAAM,UAAU,CAAC;AAElB,MAAM,WAAW,wBAAyB,SAAQ,SAAS;IACzD,+BAA+B;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,KAAK,EAAE,aAAa,CAAC;IACrB,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB,+FAA+F;IAC/F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8CAA8C;IAC9C,UAAU,CAAC,EAAE,kBAAkB,CAAC;IAChC,iDAAiD;IACjD,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,2EAA2E;IAC3E,QAAQ,EAAE,CAAC,KAAK,EAAE,oBAAoB,CAAC,wBAAwB,CAAC,KAAK,IAAI,CAAC;CAC3E;AA+BD,eAAO,MAAM,mBAAmB,EAAE,KAAK,CAAC,EAAE,CAAC,wBAAwB,CAclE,CAAC"}
|