flintn-checkout 0.0.17 → 0.0.19
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 +148 -7
- package/dist/index.d.mts +28 -4
- package/dist/index.d.ts +28 -4
- package/dist/index.js +82 -33
- package/dist/index.mjs +82 -33
- package/dist/react.d.mts +36 -2
- package/dist/react.d.ts +36 -2
- package/dist/react.js +364 -264
- package/dist/react.mjs +364 -264
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npm install flintn-checkout
|
|
|
10
10
|
|
|
11
11
|
## Iframe Checkout
|
|
12
12
|
|
|
13
|
-
Full checkout UI rendered inside a single iframe. Includes card form, express payments (Apple Pay, PayPal), and 3DS handling.
|
|
13
|
+
Full checkout UI rendered inside a single iframe. Includes card form, express payments (Apple Pay, Google Pay, PayPal), and 3DS handling.
|
|
14
14
|
|
|
15
15
|
**React**
|
|
16
16
|
|
|
@@ -108,6 +108,92 @@ payment.mount('#payment-container');
|
|
|
108
108
|
</html>
|
|
109
109
|
```
|
|
110
110
|
|
|
111
|
+
### External express buttons (`expressButtons: false`)
|
|
112
|
+
|
|
113
|
+
By default the iframe renders express buttons (Apple Pay / Google Pay /
|
|
114
|
+
PayPal) inside the checkout form. Set `config.expressButtons: false` to keep
|
|
115
|
+
the iframe card-form only and render the wallet buttons directly in your own
|
|
116
|
+
page — full control over placement and layout, and wallet payment sheets open
|
|
117
|
+
from the top-level page instead of inside an iframe.
|
|
118
|
+
|
|
119
|
+
**React** — the hook mounts the buttons for you and merges wallet results
|
|
120
|
+
into the same `onPayment` / `onEvent` callbacks as the card form:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { useFlintNPayment } from 'flintn-checkout/react';
|
|
124
|
+
|
|
125
|
+
function Checkout() {
|
|
126
|
+
const {
|
|
127
|
+
containerRef,
|
|
128
|
+
applePayRef,
|
|
129
|
+
googlePayRef,
|
|
130
|
+
payPalRef,
|
|
131
|
+
expressAvailable,
|
|
132
|
+
paymentResult,
|
|
133
|
+
} = useFlintNPayment({
|
|
134
|
+
config: {
|
|
135
|
+
clientSessionId: 'your_client_session_id',
|
|
136
|
+
expressButtons: false,
|
|
137
|
+
},
|
|
138
|
+
// Single callback for wallets AND the card form
|
|
139
|
+
onPayment: (result) => console.log('Payment:', result),
|
|
140
|
+
// Single per-attempt stream for wallets AND the card form
|
|
141
|
+
onEvent: (event) => console.log('Event:', event),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div style={{ maxWidth: 440 }}>
|
|
146
|
+
{/* Your layout, your styling. Hide with CSS while unavailable —
|
|
147
|
+
do not conditionally unmount the refs. */}
|
|
148
|
+
<div style={{ display: expressAvailable ? 'block' : 'none' }}>
|
|
149
|
+
<div ref={applePayRef} />
|
|
150
|
+
<div ref={googlePayRef} />
|
|
151
|
+
<div ref={payPalRef} />
|
|
152
|
+
<hr />
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Card form iframe — no express buttons inside */}
|
|
156
|
+
<div ref={containerRef} style={{ width: '100%' }} />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Vanilla JavaScript** — pair `createFlintNPayment` with the standalone
|
|
163
|
+
express module:
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
import { createFlintNPayment, createFlintNExpressButtons } from 'flintn-checkout';
|
|
167
|
+
|
|
168
|
+
const payment = createFlintNPayment({
|
|
169
|
+
config: { clientSessionId: 'your_client_session_id', expressButtons: false },
|
|
170
|
+
onPayment: (result) => console.log('Card payment:', result),
|
|
171
|
+
});
|
|
172
|
+
payment.mount('#payment-container');
|
|
173
|
+
|
|
174
|
+
const express = createFlintNExpressButtons({
|
|
175
|
+
config: { clientSessionId: 'your_client_session_id' },
|
|
176
|
+
onPayment: (result) => console.log('Wallet payment:', result),
|
|
177
|
+
onCapability: (cap) => {
|
|
178
|
+
// Show/hide your express section based on device availability
|
|
179
|
+
document.getElementById('express-row').hidden =
|
|
180
|
+
!(cap.applePay || cap.googlePay || cap.paypal);
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
express.mountExpressButtons([
|
|
184
|
+
{ method: 'APPLE_PAY', elementSelector: '#apple-pay' },
|
|
185
|
+
{ method: 'GOOGLE_PAY', elementSelector: '#google-pay' },
|
|
186
|
+
{ method: 'PAYPAL', elementSelector: '#paypal' },
|
|
187
|
+
]);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Notes:
|
|
191
|
+
|
|
192
|
+
- `formFields.formVariant` `LIST`/`RADIO` are ignored with
|
|
193
|
+
`expressButtons: false` — the express/card switch lives in your page now.
|
|
194
|
+
- After a successful wallet payment, disable or hide the card form — the
|
|
195
|
+
session is completed and further card submits will fail.
|
|
196
|
+
|
|
111
197
|
## Events — terminal vs per-attempt
|
|
112
198
|
|
|
113
199
|
The SDK fires two streams of events. Use the one that fits your use case:
|
|
@@ -166,7 +252,7 @@ For UI events (LIST and RADIO variants), `view` is the view that became active:
|
|
|
166
252
|
|
|
167
253
|
```
|
|
168
254
|
onEvent → { event: 'INITIATED', method: 'PAYMENT_CARD' }
|
|
169
|
-
onEvent → { event: 'SOFT_DECLINE', method: 'PAYMENT_CARD', code: '
|
|
255
|
+
onEvent → { event: 'SOFT_DECLINE', method: 'PAYMENT_CARD', code: 'FNT-EC-2003', message: 'Insufficient funds.' }
|
|
170
256
|
// buyer retries with another card
|
|
171
257
|
onEvent → { event: 'INITIATED', method: 'PAYMENT_CARD' }
|
|
172
258
|
onEvent → { event: 'AUTHORIZED', method: 'PAYMENT_CARD', paymentId: 'pay_...' }
|
|
@@ -246,6 +332,7 @@ Do **not** set a fixed `height` on the container — it will leave empty space b
|
|
|
246
332
|
| `styles` | `FormStyles` | No | Custom styles (colors, border radius, text) for the checkout form |
|
|
247
333
|
| `placeholders` | `FormPlaceholders` | No | Per-field placeholder text overrides (see `FormPlaceholders`) |
|
|
248
334
|
| `successRedirectUrl` | `string` | No | Redirect URL after successful payment |
|
|
335
|
+
| `expressButtons` | `boolean` | No | Default `true` — express buttons (Apple Pay / Google Pay / PayPal) render inside the iframe form. Set `false` to render the card form only and mount express buttons in your own page via `createFlintNExpressButtons`; `formFields.formVariant` `LIST`/`RADIO` are ignored in that case |
|
|
249
336
|
|
|
250
337
|
### React Hook Return Values
|
|
251
338
|
|
|
@@ -253,9 +340,13 @@ Do **not** set a fixed `height` on the container — it will leave empty space b
|
|
|
253
340
|
|-------|------|-------------|
|
|
254
341
|
| `containerRef` | `RefObject<HTMLDivElement>` | Ref to attach to container element |
|
|
255
342
|
| `isReady` | `boolean` | Widget is loaded and ready |
|
|
256
|
-
| `paymentResult` | `PaymentResult \| null` | Result after terminal `onPayment` event |
|
|
343
|
+
| `paymentResult` | `PaymentResult \| null` | Result after terminal `onPayment` event (card or wallet) |
|
|
257
344
|
| `events` | `CheckoutEvent[]` | Per-attempt event log (accumulated since mount) |
|
|
258
345
|
| `error` | `PaymentError \| null` | SDK error if any |
|
|
346
|
+
| `applePayRef` | `RefObject<HTMLDivElement>` | Apple Pay mount point — used with `expressButtons: false` |
|
|
347
|
+
| `googlePayRef` | `RefObject<HTMLDivElement>` | Google Pay mount point — used with `expressButtons: false` |
|
|
348
|
+
| `payPalRef` | `RefObject<HTMLDivElement>` | PayPal mount point — used with `expressButtons: false` |
|
|
349
|
+
| `expressAvailable` | `boolean` | At least one wallet is available on this device/session (`expressButtons: false` only) |
|
|
259
350
|
|
|
260
351
|
---
|
|
261
352
|
|
|
@@ -270,7 +361,7 @@ When a card payment requires 3DS, the SDK shows the bank's challenge in a
|
|
|
270
361
|
`THREE_DS_CANCELLED` payment error. While the challenge is open, the
|
|
271
362
|
`submit()` promise stays pending with a 10-minute timeout.
|
|
272
363
|
|
|
273
|
-
> **Note:**
|
|
364
|
+
> **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
365
|
|
|
275
366
|
**React**
|
|
276
367
|
|
|
@@ -409,6 +500,8 @@ if (validation.isValid) {
|
|
|
409
500
|
| `onChange` | `(event: FieldChangeEvent) => void` | No | — | Field value changed |
|
|
410
501
|
| `onFocus` | `(fieldType: TFieldType) => void` | No | — | Field gained focus |
|
|
411
502
|
| `onBlur` | `(fieldType: TFieldType, state: FieldState) => void` | No | — | Field lost focus |
|
|
503
|
+
| `onEvent` | `(event: AttemptEvent) => void` | No | — | Per-attempt analytics for the card flow (`method: 'PAYMENT_CARD'`) |
|
|
504
|
+
| `onPostalCodeRequirement` | `(required: boolean) => void` | No | — | Entered card BIN requires a postal code — see "Postal code (BIN-driven)" |
|
|
412
505
|
| `onError` | `(error: PaymentError) => void` | No | — | SDK initialization error |
|
|
413
506
|
| `debug` | `boolean` | No | `false` | Enable console debug logs |
|
|
414
507
|
|
|
@@ -433,7 +526,7 @@ Additional React-only options:
|
|
|
433
526
|
| `onChange` | `(event: FieldChangeEvent) => void` | Optional additional callback |
|
|
434
527
|
| `onFocus` | `(fieldType: TFieldType) => void` | Optional additional callback |
|
|
435
528
|
| `onBlur` | `(fieldType: TFieldType, state: FieldState) => void` | Optional additional callback |
|
|
436
|
-
| `onEvent` | `(event:
|
|
529
|
+
| `onEvent` | `(event: AttemptEvent) => void` | Per-attempt analytics — card flow (`method: 'PAYMENT_CARD'`) and express buttons share this stream |
|
|
437
530
|
|
|
438
531
|
### FieldOptions
|
|
439
532
|
|
|
@@ -457,6 +550,7 @@ Additional React-only options:
|
|
|
457
550
|
| `fieldErrors` | `Partial<Record<TFieldType, string \| null>>` | Per-field validation errors (populated after first submit) |
|
|
458
551
|
| `fieldStates` | `Partial<Record<TFieldType, FieldState>>` | Per-field state (always up to date) |
|
|
459
552
|
| `cardBrand` | `string \| null` | Detected card brand (e.g. `"visa"`, `"mastercard"`) |
|
|
553
|
+
| `postalCodeRequired` | `boolean` | The entered card BIN requires a postal code — see "Postal code (BIN-driven)" |
|
|
460
554
|
| `paymentResult` | `PaymentResult \| null` | Result after payment attempt |
|
|
461
555
|
| `error` | `PaymentError \| null` | SDK error if any |
|
|
462
556
|
| `validate` | `() => Promise<FieldsValidationResult>` | Validate all fields |
|
|
@@ -475,6 +569,29 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
|
|
|
475
569
|
- All three fields (card number, expiry, CVV) are required — submitting with a missing field returns a `MISSING_FIELDS` error
|
|
476
570
|
- 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
571
|
|
|
572
|
+
### Postal code (BIN-driven)
|
|
573
|
+
|
|
574
|
+
Some issuers require a postal code for online payments (currently US-issued
|
|
575
|
+
cards — the requirement is resolved server-side by BIN). Once the buyer has
|
|
576
|
+
typed enough digits, the card-number field checks the BIN against the payments
|
|
577
|
+
API and reports whether a postal code is required — `postalCodeRequired`
|
|
578
|
+
(React) / `onPostalCodeRequirement` (vanilla). Show your own postal code input
|
|
579
|
+
when it flips to `true` and pass the value at submit:
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
582
|
+
const { postalCodeRequired, submit } = useFlintNFields({ ... });
|
|
583
|
+
|
|
584
|
+
// render your postal input when postalCodeRequired === true, then:
|
|
585
|
+
await submit({
|
|
586
|
+
cardholderName,
|
|
587
|
+
billingAddress: { postalCode },
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
If the BIN requires a postal code and `billingAddress.postalCode` is missing,
|
|
592
|
+
`submit()` resolves with `PAYMENT_ERROR` / `POSTAL_CODE_REQUIRED` without
|
|
593
|
+
calling the payments API.
|
|
594
|
+
|
|
478
595
|
### Field Types
|
|
479
596
|
|
|
480
597
|
| Value | Description |
|
|
@@ -617,6 +734,25 @@ interface PaymentError {
|
|
|
617
734
|
}
|
|
618
735
|
```
|
|
619
736
|
|
|
737
|
+
### SubmitOptions (Hosted Fields)
|
|
738
|
+
|
|
739
|
+
```typescript
|
|
740
|
+
interface SubmitOptions {
|
|
741
|
+
// Required by default — the API rejects card payments without it unless the
|
|
742
|
+
// merchant disabled the cardholder name field in the client session request.
|
|
743
|
+
cardholderName?: string;
|
|
744
|
+
email?: string;
|
|
745
|
+
billingAddress?: {
|
|
746
|
+
addressLine1?: string;
|
|
747
|
+
addressLine2?: string;
|
|
748
|
+
city?: string;
|
|
749
|
+
state?: string;
|
|
750
|
+
postalCode?: string; // required when the BIN demands it — see "Postal code (BIN-driven)"
|
|
751
|
+
country?: string;
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
620
756
|
### Error handling
|
|
621
757
|
|
|
622
758
|
Errors surface through three separate channels — don't conflate them:
|
|
@@ -637,9 +773,11 @@ Common `code` values:
|
|
|
637
773
|
| `CONFIG_ERROR` | error | Session config failed to load and the API gave no code |
|
|
638
774
|
| `NETWORK_ERROR` / `HTTP_<status>` / `INVALID_RESPONSE` | error or payment | Network failure / HTTP error without an API code / malformed response |
|
|
639
775
|
| `VALIDATION_ERROR` | payment | `submit()` blocked by field validation (details in `fieldErrors`) |
|
|
776
|
+
| `POSTAL_CODE_REQUIRED` | payment | The card BIN requires a postal code and `billingAddress.postalCode` was not passed to `submit()` |
|
|
640
777
|
| `MISSING_FIELDS` / `NOT_INITIALIZED` / `SUBMIT_IN_PROGRESS` / `SUBMIT_TIMEOUT` | payment | Card `submit()` preconditions / duplicate call / no response in time (30s) |
|
|
641
778
|
| `THREE_DS_TIMEOUT` | payment | A 3DS challenge stayed unresolved for 10 minutes |
|
|
642
|
-
| `
|
|
779
|
+
| `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 |
|
|
780
|
+
| `PAYMENT_DECLINED` | payment | Declined without a specific decline code (fallback) |
|
|
643
781
|
| `PAYMENT_FAILED` | payment | Non-declined failure (including unexpected 3DS outcomes) |
|
|
644
782
|
| `APPLE_PAY_SDK_ERROR` / `GOOGLE_PAY_SDK_ERROR` / `PAYPAL_ERROR` | payment | The wallet SDK failed mid-flow |
|
|
645
783
|
| `PAYPAL_MISSING_PAYMENT_ID` | payment | PayPal approved but no payment id was captured (anomaly) |
|
|
@@ -870,7 +1008,10 @@ on `createField` (or the `fields` prop of `useFlintNFields`) instead.
|
|
|
870
1008
|
|
|
871
1009
|
## Form Variants
|
|
872
1010
|
|
|
873
|
-
The iframe checkout supports three layout variants via `formFields.formVariant
|
|
1011
|
+
The iframe checkout supports three layout variants via `formFields.formVariant`.
|
|
1012
|
+
Variants only apply when express buttons render inside the iframe — with
|
|
1013
|
+
`expressButtons: false` the variant is ignored and the iframe always shows the
|
|
1014
|
+
plain card form.
|
|
874
1015
|
|
|
875
1016
|
- **DEFAULT** — Express payment methods shown above the card form with dividers.
|
|
876
1017
|
- **LIST** — Express payment methods shown first; users tap a "Credit or Debit Card" button to navigate to the card form, with a back arrow to return to the express view. Toggling emits `FORM_ENTERED` / `FORM_EXITED` UI events.
|
package/dist/index.d.mts
CHANGED
|
@@ -81,6 +81,14 @@ interface FlintNConfig {
|
|
|
81
81
|
styles?: FormStyles;
|
|
82
82
|
placeholders?: FormPlaceholders;
|
|
83
83
|
successRedirectUrl?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the iframe renders express buttons (Apple Pay / Google Pay /
|
|
86
|
+
* PayPal) inside the checkout form. Default true. Set false to render the
|
|
87
|
+
* card form only and mount express buttons in your own page via
|
|
88
|
+
* `createFlintNExpressButtons` — note `formFields.formVariant` LIST/RADIO
|
|
89
|
+
* are ignored in that case.
|
|
90
|
+
*/
|
|
91
|
+
expressButtons?: boolean;
|
|
84
92
|
}
|
|
85
93
|
declare const PaymentMethod: {
|
|
86
94
|
readonly PAYMENT_CARD: "PAYMENT_CARD";
|
|
@@ -173,6 +181,8 @@ declare const FieldEventType: {
|
|
|
173
181
|
readonly FIELD_HEIGHT: "FIELD_HEIGHT";
|
|
174
182
|
readonly FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED";
|
|
175
183
|
readonly FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT";
|
|
184
|
+
readonly FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT";
|
|
185
|
+
readonly FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT";
|
|
176
186
|
};
|
|
177
187
|
type TFieldEventType = (typeof FieldEventType)[keyof typeof FieldEventType];
|
|
178
188
|
interface FieldOptions {
|
|
@@ -220,7 +230,7 @@ interface ExpressCapability {
|
|
|
220
230
|
googlePay: boolean;
|
|
221
231
|
paypal: boolean;
|
|
222
232
|
}
|
|
223
|
-
interface
|
|
233
|
+
interface AttemptEvent {
|
|
224
234
|
event: TPaymentAttemptResult;
|
|
225
235
|
method: TPaymentMethod;
|
|
226
236
|
paymentId?: string;
|
|
@@ -240,6 +250,15 @@ interface FlintNFieldsOptions {
|
|
|
240
250
|
onChange?: (event: FieldChangeEvent) => void;
|
|
241
251
|
onFocus?: (fieldType: TFieldType) => void;
|
|
242
252
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
253
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
254
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
255
|
+
/**
|
|
256
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
257
|
+
* your own postal field and pass its value via submit's
|
|
258
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
259
|
+
* otherwise.
|
|
260
|
+
*/
|
|
261
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
243
262
|
onError?: (error: PaymentError) => void;
|
|
244
263
|
debug?: boolean;
|
|
245
264
|
}
|
|
@@ -293,7 +312,7 @@ interface FlintNExpressButtonsOptions {
|
|
|
293
312
|
apiUrl?: string;
|
|
294
313
|
buttonBorderRadius?: number;
|
|
295
314
|
onPayment?: (result: PaymentResult) => void;
|
|
296
|
-
onEvent?: (event:
|
|
315
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
297
316
|
onCapability?: (capability: ExpressCapability) => void;
|
|
298
317
|
onError?: (error: PaymentError) => void;
|
|
299
318
|
debug?: boolean;
|
|
@@ -349,8 +368,11 @@ interface AuthorizeResponse {
|
|
|
349
368
|
};
|
|
350
369
|
status_reason?: {
|
|
351
370
|
decline_reason?: string;
|
|
371
|
+
decline_code?: string;
|
|
372
|
+
decline_type?: 'SOFT_DECLINE' | 'HARD_DECLINE';
|
|
352
373
|
message?: string;
|
|
353
374
|
};
|
|
375
|
+
session_retryable?: boolean;
|
|
354
376
|
}
|
|
355
377
|
interface ApplePayToken {
|
|
356
378
|
payment_data: {
|
|
@@ -377,7 +399,9 @@ interface GooglePayToken {
|
|
|
377
399
|
interface WalletAuthorizeHandlers {
|
|
378
400
|
onAuthorized: (paymentId: string) => void;
|
|
379
401
|
onThreeDs: (redirectUrl: string, paymentId: string) => void;
|
|
380
|
-
onFailed: (code: string, message: string, paymentId?: string
|
|
402
|
+
onFailed: (code: string, message: string, paymentId?: string,
|
|
403
|
+
/** Attempt analytics classification; defaults to HARD_DECLINE. */
|
|
404
|
+
event?: TPaymentAttemptResult) => void;
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
/**
|
|
@@ -425,4 +449,4 @@ declare const validateConfig: (config: {
|
|
|
425
449
|
}) => void;
|
|
426
450
|
declare const sanitizeToken: (token: string) => string;
|
|
427
451
|
|
|
428
|
-
export { type ApplePayConfig, type ApplePayToken, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type
|
|
452
|
+
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
|
@@ -81,6 +81,14 @@ interface FlintNConfig {
|
|
|
81
81
|
styles?: FormStyles;
|
|
82
82
|
placeholders?: FormPlaceholders;
|
|
83
83
|
successRedirectUrl?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Whether the iframe renders express buttons (Apple Pay / Google Pay /
|
|
86
|
+
* PayPal) inside the checkout form. Default true. Set false to render the
|
|
87
|
+
* card form only and mount express buttons in your own page via
|
|
88
|
+
* `createFlintNExpressButtons` — note `formFields.formVariant` LIST/RADIO
|
|
89
|
+
* are ignored in that case.
|
|
90
|
+
*/
|
|
91
|
+
expressButtons?: boolean;
|
|
84
92
|
}
|
|
85
93
|
declare const PaymentMethod: {
|
|
86
94
|
readonly PAYMENT_CARD: "PAYMENT_CARD";
|
|
@@ -173,6 +181,8 @@ declare const FieldEventType: {
|
|
|
173
181
|
readonly FIELD_HEIGHT: "FIELD_HEIGHT";
|
|
174
182
|
readonly FIELD_THREE_DS_REQUIRED: "FIELD_THREE_DS_REQUIRED";
|
|
175
183
|
readonly FIELD_THREE_DS_RESULT: "FIELD_THREE_DS_RESULT";
|
|
184
|
+
readonly FIELD_PAYMENT_ATTEMPT: "FIELD_PAYMENT_ATTEMPT";
|
|
185
|
+
readonly FIELD_POSTAL_CODE_REQUIREMENT: "FIELD_POSTAL_CODE_REQUIREMENT";
|
|
176
186
|
};
|
|
177
187
|
type TFieldEventType = (typeof FieldEventType)[keyof typeof FieldEventType];
|
|
178
188
|
interface FieldOptions {
|
|
@@ -220,7 +230,7 @@ interface ExpressCapability {
|
|
|
220
230
|
googlePay: boolean;
|
|
221
231
|
paypal: boolean;
|
|
222
232
|
}
|
|
223
|
-
interface
|
|
233
|
+
interface AttemptEvent {
|
|
224
234
|
event: TPaymentAttemptResult;
|
|
225
235
|
method: TPaymentMethod;
|
|
226
236
|
paymentId?: string;
|
|
@@ -240,6 +250,15 @@ interface FlintNFieldsOptions {
|
|
|
240
250
|
onChange?: (event: FieldChangeEvent) => void;
|
|
241
251
|
onFocus?: (fieldType: TFieldType) => void;
|
|
242
252
|
onBlur?: (fieldType: TFieldType, state: FieldState) => void;
|
|
253
|
+
/** Per-attempt analytics for the card flow (express buttons have their own). */
|
|
254
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
255
|
+
/**
|
|
256
|
+
* The entered card BIN requires (or stops requiring) a postal code. Show
|
|
257
|
+
* your own postal field and pass its value via submit's
|
|
258
|
+
* `billingAddress.postalCode` — submit fails with POSTAL_CODE_REQUIRED
|
|
259
|
+
* otherwise.
|
|
260
|
+
*/
|
|
261
|
+
onPostalCodeRequirement?: (required: boolean) => void;
|
|
243
262
|
onError?: (error: PaymentError) => void;
|
|
244
263
|
debug?: boolean;
|
|
245
264
|
}
|
|
@@ -293,7 +312,7 @@ interface FlintNExpressButtonsOptions {
|
|
|
293
312
|
apiUrl?: string;
|
|
294
313
|
buttonBorderRadius?: number;
|
|
295
314
|
onPayment?: (result: PaymentResult) => void;
|
|
296
|
-
onEvent?: (event:
|
|
315
|
+
onEvent?: (event: AttemptEvent) => void;
|
|
297
316
|
onCapability?: (capability: ExpressCapability) => void;
|
|
298
317
|
onError?: (error: PaymentError) => void;
|
|
299
318
|
debug?: boolean;
|
|
@@ -349,8 +368,11 @@ interface AuthorizeResponse {
|
|
|
349
368
|
};
|
|
350
369
|
status_reason?: {
|
|
351
370
|
decline_reason?: string;
|
|
371
|
+
decline_code?: string;
|
|
372
|
+
decline_type?: 'SOFT_DECLINE' | 'HARD_DECLINE';
|
|
352
373
|
message?: string;
|
|
353
374
|
};
|
|
375
|
+
session_retryable?: boolean;
|
|
354
376
|
}
|
|
355
377
|
interface ApplePayToken {
|
|
356
378
|
payment_data: {
|
|
@@ -377,7 +399,9 @@ interface GooglePayToken {
|
|
|
377
399
|
interface WalletAuthorizeHandlers {
|
|
378
400
|
onAuthorized: (paymentId: string) => void;
|
|
379
401
|
onThreeDs: (redirectUrl: string, paymentId: string) => void;
|
|
380
|
-
onFailed: (code: string, message: string, paymentId?: string
|
|
402
|
+
onFailed: (code: string, message: string, paymentId?: string,
|
|
403
|
+
/** Attempt analytics classification; defaults to HARD_DECLINE. */
|
|
404
|
+
event?: TPaymentAttemptResult) => void;
|
|
381
405
|
}
|
|
382
406
|
|
|
383
407
|
/**
|
|
@@ -425,4 +449,4 @@ declare const validateConfig: (config: {
|
|
|
425
449
|
}) => void;
|
|
426
450
|
declare const sanitizeToken: (token: string) => string;
|
|
427
451
|
|
|
428
|
-
export { type ApplePayConfig, type ApplePayToken, type AuthorizeResponse, AuthorizedStatus, type CheckoutEvent, CheckoutFormVariant, CheckoutView, EXPRESS_BUTTON_HEIGHT, EventType, type
|
|
452
|
+
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
|
|
@@ -583,15 +590,24 @@ var ExpressApi = class {
|
|
|
583
590
|
} else if (res.status === AuthorizedStatus.THREE_DS_INITIATED && res.required_action?.type === "REDIRECT_TO_URL") {
|
|
584
591
|
handlers.onThreeDs(res.required_action.redirect_url, res.id);
|
|
585
592
|
} else {
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
593
|
+
const fallback = res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED";
|
|
594
|
+
const code = declineCodeOf(res.status_reason?.decline_code, fallback);
|
|
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 ?? (fallback === "PAYMENT_DECLINED" ? "Payment declined." : "Payment failed."),
|
|
599
|
+
res.id,
|
|
600
|
+
res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
|
|
589
601
|
);
|
|
590
|
-
handlers.onFailed(code, res.status_reason?.message ?? code, res.id);
|
|
591
602
|
}
|
|
592
603
|
} catch (err) {
|
|
593
604
|
const code = errorCodeOf(err, "UNKNOWN_ERROR");
|
|
594
|
-
handlers.onFailed(
|
|
605
|
+
handlers.onFailed(
|
|
606
|
+
code,
|
|
607
|
+
err instanceof Error ? err.message : code,
|
|
608
|
+
void 0,
|
|
609
|
+
PaymentAttemptResult.NETWORK_ERROR
|
|
610
|
+
);
|
|
595
611
|
}
|
|
596
612
|
}
|
|
597
613
|
/**
|
|
@@ -1115,7 +1131,12 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1115
1131
|
if (!check.expectedSource || event.source !== check.expectedSource) {
|
|
1116
1132
|
return null;
|
|
1117
1133
|
}
|
|
1118
|
-
const {
|
|
1134
|
+
const {
|
|
1135
|
+
type,
|
|
1136
|
+
payment_id: paymentId,
|
|
1137
|
+
status,
|
|
1138
|
+
decline_code: declineCode
|
|
1139
|
+
} = event.data ?? {};
|
|
1119
1140
|
if (type !== "3ds-result" || !status || paymentId !== check.expectedPaymentId) {
|
|
1120
1141
|
return null;
|
|
1121
1142
|
}
|
|
@@ -1129,7 +1150,10 @@ var extractThreeDsResult = (event, check) => {
|
|
|
1129
1150
|
);
|
|
1130
1151
|
return null;
|
|
1131
1152
|
}
|
|
1132
|
-
return
|
|
1153
|
+
return {
|
|
1154
|
+
status: String(status),
|
|
1155
|
+
declineCode: typeof declineCode === "string" && declineCode ? declineCode : void 0
|
|
1156
|
+
};
|
|
1133
1157
|
};
|
|
1134
1158
|
|
|
1135
1159
|
// src/express/three-ds.ts
|
|
@@ -1152,16 +1176,16 @@ var createThreeDsPopup = (payOrigin, handlers) => {
|
|
|
1152
1176
|
expectedPaymentId = null;
|
|
1153
1177
|
};
|
|
1154
1178
|
const handleMessage = (event) => {
|
|
1155
|
-
const
|
|
1179
|
+
const result = extractThreeDsResult(event, {
|
|
1156
1180
|
expectedSource: popup,
|
|
1157
1181
|
allowedOrigins,
|
|
1158
1182
|
expectedPaymentId
|
|
1159
1183
|
});
|
|
1160
|
-
if (
|
|
1184
|
+
if (result === null || handled) return;
|
|
1161
1185
|
const paymentId = expectedPaymentId;
|
|
1162
1186
|
handled = true;
|
|
1163
1187
|
cleanup();
|
|
1164
|
-
handlers.onComplete(paymentId, status);
|
|
1188
|
+
handlers.onComplete(paymentId, result.status, result.declineCode);
|
|
1165
1189
|
};
|
|
1166
1190
|
window.addEventListener("message", handleMessage);
|
|
1167
1191
|
const open = (redirectUrl, paymentId) => {
|
|
@@ -1272,24 +1296,28 @@ function createFlintNExpressButtons(options) {
|
|
|
1272
1296
|
};
|
|
1273
1297
|
let threeDsMethod = ExpressMethod.APPLE_PAY;
|
|
1274
1298
|
const threeDs = createThreeDsPopup(payOrigin, {
|
|
1275
|
-
onComplete: (paymentId, status) => {
|
|
1299
|
+
onComplete: (paymentId, status, declineCode) => {
|
|
1276
1300
|
if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
|
|
1277
|
-
else
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
{ paymentId }
|
|
1283
|
-
);
|
|
1301
|
+
else {
|
|
1302
|
+
const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
|
|
1303
|
+
const message = status === "DECLINED" ? "Payment declined." : "Payment failed.";
|
|
1304
|
+
fail(threeDsMethod, code, message, { paymentId });
|
|
1305
|
+
}
|
|
1284
1306
|
},
|
|
1285
|
-
onClose: () => fail(
|
|
1286
|
-
|
|
1287
|
-
|
|
1307
|
+
onClose: () => fail(
|
|
1308
|
+
threeDsMethod,
|
|
1309
|
+
"THREE_DS_CANCELLED",
|
|
1310
|
+
"3DS verification was cancelled by the buyer.",
|
|
1311
|
+
{ event: PaymentAttemptResult.CANCELLED }
|
|
1312
|
+
),
|
|
1288
1313
|
// Routed through fail() so analytics get a terminal attempt event too
|
|
1289
1314
|
// (NETWORK_ERROR — a client-side technical failure, not a decline).
|
|
1290
|
-
onPopupBlocked: () => fail(
|
|
1291
|
-
|
|
1292
|
-
|
|
1315
|
+
onPopupBlocked: () => fail(
|
|
1316
|
+
threeDsMethod,
|
|
1317
|
+
"THREE_DS_POPUP_BLOCKED",
|
|
1318
|
+
"The 3DS verification window was blocked by the browser.",
|
|
1319
|
+
{ event: PaymentAttemptResult.NETWORK_ERROR }
|
|
1320
|
+
)
|
|
1293
1321
|
});
|
|
1294
1322
|
const walletHandlers = (method) => (complete) => ({
|
|
1295
1323
|
onAuthorized: (paymentId) => {
|
|
@@ -1306,9 +1334,9 @@ function createFlintNExpressButtons(options) {
|
|
|
1306
1334
|
threeDsMethod = method;
|
|
1307
1335
|
threeDs.open(redirectUrl, paymentId);
|
|
1308
1336
|
},
|
|
1309
|
-
onFailed: (code, message, paymentId) => {
|
|
1337
|
+
onFailed: (code, message, paymentId, event) => {
|
|
1310
1338
|
complete(false, message);
|
|
1311
|
-
fail(method, code, message, { paymentId });
|
|
1339
|
+
fail(method, code, message, { paymentId, event });
|
|
1312
1340
|
}
|
|
1313
1341
|
});
|
|
1314
1342
|
const detection = (async () => {
|
|
@@ -1531,7 +1559,7 @@ var openThreeDsDialog = (options) => {
|
|
|
1531
1559
|
}
|
|
1532
1560
|
};
|
|
1533
1561
|
let finished = false;
|
|
1534
|
-
const finish = (status) => {
|
|
1562
|
+
const finish = (status, declineCode) => {
|
|
1535
1563
|
if (finished) return;
|
|
1536
1564
|
finished = true;
|
|
1537
1565
|
window.removeEventListener("message", handleMessage);
|
|
@@ -1539,15 +1567,15 @@ var openThreeDsDialog = (options) => {
|
|
|
1539
1567
|
overlay.remove();
|
|
1540
1568
|
closeCurrent = null;
|
|
1541
1569
|
previouslyFocused?.focus();
|
|
1542
|
-
options.onFinish(status);
|
|
1570
|
+
options.onFinish(status, declineCode);
|
|
1543
1571
|
};
|
|
1544
1572
|
const handleMessage = (event) => {
|
|
1545
|
-
const
|
|
1573
|
+
const result = extractThreeDsResult(event, {
|
|
1546
1574
|
expectedSource: iframe.contentWindow,
|
|
1547
1575
|
allowedOrigins: options.allowedOrigins,
|
|
1548
1576
|
expectedPaymentId: options.paymentId
|
|
1549
1577
|
});
|
|
1550
|
-
if (
|
|
1578
|
+
if (result !== null) finish(result.status, result.declineCode);
|
|
1551
1579
|
};
|
|
1552
1580
|
closeButton.addEventListener("click", () => finish("CANCELLED"));
|
|
1553
1581
|
window.addEventListener("message", handleMessage);
|
|
@@ -1683,6 +1711,26 @@ function createFlintNFields(options) {
|
|
|
1683
1711
|
}
|
|
1684
1712
|
return;
|
|
1685
1713
|
}
|
|
1714
|
+
if (type === FieldEventType.FIELD_PAYMENT_ATTEMPT) {
|
|
1715
|
+
log("Attempt event:", payload);
|
|
1716
|
+
const { event: attemptEvent, paymentId, code, message } = payload ?? {};
|
|
1717
|
+
if (attemptEvent) {
|
|
1718
|
+
options.onEvent?.({
|
|
1719
|
+
event: attemptEvent,
|
|
1720
|
+
method: PaymentMethod.PAYMENT_CARD,
|
|
1721
|
+
paymentId,
|
|
1722
|
+
code,
|
|
1723
|
+
message,
|
|
1724
|
+
timestamp: Date.now()
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
if (type === FieldEventType.FIELD_POSTAL_CODE_REQUIREMENT) {
|
|
1730
|
+
log("Postal code requirement:", payload);
|
|
1731
|
+
options.onPostalCodeRequirement?.(!!payload?.required);
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1686
1734
|
if (type === FieldEventType.FIELD_VALIDATION && fieldType) {
|
|
1687
1735
|
log("FIELD_VALIDATION received:", fieldType, payload);
|
|
1688
1736
|
const pending = pendingValidations.get(fieldType);
|
|
@@ -1710,11 +1758,12 @@ function createFlintNFields(options) {
|
|
|
1710
1758
|
redirectUrl,
|
|
1711
1759
|
paymentId,
|
|
1712
1760
|
allowedOrigins: threeDsResultOrigins(origin, apiUrl),
|
|
1713
|
-
onFinish: (status) => {
|
|
1761
|
+
onFinish: (status, declineCode) => {
|
|
1714
1762
|
log("3DS challenge finished:", status);
|
|
1715
1763
|
fields.get(fieldType)?._sendMessage(FieldEventType.FIELD_THREE_DS_RESULT, {
|
|
1716
1764
|
paymentId,
|
|
1717
|
-
status
|
|
1765
|
+
status,
|
|
1766
|
+
declineCode
|
|
1718
1767
|
});
|
|
1719
1768
|
}
|
|
1720
1769
|
});
|