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 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: 'do_not_honor', message: 'Payment declined.' }
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:** For card fields, Hosted Fields emits only the terminal `onPayment` event. The express buttons additionally emit per-attempt wallet analytics via the top-level `onEvent` callback (same `onPayment` / `onEvent` split as Iframe Checkout) see "Express buttons" below. For full per-attempt analytics across the card form too, use Iframe Checkout.
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: ExpressAttempt) => void` | Per-attempt wallet analytics from the express buttons (see "Express buttons") |
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
- | `PAYMENT_DECLINED` / processor decline codes | payment | Declined; the processor's own code is passed through when present |
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 ExpressAttempt {
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: ExpressAttempt) => void;
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) => void;
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 ExpressAttempt, 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 };
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 ExpressAttempt {
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: ExpressAttempt) => void;
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) => void;
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 ExpressAttempt, 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 };
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?.decline_reason,
594
+ res.status_reason?.decline_code,
588
595
  res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
589
596
  );
590
- handlers.onFailed(code, res.status_reason?.message ?? code, res.id);
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(code, err instanceof Error ? err.message : code);
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 { type, payment_id: paymentId, status } = event.data ?? {};
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 String(status);
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 status = extractThreeDsResult(event, {
1181
+ const result = extractThreeDsResult(event, {
1156
1182
  expectedSource: popup,
1157
1183
  allowedOrigins,
1158
1184
  expectedPaymentId
1159
1185
  });
1160
- if (status === null || handled) return;
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
- fail(
1279
- threeDsMethod,
1280
- status === "DECLINED" ? "THREE_DS_FAILED" : "PAYMENT_FAILED",
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 status = extractThreeDsResult(event, {
1568
+ const result = extractThreeDsResult(event, {
1546
1569
  expectedSource: iframe.contentWindow,
1547
1570
  allowedOrigins: options.allowedOrigins,
1548
1571
  expectedPaymentId: options.paymentId
1549
1572
  });
1550
- if (status !== null) finish(status);
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?.decline_reason,
546
+ res.status_reason?.decline_code,
540
547
  res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
541
548
  );
542
- handlers.onFailed(code, res.status_reason?.message ?? code, res.id);
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(code, err instanceof Error ? err.message : code);
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 { type, payment_id: paymentId, status } = event.data ?? {};
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 String(status);
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 status = extractThreeDsResult(event, {
1133
+ const result = extractThreeDsResult(event, {
1108
1134
  expectedSource: popup,
1109
1135
  allowedOrigins,
1110
1136
  expectedPaymentId
1111
1137
  });
1112
- if (status === null || handled) return;
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
- fail(
1231
- threeDsMethod,
1232
- status === "DECLINED" ? "THREE_DS_FAILED" : "PAYMENT_FAILED",
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 status = extractThreeDsResult(event, {
1520
+ const result = extractThreeDsResult(event, {
1498
1521
  expectedSource: iframe.contentWindow,
1499
1522
  allowedOrigins: options.allowedOrigins,
1500
1523
  expectedPaymentId: options.paymentId
1501
1524
  });
1502
- if (status !== null) finish(status);
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 ExpressAttempt {
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: ExpressAttempt) => void;
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 ExpressAttempt {
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: ExpressAttempt) => void;
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?.decline_reason,
619
+ res.status_reason?.decline_code,
605
620
  res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
606
621
  );
607
- handlers.onFailed(code, res.status_reason?.message ?? code, res.id);
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(code, err instanceof Error ? err.message : code);
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 { type, payment_id: paymentId, status } = event.data ?? {};
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 String(status);
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 status = extractThreeDsResult(event, {
1206
+ const result = extractThreeDsResult(event, {
1173
1207
  expectedSource: popup,
1174
1208
  allowedOrigins,
1175
1209
  expectedPaymentId
1176
1210
  });
1177
- if (status === null || handled) return;
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
- fail(
1296
- threeDsMethod,
1297
- status === "DECLINED" ? "THREE_DS_FAILED" : "PAYMENT_FAILED",
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 status = extractThreeDsResult(event, {
1593
+ const result = extractThreeDsResult(event, {
1563
1594
  expectedSource: iframe.contentWindow,
1564
1595
  allowedOrigins: options.allowedOrigins,
1565
1596
  expectedPaymentId: options.paymentId
1566
1597
  });
1567
- if (status !== null) finish(status);
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?.decline_reason,
592
+ res.status_reason?.decline_code,
578
593
  res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
579
594
  );
580
- handlers.onFailed(code, res.status_reason?.message ?? code, res.id);
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(code, err instanceof Error ? err.message : code);
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 { type, payment_id: paymentId, status } = event.data ?? {};
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 String(status);
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 status = extractThreeDsResult(event, {
1179
+ const result = extractThreeDsResult(event, {
1146
1180
  expectedSource: popup,
1147
1181
  allowedOrigins,
1148
1182
  expectedPaymentId
1149
1183
  });
1150
- if (status === null || handled) return;
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
- fail(
1269
- threeDsMethod,
1270
- status === "DECLINED" ? "THREE_DS_FAILED" : "PAYMENT_FAILED",
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 status = extractThreeDsResult(event, {
1566
+ const result = extractThreeDsResult(event, {
1536
1567
  expectedSource: iframe.contentWindow,
1537
1568
  allowedOrigins: options.allowedOrigins,
1538
1569
  expectedPaymentId: options.paymentId
1539
1570
  });
1540
- if (status !== null) finish(status);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flintn-checkout",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "FlintN Payment SDK — drop-in iframe checkout for card payments and wallets with localization and theming.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",