flintn-checkout 0.0.16 → 0.0.18
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 +59 -9
- package/dist/index.d.mts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +151 -40
- package/dist/index.mjs +151 -40
- package/dist/react.d.mts +16 -2
- package/dist/react.d.ts +16 -2
- package/dist/react.js +167 -41
- package/dist/react.mjs +167 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ For UI events (LIST and RADIO variants), `view` is the view that became active:
|
|
|
166
166
|
|
|
167
167
|
```
|
|
168
168
|
onEvent → { event: 'INITIATED', method: 'PAYMENT_CARD' }
|
|
169
|
-
onEvent → { event: 'SOFT_DECLINE', method: 'PAYMENT_CARD', code: '
|
|
169
|
+
onEvent → { event: 'SOFT_DECLINE', method: 'PAYMENT_CARD', code: 'FNT-EC-2003', message: 'Insufficient funds.' }
|
|
170
170
|
// buyer retries with another card
|
|
171
171
|
onEvent → { event: 'INITIATED', method: 'PAYMENT_CARD' }
|
|
172
172
|
onEvent → { event: 'AUTHORIZED', method: 'PAYMENT_CARD', paymentId: 'pay_...' }
|
|
@@ -270,7 +270,7 @@ When a card payment requires 3DS, the SDK shows the bank's challenge in a
|
|
|
270
270
|
`THREE_DS_CANCELLED` payment error. While the challenge is open, the
|
|
271
271
|
`submit()` promise stays pending with a 10-minute timeout.
|
|
272
272
|
|
|
273
|
-
> **Note:**
|
|
273
|
+
> **Note:** Hosted Fields emit the same per-attempt analytics as Iframe Checkout — the card flow and the express buttons both report through the top-level `onEvent` callback (`method: 'PAYMENT_CARD'` for the card form), while terminal results arrive via `onPayment`. See "Events — terminal vs per-attempt" above; UI events (`FORM_ENTERED`/`FORM_EXITED`) do not apply since your page owns the layout.
|
|
274
274
|
|
|
275
275
|
**React**
|
|
276
276
|
|
|
@@ -331,7 +331,11 @@ function CheckoutForm() {
|
|
|
331
331
|
</div>
|
|
332
332
|
<div style={{ flex: 1 }}>
|
|
333
333
|
<label>CVV</label>
|
|
334
|
-
|
|
334
|
+
{/* position/zIndex let the CVV helper panel overlay content below */}
|
|
335
|
+
<div
|
|
336
|
+
ref={cvvRef}
|
|
337
|
+
style={{ height: 40, marginBottom: 4, position: 'relative', zIndex: 1 }}
|
|
338
|
+
/>
|
|
335
339
|
{fieldErrors['cvv'] && (
|
|
336
340
|
<span style={{ color: 'red' }}>{fieldErrors['cvv']}</span>
|
|
337
341
|
)}
|
|
@@ -379,7 +383,7 @@ const fields = createFlintNFields({
|
|
|
379
383
|
|
|
380
384
|
const cardNumber = fields.createField('card-number', { placeholder: '4111 1111 1111 1111' });
|
|
381
385
|
const expiry = fields.createField('expiry', { placeholder: 'MM/YY' });
|
|
382
|
-
const cvv = fields.createField('cvv'
|
|
386
|
+
const cvv = fields.createField('cvv');
|
|
383
387
|
|
|
384
388
|
cardNumber.mount('#card-number');
|
|
385
389
|
expiry.mount('#expiry');
|
|
@@ -405,6 +409,8 @@ if (validation.isValid) {
|
|
|
405
409
|
| `onChange` | `(event: FieldChangeEvent) => void` | No | — | Field value changed |
|
|
406
410
|
| `onFocus` | `(fieldType: TFieldType) => void` | No | — | Field gained focus |
|
|
407
411
|
| `onBlur` | `(fieldType: TFieldType, state: FieldState) => void` | No | — | Field lost focus |
|
|
412
|
+
| `onEvent` | `(event: AttemptEvent) => void` | No | — | Per-attempt analytics for the card flow (`method: 'PAYMENT_CARD'`) |
|
|
413
|
+
| `onPostalCodeRequirement` | `(required: boolean) => void` | No | — | Entered card BIN requires a postal code — see "Postal code (BIN-driven)" |
|
|
408
414
|
| `onError` | `(error: PaymentError) => void` | No | — | SDK initialization error |
|
|
409
415
|
| `debug` | `boolean` | No | `false` | Enable console debug logs |
|
|
410
416
|
|
|
@@ -429,13 +435,13 @@ Additional React-only options:
|
|
|
429
435
|
| `onChange` | `(event: FieldChangeEvent) => void` | Optional additional callback |
|
|
430
436
|
| `onFocus` | `(fieldType: TFieldType) => void` | Optional additional callback |
|
|
431
437
|
| `onBlur` | `(fieldType: TFieldType, state: FieldState) => void` | Optional additional callback |
|
|
432
|
-
| `onEvent` | `(event:
|
|
438
|
+
| `onEvent` | `(event: AttemptEvent) => void` | Per-attempt analytics — card flow (`method: 'PAYMENT_CARD'`) and express buttons share this stream |
|
|
433
439
|
|
|
434
440
|
### FieldOptions
|
|
435
441
|
|
|
436
442
|
| Property | Type | Default | Description |
|
|
437
443
|
|----------|------|---------|-------------|
|
|
438
|
-
| `placeholder` | `string` |
|
|
444
|
+
| `placeholder` | `string` | `'1234 1234 1234 1234'` / `'MM/YY'` / `'***'` | Input placeholder text |
|
|
439
445
|
| `disabled` | `boolean` | `false` | Disable the input |
|
|
440
446
|
|
|
441
447
|
### React Hook Return Values
|
|
@@ -453,6 +459,7 @@ Additional React-only options:
|
|
|
453
459
|
| `fieldErrors` | `Partial<Record<TFieldType, string \| null>>` | Per-field validation errors (populated after first submit) |
|
|
454
460
|
| `fieldStates` | `Partial<Record<TFieldType, FieldState>>` | Per-field state (always up to date) |
|
|
455
461
|
| `cardBrand` | `string \| null` | Detected card brand (e.g. `"visa"`, `"mastercard"`) |
|
|
462
|
+
| `postalCodeRequired` | `boolean` | The entered card BIN requires a postal code — see "Postal code (BIN-driven)" |
|
|
456
463
|
| `paymentResult` | `PaymentResult \| null` | Result after payment attempt |
|
|
457
464
|
| `error` | `PaymentError \| null` | SDK error if any |
|
|
458
465
|
| `validate` | `() => Promise<FieldsValidationResult>` | Validate all fields |
|
|
@@ -469,6 +476,29 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
|
|
|
469
476
|
- After first submit: errors update on every field change (revalidation)
|
|
470
477
|
- Card number changes trigger CVV revalidation (CVV length depends on card brand)
|
|
471
478
|
- All three fields (card number, expiry, CVV) are required — submitting with a missing field returns a `MISSING_FIELDS` error
|
|
479
|
+
- The CVV field ships a built-in helper: tapping the card icon expands a “where to find CVV” panel below the input. The field iframe grows to fit it, so keep the CVV container at a fixed `height` with `position: relative; z-index: 1` — the open panel then overlays the content below (like a popover) instead of pushing your layout
|
|
480
|
+
|
|
481
|
+
### Postal code (BIN-driven)
|
|
482
|
+
|
|
483
|
+
Some issuers require a postal code for online payments. Once the buyer has
|
|
484
|
+
typed enough digits, the card-number field checks the BIN against the payments
|
|
485
|
+
API and reports whether a postal code is required — `postalCodeRequired`
|
|
486
|
+
(React) / `onPostalCodeRequirement` (vanilla). Show your own postal code input
|
|
487
|
+
when it flips to `true` and pass the value at submit:
|
|
488
|
+
|
|
489
|
+
```tsx
|
|
490
|
+
const { postalCodeRequired, submit } = useFlintNFields({ ... });
|
|
491
|
+
|
|
492
|
+
// render your postal input when postalCodeRequired === true, then:
|
|
493
|
+
await submit({
|
|
494
|
+
cardholderName,
|
|
495
|
+
billingAddress: { postalCode },
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
If the BIN requires a postal code and `billingAddress.postalCode` is missing,
|
|
500
|
+
`submit()` resolves with `PAYMENT_ERROR` / `POSTAL_CODE_REQUIRED` without
|
|
501
|
+
calling the payments API.
|
|
472
502
|
|
|
473
503
|
### Field Types
|
|
474
504
|
|
|
@@ -476,7 +506,7 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
|
|
|
476
506
|
|-------|-------------|
|
|
477
507
|
| `'card-number'` | Card number input with automatic formatting and brand detection |
|
|
478
508
|
| `'expiry'` | Expiry date input (MM/YY format) |
|
|
479
|
-
| `'cvv'` | CVV/CVC input (3 or 4 digits depending on card brand) |
|
|
509
|
+
| `'cvv'` | CVV/CVC input (3 or 4 digits depending on card brand). Includes a built-in “where to find CVV” helper: tapping the card icon expands an explainer panel below the input |
|
|
480
510
|
|
|
481
511
|
### Individual Field Methods (Vanilla JS)
|
|
482
512
|
|
|
@@ -612,6 +642,23 @@ interface PaymentError {
|
|
|
612
642
|
}
|
|
613
643
|
```
|
|
614
644
|
|
|
645
|
+
### SubmitOptions (Hosted Fields)
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
interface SubmitOptions {
|
|
649
|
+
cardholderName?: string;
|
|
650
|
+
email?: string;
|
|
651
|
+
billingAddress?: {
|
|
652
|
+
addressLine1?: string;
|
|
653
|
+
addressLine2?: string;
|
|
654
|
+
city?: string;
|
|
655
|
+
state?: string;
|
|
656
|
+
postalCode?: string; // required when the BIN demands it — see "Postal code (BIN-driven)"
|
|
657
|
+
country?: string;
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
```
|
|
661
|
+
|
|
615
662
|
### Error handling
|
|
616
663
|
|
|
617
664
|
Errors surface through three separate channels — don't conflate them:
|
|
@@ -632,8 +679,11 @@ Common `code` values:
|
|
|
632
679
|
| `CONFIG_ERROR` | error | Session config failed to load and the API gave no code |
|
|
633
680
|
| `NETWORK_ERROR` / `HTTP_<status>` / `INVALID_RESPONSE` | error or payment | Network failure / HTTP error without an API code / malformed response |
|
|
634
681
|
| `VALIDATION_ERROR` | payment | `submit()` blocked by field validation (details in `fieldErrors`) |
|
|
635
|
-
| `
|
|
636
|
-
| `
|
|
682
|
+
| `POSTAL_CODE_REQUIRED` | payment | The card BIN requires a postal code and `billingAddress.postalCode` was not passed to `submit()` |
|
|
683
|
+
| `MISSING_FIELDS` / `NOT_INITIALIZED` / `SUBMIT_IN_PROGRESS` / `SUBMIT_TIMEOUT` | payment | Card `submit()` preconditions / duplicate call / no response in time (30s) |
|
|
684
|
+
| `THREE_DS_TIMEOUT` | payment | A 3DS challenge stayed unresolved for 10 minutes |
|
|
685
|
+
| `FNT-EC-*` decline codes | payment | Declined — the FNT-EC decline code identifies the reason (e.g. `FNT-EC-2003` insufficient funds). Sent for both immediate and post-3DS declines |
|
|
686
|
+
| `PAYMENT_DECLINED` | payment | Declined without a specific decline code (fallback) |
|
|
637
687
|
| `PAYMENT_FAILED` | payment | Non-declined failure (including unexpected 3DS outcomes) |
|
|
638
688
|
| `APPLE_PAY_SDK_ERROR` / `GOOGLE_PAY_SDK_ERROR` / `PAYPAL_ERROR` | payment | The wallet SDK failed mid-flow |
|
|
639
689
|
| `PAYPAL_MISSING_PAYMENT_ID` | payment | PayPal approved but no payment id was captured (anomaly) |
|
package/dist/index.d.mts
CHANGED
|
@@ -173,6 +173,8 @@ declare const FieldEventType: {
|
|
|
173
173
|
readonly FIELD_HEIGHT: "FIELD_HEIGHT";
|
|
174
174
|
readonly FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED";
|
|
175
175
|
readonly FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT";
|
|
176
|
+
readonly FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT";
|
|
177
|
+
readonly FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT";
|
|
176
178
|
};
|
|
177
179
|
type TFieldEventType = (typeof FieldEventType)[keyof typeof FieldEventType];
|
|
178
180
|
interface FieldOptions {
|
|
@@ -220,7 +222,7 @@ interface ExpressCapability {
|
|
|
220
222
|
googlePay: boolean;
|
|
221
223
|
paypal: boolean;
|
|
222
224
|
}
|
|
223
|
-
interface
|
|
225
|
+
interface AttemptEvent {
|
|
224
226
|
event: TPaymentAttemptResult;
|
|
225
227
|
method: TPaymentMethod;
|
|
226
228
|
paymentId?: string;
|
|
@@ -240,6 +242,15 @@ interface FlintNFieldsOptions {
|
|
|
240
242
|
onChange?: (event: FieldChangeEvent) => void;
|
|
241
243
|
onFocus?: (fieldType: TFieldType) => void;
|
|
242
244
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
245
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
246
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
247
|
+
/**
|
|
248
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
249
|
+
* your own postal field and pass its value via submit's
|
|
250
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
251
|
+
* otherwise.
|
|
252
|
+
*/
|
|
253
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
243
254
|
onError?: (error: PaymentError) => void;
|
|
244
255
|
debug?: boolean;
|
|
245
256
|
}
|
|
@@ -293,7 +304,7 @@ interface FlintNExpressButtonsOptions {
|
|
|
293
304
|
apiUrl?: string;
|
|
294
305
|
buttonBorderRadius?: number;
|
|
295
306
|
onPayment?: (result: PaymentResult) => void;
|
|
296
|
-
onEvent?: (event:
|
|
307
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
297
308
|
onCapability?: (capability: ExpressCapability) => void;
|
|
298
309
|
onError?: (error: PaymentError) => void;
|
|
299
310
|
debug?: boolean;
|
|
@@ -349,8 +360,11 @@ interface AuthorizeResponse {
|
|
|
349
360
|
};
|
|
350
361
|
status_reason?: {
|
|
351
362
|
decline_reason?: string;
|
|
363
|
+
decline_code?: string;
|
|
364
|
+
decline_type?: 'SOFT_DECLINE' | 'HARD_DECLINE';
|
|
352
365
|
message?: string;
|
|
353
366
|
};
|
|
367
|
+
session_retryable?: boolean;
|
|
354
368
|
}
|
|
355
369
|
interface ApplePayToken {
|
|
356
370
|
payment_data: {
|
|
@@ -377,7 +391,9 @@ interface GooglePayToken {
|
|
|
377
391
|
interface WalletAuthorizeHandlers {
|
|
378
392
|
onAuthorized: (paymentId: string) => void;
|
|
379
393
|
onThreeDs: (redirectUrl: string, paymentId: string) => void;
|
|
380
|
-
onFailed: (code: string, message: string, paymentId?: string
|
|
394
|
+
onFailed: (code: string, message: string, paymentId?: string,
|
|
395
|
+
/** Attempt analytics classification; defaults to HARD_DECLINE. */
|
|
396
|
+
event?: TPaymentAttemptResult) => void;
|
|
381
397
|
}
|
|
382
398
|
|
|
383
399
|
/**
|
|
@@ -425,4 +441,4 @@ declare const validateConfig: (config: {
|
|
|
425
441
|
}) => void;
|
|
426
442
|
declare const sanitizeToken: (token: string) => string;
|
|
427
443
|
|
|
428
|
-
export { type ApplePayConfig, type ApplePayToken, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type
|
|
444
|
+
export { type ApplePayConfig, type ApplePayToken, type AttemptEvent, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type ExpressButtonMount, type ExpressButtonsOptions, type ExpressCapability, ExpressMethod, type FieldChangeEvent, FieldEventType, type FieldOptions, type FieldState, FieldType, type FieldValidationResult, type FieldsValidationResult, type FlintNConfig, type FlintNExpressButtons, type FlintNExpressButtonsOptions, type FlintNField, type FlintNFieldInternalCallbacks, type FlintNFields, type FlintNFieldsConfig, type FlintNFieldsOptions, type FlintNPayment, type FlintNPaymentOptions, type FormFields, type FormPlaceholders, type FormStyles, type GooglePayConfig, type GooglePayToken, type PayPalConfig, type PaymentAttempt, PaymentAttemptResult, type PaymentError, PaymentMethod, type PaymentResult, type PaymentUI, PaymentUIEvent, type SessionConfig, type SubmitBillingAddress, type SubmitOptions, type TAuthorizedStatus, type TCheckoutFormVariant, type TCheckoutView, type TEventType, type TExpressMethod, type TFieldEventType, type TFieldType, type TPaymentAttemptResult, type TPaymentMethod, type TPaymentUIEvent, type WalletAuthorizeHandlers, buildIframeSrc, createFlintNExpressButtons, createFlintNField, createFlintNFields, createFlintNPayment, createIframeElement, deriveApiUrl, isAttemptEvent, isUIEvent, parseOrigin, sanitizeToken, validateConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -173,6 +173,8 @@ declare const FieldEventType: {
|
|
|
173
173
|
readonly FIELD_HEIGHT: "FIELD_HEIGHT";
|
|
174
174
|
readonly FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED";
|
|
175
175
|
readonly FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT";
|
|
176
|
+
readonly FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT";
|
|
177
|
+
readonly FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT";
|
|
176
178
|
};
|
|
177
179
|
type TFieldEventType = (typeof FieldEventType)[keyof typeof FieldEventType];
|
|
178
180
|
interface FieldOptions {
|
|
@@ -220,7 +222,7 @@ interface ExpressCapability {
|
|
|
220
222
|
googlePay: boolean;
|
|
221
223
|
paypal: boolean;
|
|
222
224
|
}
|
|
223
|
-
interface
|
|
225
|
+
interface AttemptEvent {
|
|
224
226
|
event: TPaymentAttemptResult;
|
|
225
227
|
method: TPaymentMethod;
|
|
226
228
|
paymentId?: string;
|
|
@@ -240,6 +242,15 @@ interface FlintNFieldsOptions {
|
|
|
240
242
|
onChange?: (event: FieldChangeEvent) => void;
|
|
241
243
|
onFocus?: (fieldType: TFieldType) => void;
|
|
242
244
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
245
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
246
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
247
|
+
/**
|
|
248
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
249
|
+
* your own postal field and pass its value via submit's
|
|
250
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
251
|
+
* otherwise.
|
|
252
|
+
*/
|
|
253
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
243
254
|
onError?: (error: PaymentError) => void;
|
|
244
255
|
debug?: boolean;
|
|
245
256
|
}
|
|
@@ -293,7 +304,7 @@ interface FlintNExpressButtonsOptions {
|
|
|
293
304
|
apiUrl?: string;
|
|
294
305
|
buttonBorderRadius?: number;
|
|
295
306
|
onPayment?: (result: PaymentResult) => void;
|
|
296
|
-
onEvent?: (event:
|
|
307
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
297
308
|
onCapability?: (capability: ExpressCapability) => void;
|
|
298
309
|
onError?: (error: PaymentError) => void;
|
|
299
310
|
debug?: boolean;
|
|
@@ -349,8 +360,11 @@ interface AuthorizeResponse {
|
|
|
349
360
|
};
|
|
350
361
|
status_reason?: {
|
|
351
362
|
decline_reason?: string;
|
|
363
|
+
decline_code?: string;
|
|
364
|
+
decline_type?: 'SOFT_DECLINE' | 'HARD_DECLINE';
|
|
352
365
|
message?: string;
|
|
353
366
|
};
|
|
367
|
+
session_retryable?: boolean;
|
|
354
368
|
}
|
|
355
369
|
interface ApplePayToken {
|
|
356
370
|
payment_data: {
|
|
@@ -377,7 +391,9 @@ interface GooglePayToken {
|
|
|
377
391
|
interface WalletAuthorizeHandlers {
|
|
378
392
|
onAuthorized: (paymentId: string) => void;
|
|
379
393
|
onThreeDs: (redirectUrl: string, paymentId: string) => void;
|
|
380
|
-
onFailed: (code: string, message: string, paymentId?: string
|
|
394
|
+
onFailed: (code: string, message: string, paymentId?: string,
|
|
395
|
+
/** Attempt analytics classification; defaults to HARD_DECLINE. */
|
|
396
|
+
event?: TPaymentAttemptResult) => void;
|
|
381
397
|
}
|
|
382
398
|
|
|
383
399
|
/**
|
|
@@ -425,4 +441,4 @@ declare const validateConfig: (config: {
|
|
|
425
441
|
}) => void;
|
|
426
442
|
declare const sanitizeToken: (token: string) => string;
|
|
427
443
|
|
|
428
|
-
export { type ApplePayConfig, type ApplePayToken, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type
|
|
444
|
+
export { type ApplePayConfig, type ApplePayToken, type AttemptEvent, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type ExpressButtonMount, type ExpressButtonsOptions, type ExpressCapability, ExpressMethod, type FieldChangeEvent, FieldEventType, type FieldOptions, type FieldState, FieldType, type FieldValidationResult, type FieldsValidationResult, type FlintNConfig, type FlintNExpressButtons, type FlintNExpressButtonsOptions, type FlintNField, type FlintNFieldInternalCallbacks, type FlintNFields, type FlintNFieldsConfig, type FlintNFieldsOptions, type FlintNPayment, type FlintNPaymentOptions, type FormFields, type FormPlaceholders, type FormStyles, type GooglePayConfig, type GooglePayToken, type PayPalConfig, type PaymentAttempt, PaymentAttemptResult, type PaymentError, PaymentMethod, type PaymentResult, type PaymentUI, PaymentUIEvent, type SessionConfig, type SubmitBillingAddress, type SubmitOptions, type TAuthorizedStatus, type TCheckoutFormVariant, type TCheckoutView, type TEventType, type TExpressMethod, type TFieldEventType, type TFieldType, type TPaymentAttemptResult, type TPaymentMethod, type TPaymentUIEvent, type WalletAuthorizeHandlers, buildIframeSrc, createFlintNExpressButtons, createFlintNField, createFlintNFields, createFlintNPayment, createIframeElement, deriveApiUrl, isAttemptEvent, isUIEvent, parseOrigin, sanitizeToken, validateConfig };
|
package/dist/index.js
CHANGED
|
@@ -113,7 +113,14 @@ var FieldEventType = {
|
|
|
113
113
|
// modal (the field iframe is too small, and popups get blocked).
|
|
114
114
|
FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED",
|
|
115
115
|
// SDK reports the challenge outcome back into the card iframe.
|
|
116
|
-
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT"
|
|
116
|
+
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT",
|
|
117
|
+
// Card iframe reports attempt analytics (INITIATED / declines / …) which
|
|
118
|
+
// the SDK forwards to the merchant's onEvent — same stream the direct
|
|
119
|
+
// iframe checkout emits.
|
|
120
|
+
FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT",
|
|
121
|
+
// Card iframe reports whether the entered BIN requires a postal code so
|
|
122
|
+
// the merchant can show/require their own postal field.
|
|
123
|
+
FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT"
|
|
117
124
|
};
|
|
118
125
|
|
|
119
126
|
// src/utils.ts
|
|
@@ -584,14 +591,25 @@ var ExpressApi = class {
|
|
|
584
591
|
handlers.onThreeDs(res.required_action.redirect_url, res.id);
|
|
585
592
|
} else {
|
|
586
593
|
const code = declineCodeOf(
|
|
587
|
-
res.status_reason?.
|
|
594
|
+
res.status_reason?.decline_code,
|
|
588
595
|
res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
|
|
589
596
|
);
|
|
590
|
-
|
|
597
|
+
const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
|
|
598
|
+
handlers.onFailed(
|
|
599
|
+
code,
|
|
600
|
+
res.status_reason?.message ?? code,
|
|
601
|
+
res.id,
|
|
602
|
+
res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
|
|
603
|
+
);
|
|
591
604
|
}
|
|
592
605
|
} catch (err) {
|
|
593
606
|
const code = errorCodeOf(err, "UNKNOWN_ERROR");
|
|
594
|
-
handlers.onFailed(
|
|
607
|
+
handlers.onFailed(
|
|
608
|
+
code,
|
|
609
|
+
err instanceof Error ? err.message : code,
|
|
610
|
+
void 0,
|
|
611
|
+
PaymentAttemptResult.NETWORK_ERROR
|
|
612
|
+
);
|
|
595
613
|
}
|
|
596
614
|
}
|
|
597
615
|
/**
|
|
@@ -793,7 +811,7 @@ var injectAppleButtonStyle = () => {
|
|
|
793
811
|
if (document.getElementById(APPLE_STYLE_ID)) return;
|
|
794
812
|
const style = document.createElement("style");
|
|
795
813
|
style.id = APPLE_STYLE_ID;
|
|
796
|
-
style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out
|
|
814
|
+
style.textContent = `.${APPLE_BUTTON_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${APPLE_BUTTON_CLASS}:hover{opacity:.8;}.${APPLE_BUTTON_CLASS}:active{opacity:.7;}.${APPLE_BUTTON_CLASS}:focus-visible{outline:2px solid #0071e3;outline-offset:2px;}`;
|
|
797
815
|
document.head.appendChild(style);
|
|
798
816
|
};
|
|
799
817
|
var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
|
|
@@ -818,17 +836,23 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
|
|
|
818
836
|
if (processing || !applePayConfig || version === null) return;
|
|
819
837
|
if (!callbacks.canStart()) return;
|
|
820
838
|
callbacks.onStart();
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
839
|
+
let session;
|
|
840
|
+
try {
|
|
841
|
+
session = new window.ApplePaySession(version, {
|
|
842
|
+
countryCode: applePayConfig.country_code,
|
|
843
|
+
currencyCode: config.session.currency_code,
|
|
844
|
+
supportedNetworks: applePayConfig.supported_networks,
|
|
845
|
+
merchantCapabilities: applePayConfig.merchant_capabilities,
|
|
846
|
+
total: {
|
|
847
|
+
label: applePayConfig.display_name,
|
|
848
|
+
amount: (config.session.amount / 100).toFixed(2),
|
|
849
|
+
type: "final"
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
} catch {
|
|
853
|
+
callbacks.onSdkError("Apple Pay is not available in this browser.");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
832
856
|
processing = true;
|
|
833
857
|
let abortedDueToError = false;
|
|
834
858
|
session.onvalidatemerchant = async (event) => {
|
|
@@ -871,7 +895,13 @@ var mountApplePayButton = (container, config, api, borderRadius, callbacks) => {
|
|
|
871
895
|
}
|
|
872
896
|
callbacks.onCancel();
|
|
873
897
|
};
|
|
874
|
-
|
|
898
|
+
try {
|
|
899
|
+
session.begin();
|
|
900
|
+
} catch {
|
|
901
|
+
processing = false;
|
|
902
|
+
callbacks.onSdkError("Apple Pay is not available in this browser.");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
875
905
|
styleApplePayModalWhenReady();
|
|
876
906
|
};
|
|
877
907
|
button.addEventListener("click", handleClick);
|
|
@@ -962,13 +992,15 @@ var detectGooglePay = async (config) => {
|
|
|
962
992
|
};
|
|
963
993
|
var GPAY_WRAP_CLASS = "flintn-gpay";
|
|
964
994
|
var GPAY_STYLE_ID = "flintn-gpay-style";
|
|
995
|
+
var gpaySurfaceSelectors = [".gpay-button", ".gpay-card-info-container"].flatMap((s) => [s, `${s}:hover`, `${s}:active`, `${s}.hover`, `${s}.active`]).map((s) => `.${GPAY_WRAP_CLASS} ${s}`).join(",");
|
|
965
996
|
var injectGpayStyle = () => {
|
|
966
997
|
if (document.getElementById(GPAY_STYLE_ID)) return;
|
|
967
998
|
const style = document.createElement("style");
|
|
968
999
|
style.id = GPAY_STYLE_ID;
|
|
969
|
-
style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
|
|
1000
|
+
style.textContent = `.gpay-button:focus:not(:focus-visible){outline:none;box-shadow:none;}.gpay-button-fill:focus-within:not(:has(:focus-visible)){outline:none;box-shadow:none;}.${GPAY_WRAP_CLASS}{cursor:pointer;transition:opacity .15s ease-in-out;}.${GPAY_WRAP_CLASS}:hover{opacity:.8;}.${GPAY_WRAP_CLASS}:active{opacity:.7;}.${GPAY_WRAP_CLASS} button{height:100%;cursor:pointer;}${gpaySurfaceSelectors}{border:none!important;outline:none!important;box-shadow:none!important;background-color:#000!important;}.${GPAY_WRAP_CLASS} .gpay-card-info-container:focus-visible,.${GPAY_WRAP_CLASS} .gpay-button:focus-visible{outline:2px solid #0071e3!important;outline-offset:2px;}.${GPAY_WRAP_CLASS} .gpay-card-info-iframe{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button-fill,.${GPAY_WRAP_CLASS} .gpay-card-info-animation-container-fade-out{opacity:1!important;}.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::before,.${GPAY_WRAP_CLASS} .gpay-button-static-content-container-loading::after{display:none!important;}.${GPAY_WRAP_CLASS} .gpay-button.gpay-button-size-match-height{height:50%!important;}.${GPAY_WRAP_CLASS} > *{width:100%;height:100%;}`;
|
|
970
1001
|
document.head.appendChild(style);
|
|
971
1002
|
};
|
|
1003
|
+
var SHEET_SETTLE_GRACE_MS = 3e3;
|
|
972
1004
|
var mountGooglePayButton = (container, client, config, borderRadius, callbacks) => {
|
|
973
1005
|
let processing = false;
|
|
974
1006
|
const handleClick = () => {
|
|
@@ -977,7 +1009,31 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
|
|
|
977
1009
|
if (!callbacks.canStart()) return;
|
|
978
1010
|
processing = true;
|
|
979
1011
|
callbacks.onStart();
|
|
1012
|
+
let settled = false;
|
|
1013
|
+
let graceTimer = null;
|
|
1014
|
+
const disarmWatchdog = () => {
|
|
1015
|
+
document.removeEventListener("visibilitychange", onVisibleAgain);
|
|
1016
|
+
if (graceTimer !== null) clearTimeout(graceTimer);
|
|
1017
|
+
};
|
|
1018
|
+
const trySettle = () => {
|
|
1019
|
+
if (settled) return false;
|
|
1020
|
+
settled = true;
|
|
1021
|
+
disarmWatchdog();
|
|
1022
|
+
return true;
|
|
1023
|
+
};
|
|
1024
|
+
const onVisibleAgain = () => {
|
|
1025
|
+
if (document.visibilityState !== "visible" || graceTimer !== null) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
graceTimer = setTimeout(() => {
|
|
1029
|
+
if (!trySettle()) return;
|
|
1030
|
+
processing = false;
|
|
1031
|
+
callbacks.onCancel();
|
|
1032
|
+
}, SHEET_SETTLE_GRACE_MS);
|
|
1033
|
+
};
|
|
1034
|
+
document.addEventListener("visibilitychange", onVisibleAgain);
|
|
980
1035
|
client.loadPaymentData(request2).then((paymentData) => {
|
|
1036
|
+
if (!trySettle()) return;
|
|
981
1037
|
const complete = (_success, errorMessage) => {
|
|
982
1038
|
processing = false;
|
|
983
1039
|
if (errorMessage) callbacks.onSdkError(errorMessage);
|
|
@@ -987,6 +1043,7 @@ var mountGooglePayButton = (container, client, config, borderRadius, callbacks)
|
|
|
987
1043
|
callbacks.handlers(complete)
|
|
988
1044
|
);
|
|
989
1045
|
}).catch((err) => {
|
|
1046
|
+
if (!trySettle()) return;
|
|
990
1047
|
processing = false;
|
|
991
1048
|
if (err?.statusCode === "CANCELED") callbacks.onCancel();
|
|
992
1049
|
else callbacks.onSdkError("Google Pay payment failed");
|
|
@@ -1076,7 +1133,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1076
1133
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1077
1134
|
return null;
|
|
1078
1135
|
}
|
|
1079
|
-
const {
|
|
1136
|
+
const {
|
|
1137
|
+
type,
|
|
1138
|
+
payment_id: paymentId,
|
|
1139
|
+
status,
|
|
1140
|
+
decline_code: declineCode
|
|
1141
|
+
} = event.data ?? {};
|
|
1080
1142
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1081
1143
|
return null;
|
|
1082
1144
|
}
|
|
@@ -1090,7 +1152,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1090
1152
|
);
|
|
1091
1153
|
return null;
|
|
1092
1154
|
}
|
|
1093
|
-
return
|
|
1155
|
+
return {
|
|
1156
|
+
status: String(status),
|
|
1157
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1158
|
+
};
|
|
1094
1159
|
};
|
|
1095
1160
|
|
|
1096
1161
|
// src/express/three-ds.ts
|
|
@@ -1113,16 +1178,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1113
1178
|
expectedPaymentId = null;
|
|
1114
1179
|
};
|
|
1115
1180
|
const handleMessage = (event) => {
|
|
1116
|
-
const
|
|
1181
|
+
const result = extractThreeDsResult(event, {
|
|
1117
1182
|
expectedSource: popup,
|
|
1118
1183
|
allowedOrigins,
|
|
1119
1184
|
expectedPaymentId
|
|
1120
1185
|
});
|
|
1121
|
-
if (
|
|
1186
|
+
if (result === null || handled) return;
|
|
1122
1187
|
const paymentId = expectedPaymentId;
|
|
1123
1188
|
handled = true;
|
|
1124
1189
|
cleanup();
|
|
1125
|
-
handlers.onComplete(paymentId, status);
|
|
1190
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1126
1191
|
};
|
|
1127
1192
|
window.addEventListener("message", handleMessage);
|
|
1128
1193
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1233,15 +1298,12 @@ function createFlintNExpressButtons(options) {
|
|
|
1233
1298
|
};
|
|
1234
1299
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1235
1300
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1236
|
-
onComplete: (paymentId, status) => {
|
|
1301
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1237
1302
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1238
|
-
else
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
status,
|
|
1243
|
-
{ paymentId }
|
|
1244
|
-
);
|
|
1303
|
+
else {
|
|
1304
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1305
|
+
fail(threeDsMethod, code, code, { paymentId });
|
|
1306
|
+
}
|
|
1245
1307
|
},
|
|
1246
1308
|
onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
|
|
1247
1309
|
event: PaymentAttemptResult.CANCELLED
|
|
@@ -1267,9 +1329,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1267
1329
|
threeDsMethod = method;
|
|
1268
1330
|
threeDs.open(redirectUrl, paymentId);
|
|
1269
1331
|
},
|
|
1270
|
-
onFailed: (code, message, paymentId) => {
|
|
1332
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1271
1333
|
complete(false, message);
|
|
1272
|
-
fail(method, code, message, { paymentId });
|
|
1334
|
+
fail(method, code, message, { paymentId, event });
|
|
1273
1335
|
}
|
|
1274
1336
|
});
|
|
1275
1337
|
const detection = (async () => {
|
|
@@ -1462,6 +1524,9 @@ var openThreeDsDialog = (options) => {
|
|
|
1462
1524
|
overlay.id = OVERLAY_ID;
|
|
1463
1525
|
overlay.style.cssText = "position: fixed; inset: 0; z-index: 2147483647; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center;";
|
|
1464
1526
|
const panel = document.createElement("div");
|
|
1527
|
+
panel.setAttribute("role", "dialog");
|
|
1528
|
+
panel.setAttribute("aria-modal", "true");
|
|
1529
|
+
panel.setAttribute("aria-label", "3DS verification");
|
|
1465
1530
|
panel.style.cssText = `position: relative; background: #fff; border-radius: 8px; width: min(${PANEL_WIDTH}px, calc(100vw - 32px)); height: min(${PANEL_HEIGHT}px, calc(100vh - 32px)); box-shadow: 0 8px 40px rgba(0, 0, 0, 0.25); overflow: hidden;`;
|
|
1466
1531
|
const closeButton = document.createElement("button");
|
|
1467
1532
|
closeButton.type = "button";
|
|
@@ -1475,26 +1540,43 @@ var openThreeDsDialog = (options) => {
|
|
|
1475
1540
|
iframe.style.cssText = "width: 100%; height: 100%; border: none; display: block;";
|
|
1476
1541
|
panel.append(iframe, closeButton);
|
|
1477
1542
|
overlay.appendChild(panel);
|
|
1543
|
+
const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
1544
|
+
const trapFocus = (event) => {
|
|
1545
|
+
if (event.key !== "Tab") return;
|
|
1546
|
+
const active = document.activeElement;
|
|
1547
|
+
const inside = active !== null && overlay.contains(active);
|
|
1548
|
+
if (event.shiftKey && (!inside || active === iframe)) {
|
|
1549
|
+
event.preventDefault();
|
|
1550
|
+
closeButton.focus();
|
|
1551
|
+
} else if (!event.shiftKey && (!inside || active === closeButton)) {
|
|
1552
|
+
event.preventDefault();
|
|
1553
|
+
iframe.focus();
|
|
1554
|
+
}
|
|
1555
|
+
};
|
|
1478
1556
|
let finished = false;
|
|
1479
|
-
const finish = (status) => {
|
|
1557
|
+
const finish = (status, declineCode) => {
|
|
1480
1558
|
if (finished) return;
|
|
1481
1559
|
finished = true;
|
|
1482
1560
|
window.removeEventListener("message", handleMessage);
|
|
1561
|
+
window.removeEventListener("keydown", trapFocus);
|
|
1483
1562
|
overlay.remove();
|
|
1484
1563
|
closeCurrent = null;
|
|
1485
|
-
|
|
1564
|
+
previouslyFocused?.focus();
|
|
1565
|
+
options.onFinish(status, declineCode);
|
|
1486
1566
|
};
|
|
1487
1567
|
const handleMessage = (event) => {
|
|
1488
|
-
const
|
|
1568
|
+
const result = extractThreeDsResult(event, {
|
|
1489
1569
|
expectedSource: iframe.contentWindow,
|
|
1490
1570
|
allowedOrigins: options.allowedOrigins,
|
|
1491
1571
|
expectedPaymentId: options.paymentId
|
|
1492
1572
|
});
|
|
1493
|
-
if (
|
|
1573
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1494
1574
|
};
|
|
1495
1575
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1496
1576
|
window.addEventListener("message", handleMessage);
|
|
1577
|
+
window.addEventListener("keydown", trapFocus);
|
|
1497
1578
|
document.body.appendChild(overlay);
|
|
1579
|
+
closeButton.focus();
|
|
1498
1580
|
closeCurrent = () => finish("CANCELLED");
|
|
1499
1581
|
};
|
|
1500
1582
|
|
|
@@ -1607,7 +1689,15 @@ function createFlintNFields(options) {
|
|
|
1607
1689
|
const pending = pendingSubmit;
|
|
1608
1690
|
pending.timer = setTimeout(() => {
|
|
1609
1691
|
if (pendingSubmit === pending) {
|
|
1610
|
-
|
|
1692
|
+
const timeoutResult = {
|
|
1693
|
+
status: "PAYMENT_ERROR",
|
|
1694
|
+
error: {
|
|
1695
|
+
code: "THREE_DS_TIMEOUT",
|
|
1696
|
+
message: "3DS challenge timed out"
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
pendingSubmit.resolve(timeoutResult);
|
|
1700
|
+
options.onPayment?.(timeoutResult);
|
|
1611
1701
|
pendingSubmit = null;
|
|
1612
1702
|
}
|
|
1613
1703
|
}, THREE_DS_TIMEOUT);
|
|
@@ -1616,6 +1706,26 @@ function createFlintNFields(options) {
|
|
|
1616
1706
|
}
|
|
1617
1707
|
return;
|
|
1618
1708
|
}
|
|
1709
|
+
if (type === FieldEventType.FIELD_PAYMENT_ATTEMPT) {
|
|
1710
|
+
log("Attempt event:", payload);
|
|
1711
|
+
const { event: attemptEvent, paymentId, code, message } = payload ?? {};
|
|
1712
|
+
if (attemptEvent) {
|
|
1713
|
+
options.onEvent?.({
|
|
1714
|
+
event: attemptEvent,
|
|
1715
|
+
method: PaymentMethod.PAYMENT_CARD,
|
|
1716
|
+
paymentId,
|
|
1717
|
+
code,
|
|
1718
|
+
message,
|
|
1719
|
+
timestamp: Date.now()
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
if (type === FieldEventType.FIELD_POSTAL_CODE_REQUIREMENT) {
|
|
1725
|
+
log("Postal code requirement:", payload);
|
|
1726
|
+
options.onPostalCodeRequirement?.(!!payload?.required);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1619
1729
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1620
1730
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1621
1731
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1643,11 +1753,12 @@ function createFlintNFields(options) {
|
|
|
1643
1753
|
redirectUrl,
|
|
1644
1754
|
paymentId,
|
|
1645
1755
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1646
|
-
onFinish: (status) => {
|
|
1756
|
+
onFinish: (status, declineCode) => {
|
|
1647
1757
|
log("3DS challenge finished:", status);
|
|
1648
1758
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1649
1759
|
paymentId,
|
|
1650
|
-
status
|
|
1760
|
+
status,
|
|
1761
|
+
declineCode
|
|
1651
1762
|
});
|
|
1652
1763
|
}
|
|
1653
1764
|
});
|