flintn-checkout 0.0.17 → 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 +48 -4
- package/dist/index.d.mts +20 -4
- package/dist/index.d.ts +20 -4
- package/dist/index.js +69 -25
- package/dist/index.mjs +69 -25
- package/dist/react.d.mts +16 -2
- package/dist/react.d.ts +16 -2
- package/dist/react.js +84 -25
- package/dist/react.mjs +84 -25
- 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
|
|
|
@@ -409,6 +409,8 @@ if (validation.isValid) {
|
|
|
409
409
|
| `onChange` | `(event: FieldChangeEvent) => void` | No | — | Field value changed |
|
|
410
410
|
| `onFocus` | `(fieldType: TFieldType) => void` | No | — | Field gained focus |
|
|
411
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)" |
|
|
412
414
|
| `onError` | `(error: PaymentError) => void` | No | — | SDK initialization error |
|
|
413
415
|
| `debug` | `boolean` | No | `false` | Enable console debug logs |
|
|
414
416
|
|
|
@@ -433,7 +435,7 @@ Additional React-only options:
|
|
|
433
435
|
| `onChange` | `(event: FieldChangeEvent) => void` | Optional additional callback |
|
|
434
436
|
| `onFocus` | `(fieldType: TFieldType) => void` | Optional additional callback |
|
|
435
437
|
| `onBlur` | `(fieldType: TFieldType, state: FieldState) => void` | Optional additional callback |
|
|
436
|
-
| `onEvent` | `(event:
|
|
438
|
+
| `onEvent` | `(event: AttemptEvent) => void` | Per-attempt analytics — card flow (`method: 'PAYMENT_CARD'`) and express buttons share this stream |
|
|
437
439
|
|
|
438
440
|
### FieldOptions
|
|
439
441
|
|
|
@@ -457,6 +459,7 @@ Additional React-only options:
|
|
|
457
459
|
| `fieldErrors` | `Partial<Record<TFieldType, string \| null>>` | Per-field validation errors (populated after first submit) |
|
|
458
460
|
| `fieldStates` | `Partial<Record<TFieldType, FieldState>>` | Per-field state (always up to date) |
|
|
459
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)" |
|
|
460
463
|
| `paymentResult` | `PaymentResult \| null` | Result after payment attempt |
|
|
461
464
|
| `error` | `PaymentError \| null` | SDK error if any |
|
|
462
465
|
| `validate` | `() => Promise<FieldsValidationResult>` | Validate all fields |
|
|
@@ -475,6 +478,28 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
|
|
|
475
478
|
- All three fields (card number, expiry, CVV) are required — submitting with a missing field returns a `MISSING_FIELDS` error
|
|
476
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
|
|
477
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.
|
|
502
|
+
|
|
478
503
|
### Field Types
|
|
479
504
|
|
|
480
505
|
| Value | Description |
|
|
@@ -617,6 +642,23 @@ interface PaymentError {
|
|
|
617
642
|
}
|
|
618
643
|
```
|
|
619
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
|
+
|
|
620
662
|
### Error handling
|
|
621
663
|
|
|
622
664
|
Errors surface through three separate channels — don't conflate them:
|
|
@@ -637,9 +679,11 @@ Common `code` values:
|
|
|
637
679
|
| `CONFIG_ERROR` | error | Session config failed to load and the API gave no code |
|
|
638
680
|
| `NETWORK_ERROR` / `HTTP_<status>` / `INVALID_RESPONSE` | error or payment | Network failure / HTTP error without an API code / malformed response |
|
|
639
681
|
| `VALIDATION_ERROR` | payment | `submit()` blocked by field validation (details in `fieldErrors`) |
|
|
682
|
+
| `POSTAL_CODE_REQUIRED` | payment | The card BIN requires a postal code and `billingAddress.postalCode` was not passed to `submit()` |
|
|
640
683
|
| `MISSING_FIELDS` / `NOT_INITIALIZED` / `SUBMIT_IN_PROGRESS` / `SUBMIT_TIMEOUT` | payment | Card `submit()` preconditions / duplicate call / no response in time (30s) |
|
|
641
684
|
| `THREE_DS_TIMEOUT` | payment | A 3DS challenge stayed unresolved for 10 minutes |
|
|
642
|
-
| `
|
|
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) |
|
|
643
687
|
| `PAYMENT_FAILED` | payment | Non-declined failure (including unexpected 3DS outcomes) |
|
|
644
688
|
| `APPLE_PAY_SDK_ERROR` / `GOOGLE_PAY_SDK_ERROR` / `PAYPAL_ERROR` | payment | The wallet SDK failed mid-flow |
|
|
645
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
|
/**
|
|
@@ -1115,7 +1133,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1115
1133
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1116
1134
|
return null;
|
|
1117
1135
|
}
|
|
1118
|
-
const {
|
|
1136
|
+
const {
|
|
1137
|
+
type,
|
|
1138
|
+
payment_id: paymentId,
|
|
1139
|
+
status,
|
|
1140
|
+
decline_code: declineCode
|
|
1141
|
+
} = event.data ?? {};
|
|
1119
1142
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1120
1143
|
return null;
|
|
1121
1144
|
}
|
|
@@ -1129,7 +1152,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1129
1152
|
);
|
|
1130
1153
|
return null;
|
|
1131
1154
|
}
|
|
1132
|
-
return
|
|
1155
|
+
return {
|
|
1156
|
+
status: String(status),
|
|
1157
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1158
|
+
};
|
|
1133
1159
|
};
|
|
1134
1160
|
|
|
1135
1161
|
// src/express/three-ds.ts
|
|
@@ -1152,16 +1178,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1152
1178
|
expectedPaymentId = null;
|
|
1153
1179
|
};
|
|
1154
1180
|
const handleMessage = (event) => {
|
|
1155
|
-
const
|
|
1181
|
+
const result = extractThreeDsResult(event, {
|
|
1156
1182
|
expectedSource: popup,
|
|
1157
1183
|
allowedOrigins,
|
|
1158
1184
|
expectedPaymentId
|
|
1159
1185
|
});
|
|
1160
|
-
if (
|
|
1186
|
+
if (result === null || handled) return;
|
|
1161
1187
|
const paymentId = expectedPaymentId;
|
|
1162
1188
|
handled = true;
|
|
1163
1189
|
cleanup();
|
|
1164
|
-
handlers.onComplete(paymentId, status);
|
|
1190
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1165
1191
|
};
|
|
1166
1192
|
window.addEventListener("message", handleMessage);
|
|
1167
1193
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1272,15 +1298,12 @@ function createFlintNExpressButtons(options) {
|
|
|
1272
1298
|
};
|
|
1273
1299
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1274
1300
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1275
|
-
onComplete: (paymentId, status) => {
|
|
1301
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1276
1302
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1277
|
-
else
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
status,
|
|
1282
|
-
{ paymentId }
|
|
1283
|
-
);
|
|
1303
|
+
else {
|
|
1304
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1305
|
+
fail(threeDsMethod, code, code, { paymentId });
|
|
1306
|
+
}
|
|
1284
1307
|
},
|
|
1285
1308
|
onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
|
|
1286
1309
|
event: PaymentAttemptResult.CANCELLED
|
|
@@ -1306,9 +1329,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1306
1329
|
threeDsMethod = method;
|
|
1307
1330
|
threeDs.open(redirectUrl, paymentId);
|
|
1308
1331
|
},
|
|
1309
|
-
onFailed: (code, message, paymentId) => {
|
|
1332
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1310
1333
|
complete(false, message);
|
|
1311
|
-
fail(method, code, message, { paymentId });
|
|
1334
|
+
fail(method, code, message, { paymentId, event });
|
|
1312
1335
|
}
|
|
1313
1336
|
});
|
|
1314
1337
|
const detection = (async () => {
|
|
@@ -1531,7 +1554,7 @@ var openThreeDsDialog = (options) => {
|
|
|
1531
1554
|
}
|
|
1532
1555
|
};
|
|
1533
1556
|
let finished = false;
|
|
1534
|
-
const finish = (status) => {
|
|
1557
|
+
const finish = (status, declineCode) => {
|
|
1535
1558
|
if (finished) return;
|
|
1536
1559
|
finished = true;
|
|
1537
1560
|
window.removeEventListener("message", handleMessage);
|
|
@@ -1539,15 +1562,15 @@ var openThreeDsDialog = (options) => {
|
|
|
1539
1562
|
overlay.remove();
|
|
1540
1563
|
closeCurrent = null;
|
|
1541
1564
|
previouslyFocused?.focus();
|
|
1542
|
-
options.onFinish(status);
|
|
1565
|
+
options.onFinish(status, declineCode);
|
|
1543
1566
|
};
|
|
1544
1567
|
const handleMessage = (event) => {
|
|
1545
|
-
const
|
|
1568
|
+
const result = extractThreeDsResult(event, {
|
|
1546
1569
|
expectedSource: iframe.contentWindow,
|
|
1547
1570
|
allowedOrigins: options.allowedOrigins,
|
|
1548
1571
|
expectedPaymentId: options.paymentId
|
|
1549
1572
|
});
|
|
1550
|
-
if (
|
|
1573
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1551
1574
|
};
|
|
1552
1575
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1553
1576
|
window.addEventListener("message", handleMessage);
|
|
@@ -1683,6 +1706,26 @@ function createFlintNFields(options) {
|
|
|
1683
1706
|
}
|
|
1684
1707
|
return;
|
|
1685
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
|
+
}
|
|
1686
1729
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1687
1730
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1688
1731
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1710,11 +1753,12 @@ function createFlintNFields(options) {
|
|
|
1710
1753
|
redirectUrl,
|
|
1711
1754
|
paymentId,
|
|
1712
1755
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1713
|
-
onFinish: (status) => {
|
|
1756
|
+
onFinish: (status, declineCode) => {
|
|
1714
1757
|
log("3DS challenge finished:", status);
|
|
1715
1758
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1716
1759
|
paymentId,
|
|
1717
|
-
status
|
|
1760
|
+
status,
|
|
1761
|
+
declineCode
|
|
1718
1762
|
});
|
|
1719
1763
|
}
|
|
1720
1764
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -65,7 +65,14 @@ var FieldEventType = {
|
|
|
65
65
|
// modal (the field iframe is too small, and popups get blocked).
|
|
66
66
|
FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED",
|
|
67
67
|
// SDK reports the challenge outcome back into the card iframe.
|
|
68
|
-
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT"
|
|
68
|
+
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT",
|
|
69
|
+
// Card iframe reports attempt analytics (INITIATED / declines / …) which
|
|
70
|
+
// the SDK forwards to the merchant's onEvent — same stream the direct
|
|
71
|
+
// iframe checkout emits.
|
|
72
|
+
FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT",
|
|
73
|
+
// Card iframe reports whether the entered BIN requires a postal code so
|
|
74
|
+
// the merchant can show/require their own postal field.
|
|
75
|
+
FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT"
|
|
69
76
|
};
|
|
70
77
|
|
|
71
78
|
// src/utils.ts
|
|
@@ -536,14 +543,25 @@ var ExpressApi = class {
|
|
|
536
543
|
handlers.onThreeDs(res.required_action.redirect_url, res.id);
|
|
537
544
|
} else {
|
|
538
545
|
const code = declineCodeOf(
|
|
539
|
-
res.status_reason?.
|
|
546
|
+
res.status_reason?.decline_code,
|
|
540
547
|
res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
|
|
541
548
|
);
|
|
542
|
-
|
|
549
|
+
const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
|
|
550
|
+
handlers.onFailed(
|
|
551
|
+
code,
|
|
552
|
+
res.status_reason?.message ?? code,
|
|
553
|
+
res.id,
|
|
554
|
+
res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
|
|
555
|
+
);
|
|
543
556
|
}
|
|
544
557
|
} catch (err) {
|
|
545
558
|
const code = errorCodeOf(err, "UNKNOWN_ERROR");
|
|
546
|
-
handlers.onFailed(
|
|
559
|
+
handlers.onFailed(
|
|
560
|
+
code,
|
|
561
|
+
err instanceof Error ? err.message : code,
|
|
562
|
+
void 0,
|
|
563
|
+
PaymentAttemptResult.NETWORK_ERROR
|
|
564
|
+
);
|
|
547
565
|
}
|
|
548
566
|
}
|
|
549
567
|
/**
|
|
@@ -1067,7 +1085,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1067
1085
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1068
1086
|
return null;
|
|
1069
1087
|
}
|
|
1070
|
-
const {
|
|
1088
|
+
const {
|
|
1089
|
+
type,
|
|
1090
|
+
payment_id: paymentId,
|
|
1091
|
+
status,
|
|
1092
|
+
decline_code: declineCode
|
|
1093
|
+
} = event.data ?? {};
|
|
1071
1094
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1072
1095
|
return null;
|
|
1073
1096
|
}
|
|
@@ -1081,7 +1104,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1081
1104
|
);
|
|
1082
1105
|
return null;
|
|
1083
1106
|
}
|
|
1084
|
-
return
|
|
1107
|
+
return {
|
|
1108
|
+
status: String(status),
|
|
1109
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1110
|
+
};
|
|
1085
1111
|
};
|
|
1086
1112
|
|
|
1087
1113
|
// src/express/three-ds.ts
|
|
@@ -1104,16 +1130,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1104
1130
|
expectedPaymentId = null;
|
|
1105
1131
|
};
|
|
1106
1132
|
const handleMessage = (event) => {
|
|
1107
|
-
const
|
|
1133
|
+
const result = extractThreeDsResult(event, {
|
|
1108
1134
|
expectedSource: popup,
|
|
1109
1135
|
allowedOrigins,
|
|
1110
1136
|
expectedPaymentId
|
|
1111
1137
|
});
|
|
1112
|
-
if (
|
|
1138
|
+
if (result === null || handled) return;
|
|
1113
1139
|
const paymentId = expectedPaymentId;
|
|
1114
1140
|
handled = true;
|
|
1115
1141
|
cleanup();
|
|
1116
|
-
handlers.onComplete(paymentId, status);
|
|
1142
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1117
1143
|
};
|
|
1118
1144
|
window.addEventListener("message", handleMessage);
|
|
1119
1145
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1224,15 +1250,12 @@ function createFlintNExpressButtons(options) {
|
|
|
1224
1250
|
};
|
|
1225
1251
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1226
1252
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1227
|
-
onComplete: (paymentId, status) => {
|
|
1253
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1228
1254
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1229
|
-
else
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
status,
|
|
1234
|
-
{ paymentId }
|
|
1235
|
-
);
|
|
1255
|
+
else {
|
|
1256
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1257
|
+
fail(threeDsMethod, code, code, { paymentId });
|
|
1258
|
+
}
|
|
1236
1259
|
},
|
|
1237
1260
|
onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
|
|
1238
1261
|
event: PaymentAttemptResult.CANCELLED
|
|
@@ -1258,9 +1281,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1258
1281
|
threeDsMethod = method;
|
|
1259
1282
|
threeDs.open(redirectUrl, paymentId);
|
|
1260
1283
|
},
|
|
1261
|
-
onFailed: (code, message, paymentId) => {
|
|
1284
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1262
1285
|
complete(false, message);
|
|
1263
|
-
fail(method, code, message, { paymentId });
|
|
1286
|
+
fail(method, code, message, { paymentId, event });
|
|
1264
1287
|
}
|
|
1265
1288
|
});
|
|
1266
1289
|
const detection = (async () => {
|
|
@@ -1483,7 +1506,7 @@ var openThreeDsDialog = (options) => {
|
|
|
1483
1506
|
}
|
|
1484
1507
|
};
|
|
1485
1508
|
let finished = false;
|
|
1486
|
-
const finish = (status) => {
|
|
1509
|
+
const finish = (status, declineCode) => {
|
|
1487
1510
|
if (finished) return;
|
|
1488
1511
|
finished = true;
|
|
1489
1512
|
window.removeEventListener("message", handleMessage);
|
|
@@ -1491,15 +1514,15 @@ var openThreeDsDialog = (options) => {
|
|
|
1491
1514
|
overlay.remove();
|
|
1492
1515
|
closeCurrent = null;
|
|
1493
1516
|
previouslyFocused?.focus();
|
|
1494
|
-
options.onFinish(status);
|
|
1517
|
+
options.onFinish(status, declineCode);
|
|
1495
1518
|
};
|
|
1496
1519
|
const handleMessage = (event) => {
|
|
1497
|
-
const
|
|
1520
|
+
const result = extractThreeDsResult(event, {
|
|
1498
1521
|
expectedSource: iframe.contentWindow,
|
|
1499
1522
|
allowedOrigins: options.allowedOrigins,
|
|
1500
1523
|
expectedPaymentId: options.paymentId
|
|
1501
1524
|
});
|
|
1502
|
-
if (
|
|
1525
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1503
1526
|
};
|
|
1504
1527
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1505
1528
|
window.addEventListener("message", handleMessage);
|
|
@@ -1635,6 +1658,26 @@ function createFlintNFields(options) {
|
|
|
1635
1658
|
}
|
|
1636
1659
|
return;
|
|
1637
1660
|
}
|
|
1661
|
+
if (type === FieldEventType.FIELD_PAYMENT_ATTEMPT) {
|
|
1662
|
+
log("Attempt event:", payload);
|
|
1663
|
+
const { event: attemptEvent, paymentId, code, message } = payload ?? {};
|
|
1664
|
+
if (attemptEvent) {
|
|
1665
|
+
options.onEvent?.({
|
|
1666
|
+
event: attemptEvent,
|
|
1667
|
+
method: PaymentMethod.PAYMENT_CARD,
|
|
1668
|
+
paymentId,
|
|
1669
|
+
code,
|
|
1670
|
+
message,
|
|
1671
|
+
timestamp: Date.now()
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
return;
|
|
1675
|
+
}
|
|
1676
|
+
if (type === FieldEventType.FIELD_POSTAL_CODE_REQUIREMENT) {
|
|
1677
|
+
log("Postal code requirement:", payload);
|
|
1678
|
+
options.onPostalCodeRequirement?.(!!payload?.required);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1638
1681
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1639
1682
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1640
1683
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1662,11 +1705,12 @@ function createFlintNFields(options) {
|
|
|
1662
1705
|
redirectUrl,
|
|
1663
1706
|
paymentId,
|
|
1664
1707
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1665
|
-
onFinish: (status) => {
|
|
1708
|
+
onFinish: (status, declineCode) => {
|
|
1666
1709
|
log("3DS challenge finished:", status);
|
|
1667
1710
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1668
1711
|
paymentId,
|
|
1669
|
-
status
|
|
1712
|
+
status,
|
|
1713
|
+
declineCode
|
|
1670
1714
|
});
|
|
1671
1715
|
}
|
|
1672
1716
|
});
|
package/dist/react.d.mts
CHANGED
|
@@ -190,7 +190,7 @@ interface SubmitOptions {
|
|
|
190
190
|
email?: string;
|
|
191
191
|
billingAddress?: SubmitBillingAddress;
|
|
192
192
|
}
|
|
193
|
-
interface
|
|
193
|
+
interface AttemptEvent {
|
|
194
194
|
event: TPaymentAttemptResult;
|
|
195
195
|
method: TPaymentMethod;
|
|
196
196
|
paymentId?: string;
|
|
@@ -210,6 +210,15 @@ interface FlintNFieldsOptions {
|
|
|
210
210
|
onChange?: (event: FieldChangeEvent) => void;
|
|
211
211
|
onFocus?: (fieldType: TFieldType) => void;
|
|
212
212
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
213
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
214
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
215
|
+
/**
|
|
216
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
217
|
+
* your own postal field and pass its value via submit's
|
|
218
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
219
|
+
* otherwise.
|
|
220
|
+
*/
|
|
221
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
213
222
|
onError?: (error: PaymentError) => void;
|
|
214
223
|
debug?: boolean;
|
|
215
224
|
}
|
|
@@ -247,7 +256,7 @@ interface UseFlintNFieldsOptions extends Omit<FlintNFieldsOptions, OmittedKeys>
|
|
|
247
256
|
* `expressCapability` return value (hide a ref's container when its method
|
|
248
257
|
* is unavailable).
|
|
249
258
|
*/
|
|
250
|
-
onEvent?: (event:
|
|
259
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
251
260
|
onChange?: (event: FieldChangeEvent) => void;
|
|
252
261
|
onFocus?: (fieldType: TFieldType) => void;
|
|
253
262
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
@@ -274,6 +283,11 @@ interface UseFlintNFieldsReturn {
|
|
|
274
283
|
fieldErrors: FieldErrorsMap;
|
|
275
284
|
fieldStates: FieldStatesMap;
|
|
276
285
|
cardBrand: string | null;
|
|
286
|
+
/**
|
|
287
|
+
* The entered card BIN requires a postal code — show your postal field and
|
|
288
|
+
* pass its value via submit's `billingAddress.postalCode`.
|
|
289
|
+
*/
|
|
290
|
+
postalCodeRequired: boolean;
|
|
277
291
|
paymentResult: PaymentResult | null;
|
|
278
292
|
validate: () => Promise<FieldsValidationResult>;
|
|
279
293
|
submit: (options?: SubmitOptions) => Promise<PaymentResult>;
|
package/dist/react.d.ts
CHANGED
|
@@ -190,7 +190,7 @@ interface SubmitOptions {
|
|
|
190
190
|
email?: string;
|
|
191
191
|
billingAddress?: SubmitBillingAddress;
|
|
192
192
|
}
|
|
193
|
-
interface
|
|
193
|
+
interface AttemptEvent {
|
|
194
194
|
event: TPaymentAttemptResult;
|
|
195
195
|
method: TPaymentMethod;
|
|
196
196
|
paymentId?: string;
|
|
@@ -210,6 +210,15 @@ interface FlintNFieldsOptions {
|
|
|
210
210
|
onChange?: (event: FieldChangeEvent) => void;
|
|
211
211
|
onFocus?: (fieldType: TFieldType) => void;
|
|
212
212
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
213
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
214
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
215
|
+
/**
|
|
216
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
217
|
+
* your own postal field and pass its value via submit's
|
|
218
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
219
|
+
* otherwise.
|
|
220
|
+
*/
|
|
221
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
213
222
|
onError?: (error: PaymentError) => void;
|
|
214
223
|
debug?: boolean;
|
|
215
224
|
}
|
|
@@ -247,7 +256,7 @@ interface UseFlintNFieldsOptions extends Omit<FlintNFieldsOptions, OmittedKeys>
|
|
|
247
256
|
* `expressCapability` return value (hide a ref's container when its method
|
|
248
257
|
* is unavailable).
|
|
249
258
|
*/
|
|
250
|
-
onEvent?: (event:
|
|
259
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
251
260
|
onChange?: (event: FieldChangeEvent) => void;
|
|
252
261
|
onFocus?: (fieldType: TFieldType) => void;
|
|
253
262
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
@@ -274,6 +283,11 @@ interface UseFlintNFieldsReturn {
|
|
|
274
283
|
fieldErrors: FieldErrorsMap;
|
|
275
284
|
fieldStates: FieldStatesMap;
|
|
276
285
|
cardBrand: string | null;
|
|
286
|
+
/**
|
|
287
|
+
* The entered card BIN requires a postal code — show your postal field and
|
|
288
|
+
* pass its value via submit's `billingAddress.postalCode`.
|
|
289
|
+
*/
|
|
290
|
+
postalCodeRequired: boolean;
|
|
277
291
|
paymentResult: PaymentResult | null;
|
|
278
292
|
validate: () => Promise<FieldsValidationResult>;
|
|
279
293
|
submit: (options?: SubmitOptions) => Promise<PaymentResult>;
|
package/dist/react.js
CHANGED
|
@@ -41,6 +41,14 @@ var EventType = {
|
|
|
41
41
|
REDIRECT: "REDIRECT",
|
|
42
42
|
RESIZE: "RESIZE"
|
|
43
43
|
};
|
|
44
|
+
var PaymentMethod = {
|
|
45
|
+
PAYMENT_CARD: "PAYMENT_CARD",
|
|
46
|
+
GOOGLE_PAY: "GOOGLE_PAY",
|
|
47
|
+
APPLE_PAY: "APPLE_PAY",
|
|
48
|
+
PAYPAL: "PAYPAL",
|
|
49
|
+
BANK_ACCOUNT: "BANK_ACCOUNT",
|
|
50
|
+
OTHER: "OTHER"
|
|
51
|
+
};
|
|
44
52
|
var PaymentAttemptResult = {
|
|
45
53
|
INITIATED: "INITIATED",
|
|
46
54
|
THREE_DS_INITIATED: "THREE_DS_INITIATED",
|
|
@@ -72,7 +80,14 @@ var FieldEventType = {
|
|
|
72
80
|
// modal (the field iframe is too small, and popups get blocked).
|
|
73
81
|
FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED",
|
|
74
82
|
// SDK reports the challenge outcome back into the card iframe.
|
|
75
|
-
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT"
|
|
83
|
+
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT",
|
|
84
|
+
// Card iframe reports attempt analytics (INITIATED / declines / …) which
|
|
85
|
+
// the SDK forwards to the merchant's onEvent — same stream the direct
|
|
86
|
+
// iframe checkout emits.
|
|
87
|
+
FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT",
|
|
88
|
+
// Card iframe reports whether the entered BIN requires a postal code so
|
|
89
|
+
// the merchant can show/require their own postal field.
|
|
90
|
+
FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT"
|
|
76
91
|
};
|
|
77
92
|
|
|
78
93
|
// src/utils.ts
|
|
@@ -601,14 +616,25 @@ var ExpressApi = class {
|
|
|
601
616
|
handlers.onThreeDs(res.required_action.redirect_url, res.id);
|
|
602
617
|
} else {
|
|
603
618
|
const code = declineCodeOf(
|
|
604
|
-
res.status_reason?.
|
|
619
|
+
res.status_reason?.decline_code,
|
|
605
620
|
res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
|
|
606
621
|
);
|
|
607
|
-
|
|
622
|
+
const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
|
|
623
|
+
handlers.onFailed(
|
|
624
|
+
code,
|
|
625
|
+
res.status_reason?.message ?? code,
|
|
626
|
+
res.id,
|
|
627
|
+
res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
|
|
628
|
+
);
|
|
608
629
|
}
|
|
609
630
|
} catch (err) {
|
|
610
631
|
const code = errorCodeOf(err, "UNKNOWN_ERROR");
|
|
611
|
-
handlers.onFailed(
|
|
632
|
+
handlers.onFailed(
|
|
633
|
+
code,
|
|
634
|
+
err instanceof Error ? err.message : code,
|
|
635
|
+
void 0,
|
|
636
|
+
PaymentAttemptResult.NETWORK_ERROR
|
|
637
|
+
);
|
|
612
638
|
}
|
|
613
639
|
}
|
|
614
640
|
/**
|
|
@@ -1132,7 +1158,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1132
1158
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1133
1159
|
return null;
|
|
1134
1160
|
}
|
|
1135
|
-
const {
|
|
1161
|
+
const {
|
|
1162
|
+
type,
|
|
1163
|
+
payment_id: paymentId,
|
|
1164
|
+
status,
|
|
1165
|
+
decline_code: declineCode
|
|
1166
|
+
} = event.data ?? {};
|
|
1136
1167
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1137
1168
|
return null;
|
|
1138
1169
|
}
|
|
@@ -1146,7 +1177,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1146
1177
|
);
|
|
1147
1178
|
return null;
|
|
1148
1179
|
}
|
|
1149
|
-
return
|
|
1180
|
+
return {
|
|
1181
|
+
status: String(status),
|
|
1182
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1183
|
+
};
|
|
1150
1184
|
};
|
|
1151
1185
|
|
|
1152
1186
|
// src/express/three-ds.ts
|
|
@@ -1169,16 +1203,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1169
1203
|
expectedPaymentId = null;
|
|
1170
1204
|
};
|
|
1171
1205
|
const handleMessage = (event) => {
|
|
1172
|
-
const
|
|
1206
|
+
const result = extractThreeDsResult(event, {
|
|
1173
1207
|
expectedSource: popup,
|
|
1174
1208
|
allowedOrigins,
|
|
1175
1209
|
expectedPaymentId
|
|
1176
1210
|
});
|
|
1177
|
-
if (
|
|
1211
|
+
if (result === null || handled) return;
|
|
1178
1212
|
const paymentId = expectedPaymentId;
|
|
1179
1213
|
handled = true;
|
|
1180
1214
|
cleanup();
|
|
1181
|
-
handlers.onComplete(paymentId, status);
|
|
1215
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1182
1216
|
};
|
|
1183
1217
|
window.addEventListener("message", handleMessage);
|
|
1184
1218
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1289,15 +1323,12 @@ function createFlintNExpressButtons(options) {
|
|
|
1289
1323
|
};
|
|
1290
1324
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1291
1325
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1292
|
-
onComplete: (paymentId, status) => {
|
|
1326
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1293
1327
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1294
|
-
else
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
status,
|
|
1299
|
-
{ paymentId }
|
|
1300
|
-
);
|
|
1328
|
+
else {
|
|
1329
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1330
|
+
fail(threeDsMethod, code, code, { paymentId });
|
|
1331
|
+
}
|
|
1301
1332
|
},
|
|
1302
1333
|
onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
|
|
1303
1334
|
event: PaymentAttemptResult.CANCELLED
|
|
@@ -1323,9 +1354,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1323
1354
|
threeDsMethod = method;
|
|
1324
1355
|
threeDs.open(redirectUrl, paymentId);
|
|
1325
1356
|
},
|
|
1326
|
-
onFailed: (code, message, paymentId) => {
|
|
1357
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1327
1358
|
complete(false, message);
|
|
1328
|
-
fail(method, code, message, { paymentId });
|
|
1359
|
+
fail(method, code, message, { paymentId, event });
|
|
1329
1360
|
}
|
|
1330
1361
|
});
|
|
1331
1362
|
const detection = (async () => {
|
|
@@ -1548,7 +1579,7 @@ var openThreeDsDialog = (options) => {
|
|
|
1548
1579
|
}
|
|
1549
1580
|
};
|
|
1550
1581
|
let finished = false;
|
|
1551
|
-
const finish = (status) => {
|
|
1582
|
+
const finish = (status, declineCode) => {
|
|
1552
1583
|
if (finished) return;
|
|
1553
1584
|
finished = true;
|
|
1554
1585
|
window.removeEventListener("message", handleMessage);
|
|
@@ -1556,15 +1587,15 @@ var openThreeDsDialog = (options) => {
|
|
|
1556
1587
|
overlay.remove();
|
|
1557
1588
|
closeCurrent = null;
|
|
1558
1589
|
previouslyFocused?.focus();
|
|
1559
|
-
options.onFinish(status);
|
|
1590
|
+
options.onFinish(status, declineCode);
|
|
1560
1591
|
};
|
|
1561
1592
|
const handleMessage = (event) => {
|
|
1562
|
-
const
|
|
1593
|
+
const result = extractThreeDsResult(event, {
|
|
1563
1594
|
expectedSource: iframe.contentWindow,
|
|
1564
1595
|
allowedOrigins: options.allowedOrigins,
|
|
1565
1596
|
expectedPaymentId: options.paymentId
|
|
1566
1597
|
});
|
|
1567
|
-
if (
|
|
1598
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1568
1599
|
};
|
|
1569
1600
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1570
1601
|
window.addEventListener("message", handleMessage);
|
|
@@ -1700,6 +1731,26 @@ function createFlintNFields(options) {
|
|
|
1700
1731
|
}
|
|
1701
1732
|
return;
|
|
1702
1733
|
}
|
|
1734
|
+
if (type === FieldEventType.FIELD_PAYMENT_ATTEMPT) {
|
|
1735
|
+
log("Attempt event:", payload);
|
|
1736
|
+
const { event: attemptEvent, paymentId, code, message } = payload ?? {};
|
|
1737
|
+
if (attemptEvent) {
|
|
1738
|
+
options.onEvent?.({
|
|
1739
|
+
event: attemptEvent,
|
|
1740
|
+
method: PaymentMethod.PAYMENT_CARD,
|
|
1741
|
+
paymentId,
|
|
1742
|
+
code,
|
|
1743
|
+
message,
|
|
1744
|
+
timestamp: Date.now()
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
if (type === FieldEventType.FIELD_POSTAL_CODE_REQUIREMENT) {
|
|
1750
|
+
log("Postal code requirement:", payload);
|
|
1751
|
+
options.onPostalCodeRequirement?.(!!payload?.required);
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1703
1754
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1704
1755
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1705
1756
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1727,11 +1778,12 @@ function createFlintNFields(options) {
|
|
|
1727
1778
|
redirectUrl,
|
|
1728
1779
|
paymentId,
|
|
1729
1780
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1730
|
-
onFinish: (status) => {
|
|
1781
|
+
onFinish: (status, declineCode) => {
|
|
1731
1782
|
log("3DS challenge finished:", status);
|
|
1732
1783
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1733
1784
|
paymentId,
|
|
1734
|
-
status
|
|
1785
|
+
status,
|
|
1786
|
+
declineCode
|
|
1735
1787
|
});
|
|
1736
1788
|
}
|
|
1737
1789
|
});
|
|
@@ -1956,6 +2008,7 @@ function useFlintNFields(options) {
|
|
|
1956
2008
|
const [fieldErrors, setFieldErrors] = (0, import_react2.useState)({});
|
|
1957
2009
|
const [fieldStates, setFieldStates] = (0, import_react2.useState)({});
|
|
1958
2010
|
const [cardBrand, setCardBrand] = (0, import_react2.useState)(null);
|
|
2011
|
+
const [postalCodeRequired, setPostalCodeRequired] = (0, import_react2.useState)(false);
|
|
1959
2012
|
const [expressCapability, setExpressCapability] = (0, import_react2.useState)(null);
|
|
1960
2013
|
const isSubmittedRef = (0, import_react2.useRef)(false);
|
|
1961
2014
|
const latestErrorsRef = (0, import_react2.useRef)({});
|
|
@@ -1979,6 +2032,7 @@ function useFlintNFields(options) {
|
|
|
1979
2032
|
setFieldErrors({});
|
|
1980
2033
|
setFieldStates({});
|
|
1981
2034
|
setCardBrand(null);
|
|
2035
|
+
setPostalCodeRequired(false);
|
|
1982
2036
|
setExpressCapability(null);
|
|
1983
2037
|
isSubmittedRef.current = false;
|
|
1984
2038
|
latestErrorsRef.current = {};
|
|
@@ -1992,6 +2046,10 @@ function useFlintNFields(options) {
|
|
|
1992
2046
|
setPaymentResult(result);
|
|
1993
2047
|
onPaymentRef.current?.(result);
|
|
1994
2048
|
},
|
|
2049
|
+
// Card-flow attempt analytics; express buttons wire the same
|
|
2050
|
+
// callback below, giving the merchant one event stream.
|
|
2051
|
+
onEvent: (event) => onEventRef.current?.(event),
|
|
2052
|
+
onPostalCodeRequirement: (required) => setPostalCodeRequired(required),
|
|
1995
2053
|
onChange: (event) => {
|
|
1996
2054
|
setFieldStates((prev) => ({
|
|
1997
2055
|
...prev,
|
|
@@ -2209,6 +2267,7 @@ function useFlintNFields(options) {
|
|
|
2209
2267
|
fieldErrors,
|
|
2210
2268
|
fieldStates,
|
|
2211
2269
|
cardBrand,
|
|
2270
|
+
postalCodeRequired,
|
|
2212
2271
|
paymentResult,
|
|
2213
2272
|
validate,
|
|
2214
2273
|
submit,
|
package/dist/react.mjs
CHANGED
|
@@ -14,6 +14,14 @@ var EventType = {
|
|
|
14
14
|
REDIRECT: "REDIRECT",
|
|
15
15
|
RESIZE: "RESIZE"
|
|
16
16
|
};
|
|
17
|
+
var PaymentMethod = {
|
|
18
|
+
PAYMENT_CARD: "PAYMENT_CARD",
|
|
19
|
+
GOOGLE_PAY: "GOOGLE_PAY",
|
|
20
|
+
APPLE_PAY: "APPLE_PAY",
|
|
21
|
+
PAYPAL: "PAYPAL",
|
|
22
|
+
BANK_ACCOUNT: "BANK_ACCOUNT",
|
|
23
|
+
OTHER: "OTHER"
|
|
24
|
+
};
|
|
17
25
|
var PaymentAttemptResult = {
|
|
18
26
|
INITIATED: "INITIATED",
|
|
19
27
|
THREE_DS_INITIATED: "THREE_DS_INITIATED",
|
|
@@ -45,7 +53,14 @@ var FieldEventType = {
|
|
|
45
53
|
// modal (the field iframe is too small, and popups get blocked).
|
|
46
54
|
FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED",
|
|
47
55
|
// SDK reports the challenge outcome back into the card iframe.
|
|
48
|
-
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT"
|
|
56
|
+
FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT",
|
|
57
|
+
// Card iframe reports attempt analytics (INITIATED / declines / …) which
|
|
58
|
+
// the SDK forwards to the merchant's onEvent — same stream the direct
|
|
59
|
+
// iframe checkout emits.
|
|
60
|
+
FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT",
|
|
61
|
+
// Card iframe reports whether the entered BIN requires a postal code so
|
|
62
|
+
// the merchant can show/require their own postal field.
|
|
63
|
+
FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT"
|
|
49
64
|
};
|
|
50
65
|
|
|
51
66
|
// src/utils.ts
|
|
@@ -574,14 +589,25 @@ var ExpressApi = class {
|
|
|
574
589
|
handlers.onThreeDs(res.required_action.redirect_url, res.id);
|
|
575
590
|
} else {
|
|
576
591
|
const code = declineCodeOf(
|
|
577
|
-
res.status_reason?.
|
|
592
|
+
res.status_reason?.decline_code,
|
|
578
593
|
res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
|
|
579
594
|
);
|
|
580
|
-
|
|
595
|
+
const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
|
|
596
|
+
handlers.onFailed(
|
|
597
|
+
code,
|
|
598
|
+
res.status_reason?.message ?? code,
|
|
599
|
+
res.id,
|
|
600
|
+
res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
|
|
601
|
+
);
|
|
581
602
|
}
|
|
582
603
|
} catch (err) {
|
|
583
604
|
const code = errorCodeOf(err, "UNKNOWN_ERROR");
|
|
584
|
-
handlers.onFailed(
|
|
605
|
+
handlers.onFailed(
|
|
606
|
+
code,
|
|
607
|
+
err instanceof Error ? err.message : code,
|
|
608
|
+
void 0,
|
|
609
|
+
PaymentAttemptResult.NETWORK_ERROR
|
|
610
|
+
);
|
|
585
611
|
}
|
|
586
612
|
}
|
|
587
613
|
/**
|
|
@@ -1105,7 +1131,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1105
1131
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1106
1132
|
return null;
|
|
1107
1133
|
}
|
|
1108
|
-
const {
|
|
1134
|
+
const {
|
|
1135
|
+
type,
|
|
1136
|
+
payment_id: paymentId,
|
|
1137
|
+
status,
|
|
1138
|
+
decline_code: declineCode
|
|
1139
|
+
} = event.data ?? {};
|
|
1109
1140
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1110
1141
|
return null;
|
|
1111
1142
|
}
|
|
@@ -1119,7 +1150,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1119
1150
|
);
|
|
1120
1151
|
return null;
|
|
1121
1152
|
}
|
|
1122
|
-
return
|
|
1153
|
+
return {
|
|
1154
|
+
status: String(status),
|
|
1155
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1156
|
+
};
|
|
1123
1157
|
};
|
|
1124
1158
|
|
|
1125
1159
|
// src/express/three-ds.ts
|
|
@@ -1142,16 +1176,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1142
1176
|
expectedPaymentId = null;
|
|
1143
1177
|
};
|
|
1144
1178
|
const handleMessage = (event) => {
|
|
1145
|
-
const
|
|
1179
|
+
const result = extractThreeDsResult(event, {
|
|
1146
1180
|
expectedSource: popup,
|
|
1147
1181
|
allowedOrigins,
|
|
1148
1182
|
expectedPaymentId
|
|
1149
1183
|
});
|
|
1150
|
-
if (
|
|
1184
|
+
if (result === null || handled) return;
|
|
1151
1185
|
const paymentId = expectedPaymentId;
|
|
1152
1186
|
handled = true;
|
|
1153
1187
|
cleanup();
|
|
1154
|
-
handlers.onComplete(paymentId, status);
|
|
1188
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1155
1189
|
};
|
|
1156
1190
|
window.addEventListener("message", handleMessage);
|
|
1157
1191
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1262,15 +1296,12 @@ function createFlintNExpressButtons(options) {
|
|
|
1262
1296
|
};
|
|
1263
1297
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1264
1298
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1265
|
-
onComplete: (paymentId, status) => {
|
|
1299
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1266
1300
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1267
|
-
else
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
status,
|
|
1272
|
-
{ paymentId }
|
|
1273
|
-
);
|
|
1301
|
+
else {
|
|
1302
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1303
|
+
fail(threeDsMethod, code, code, { paymentId });
|
|
1304
|
+
}
|
|
1274
1305
|
},
|
|
1275
1306
|
onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
|
|
1276
1307
|
event: PaymentAttemptResult.CANCELLED
|
|
@@ -1296,9 +1327,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1296
1327
|
threeDsMethod = method;
|
|
1297
1328
|
threeDs.open(redirectUrl, paymentId);
|
|
1298
1329
|
},
|
|
1299
|
-
onFailed: (code, message, paymentId) => {
|
|
1330
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1300
1331
|
complete(false, message);
|
|
1301
|
-
fail(method, code, message, { paymentId });
|
|
1332
|
+
fail(method, code, message, { paymentId, event });
|
|
1302
1333
|
}
|
|
1303
1334
|
});
|
|
1304
1335
|
const detection = (async () => {
|
|
@@ -1521,7 +1552,7 @@ var openThreeDsDialog = (options) => {
|
|
|
1521
1552
|
}
|
|
1522
1553
|
};
|
|
1523
1554
|
let finished = false;
|
|
1524
|
-
const finish = (status) => {
|
|
1555
|
+
const finish = (status, declineCode) => {
|
|
1525
1556
|
if (finished) return;
|
|
1526
1557
|
finished = true;
|
|
1527
1558
|
window.removeEventListener("message", handleMessage);
|
|
@@ -1529,15 +1560,15 @@ var openThreeDsDialog = (options) => {
|
|
|
1529
1560
|
overlay.remove();
|
|
1530
1561
|
closeCurrent = null;
|
|
1531
1562
|
previouslyFocused?.focus();
|
|
1532
|
-
options.onFinish(status);
|
|
1563
|
+
options.onFinish(status, declineCode);
|
|
1533
1564
|
};
|
|
1534
1565
|
const handleMessage = (event) => {
|
|
1535
|
-
const
|
|
1566
|
+
const result = extractThreeDsResult(event, {
|
|
1536
1567
|
expectedSource: iframe.contentWindow,
|
|
1537
1568
|
allowedOrigins: options.allowedOrigins,
|
|
1538
1569
|
expectedPaymentId: options.paymentId
|
|
1539
1570
|
});
|
|
1540
|
-
if (
|
|
1571
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1541
1572
|
};
|
|
1542
1573
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1543
1574
|
window.addEventListener("message", handleMessage);
|
|
@@ -1673,6 +1704,26 @@ function createFlintNFields(options) {
|
|
|
1673
1704
|
}
|
|
1674
1705
|
return;
|
|
1675
1706
|
}
|
|
1707
|
+
if (type === FieldEventType.FIELD_PAYMENT_ATTEMPT) {
|
|
1708
|
+
log("Attempt event:", payload);
|
|
1709
|
+
const { event: attemptEvent, paymentId, code, message } = payload ?? {};
|
|
1710
|
+
if (attemptEvent) {
|
|
1711
|
+
options.onEvent?.({
|
|
1712
|
+
event: attemptEvent,
|
|
1713
|
+
method: PaymentMethod.PAYMENT_CARD,
|
|
1714
|
+
paymentId,
|
|
1715
|
+
code,
|
|
1716
|
+
message,
|
|
1717
|
+
timestamp: Date.now()
|
|
1718
|
+
});
|
|
1719
|
+
}
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
if (type === FieldEventType.FIELD_POSTAL_CODE_REQUIREMENT) {
|
|
1723
|
+
log("Postal code requirement:", payload);
|
|
1724
|
+
options.onPostalCodeRequirement?.(!!payload?.required);
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1676
1727
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1677
1728
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1678
1729
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1700,11 +1751,12 @@ function createFlintNFields(options) {
|
|
|
1700
1751
|
redirectUrl,
|
|
1701
1752
|
paymentId,
|
|
1702
1753
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1703
|
-
onFinish: (status) => {
|
|
1754
|
+
onFinish: (status, declineCode) => {
|
|
1704
1755
|
log("3DS challenge finished:", status);
|
|
1705
1756
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1706
1757
|
paymentId,
|
|
1707
|
-
status
|
|
1758
|
+
status,
|
|
1759
|
+
declineCode
|
|
1708
1760
|
});
|
|
1709
1761
|
}
|
|
1710
1762
|
});
|
|
@@ -1929,6 +1981,7 @@ function useFlintNFields(options) {
|
|
|
1929
1981
|
const [fieldErrors, setFieldErrors] = useState2({});
|
|
1930
1982
|
const [fieldStates, setFieldStates] = useState2({});
|
|
1931
1983
|
const [cardBrand, setCardBrand] = useState2(null);
|
|
1984
|
+
const [postalCodeRequired, setPostalCodeRequired] = useState2(false);
|
|
1932
1985
|
const [expressCapability, setExpressCapability] = useState2(null);
|
|
1933
1986
|
const isSubmittedRef = useRef2(false);
|
|
1934
1987
|
const latestErrorsRef = useRef2({});
|
|
@@ -1952,6 +2005,7 @@ function useFlintNFields(options) {
|
|
|
1952
2005
|
setFieldErrors({});
|
|
1953
2006
|
setFieldStates({});
|
|
1954
2007
|
setCardBrand(null);
|
|
2008
|
+
setPostalCodeRequired(false);
|
|
1955
2009
|
setExpressCapability(null);
|
|
1956
2010
|
isSubmittedRef.current = false;
|
|
1957
2011
|
latestErrorsRef.current = {};
|
|
@@ -1965,6 +2019,10 @@ function useFlintNFields(options) {
|
|
|
1965
2019
|
setPaymentResult(result);
|
|
1966
2020
|
onPaymentRef.current?.(result);
|
|
1967
2021
|
},
|
|
2022
|
+
// Card-flow attempt analytics; express buttons wire the same
|
|
2023
|
+
// callback below, giving the merchant one event stream.
|
|
2024
|
+
onEvent: (event) => onEventRef.current?.(event),
|
|
2025
|
+
onPostalCodeRequirement: (required) => setPostalCodeRequired(required),
|
|
1968
2026
|
onChange: (event) => {
|
|
1969
2027
|
setFieldStates((prev) => ({
|
|
1970
2028
|
...prev,
|
|
@@ -2182,6 +2240,7 @@ function useFlintNFields(options) {
|
|
|
2182
2240
|
fieldErrors,
|
|
2183
2241
|
fieldStates,
|
|
2184
2242
|
cardBrand,
|
|
2243
|
+
postalCodeRequired,
|
|
2185
2244
|
paymentResult,
|
|
2186
2245
|
validate,
|
|
2187
2246
|
submit,
|
package/package.json
CHANGED