flintn-checkout 0.0.18 → 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 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:
@@ -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
 
@@ -480,7 +571,8 @@ Validation mirrors react-hook-form `mode: 'onSubmit'`:
480
571
 
481
572
  ### Postal code (BIN-driven)
482
573
 
483
- Some issuers require a postal code for online payments. Once the buyer has
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
484
576
  typed enough digits, the card-number field checks the BIN against the payments
485
577
  API and reports whether a postal code is required — `postalCodeRequired`
486
578
  (React) / `onPostalCodeRequirement` (vanilla). Show your own postal code input
@@ -646,6 +738,8 @@ interface PaymentError {
646
738
 
647
739
  ```typescript
648
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.
649
743
  cardholderName?: string;
650
744
  email?: string;
651
745
  billingAddress?: {
@@ -914,7 +1008,10 @@ on `createField` (or the `fields` prop of `useFlintNFields`) instead.
914
1008
 
915
1009
  ## Form Variants
916
1010
 
917
- 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.
918
1015
 
919
1016
  - **DEFAULT** — Express payment methods shown above the card form with dividers.
920
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";
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";
package/dist/index.js CHANGED
@@ -590,14 +590,12 @@ var ExpressApi = class {
590
590
  } else if (res.status === AuthorizedStatus.THREE_DS_INITIATED && res.required_action?.type === "REDIRECT_TO_URL") {
591
591
  handlers.onThreeDs(res.required_action.redirect_url, res.id);
592
592
  } else {
593
- const code = declineCodeOf(
594
- res.status_reason?.decline_code,
595
- res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
596
- );
593
+ const fallback = res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED";
594
+ const code = declineCodeOf(res.status_reason?.decline_code, fallback);
597
595
  const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
598
596
  handlers.onFailed(
599
597
  code,
600
- res.status_reason?.message ?? code,
598
+ res.status_reason?.message ?? (fallback === "PAYMENT_DECLINED" ? "Payment declined." : "Payment failed."),
601
599
  res.id,
602
600
  res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
603
601
  );
@@ -1302,17 +1300,24 @@ function createFlintNExpressButtons(options) {
1302
1300
  if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
1303
1301
  else {
1304
1302
  const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
1305
- fail(threeDsMethod, code, code, { paymentId });
1303
+ const message = status === "DECLINED" ? "Payment declined." : "Payment failed.";
1304
+ fail(threeDsMethod, code, message, { paymentId });
1306
1305
  }
1307
1306
  },
1308
- onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
1309
- event: PaymentAttemptResult.CANCELLED
1310
- }),
1307
+ onClose: () => fail(
1308
+ threeDsMethod,
1309
+ "THREE_DS_CANCELLED",
1310
+ "3DS verification was cancelled by the buyer.",
1311
+ { event: PaymentAttemptResult.CANCELLED }
1312
+ ),
1311
1313
  // Routed through fail() so analytics get a terminal attempt event too
1312
1314
  // (NETWORK_ERROR — a client-side technical failure, not a decline).
1313
- onPopupBlocked: () => fail(threeDsMethod, "THREE_DS_POPUP_BLOCKED", "THREE_DS_POPUP_BLOCKED", {
1314
- event: PaymentAttemptResult.NETWORK_ERROR
1315
- })
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
+ )
1316
1321
  });
1317
1322
  const walletHandlers = (method) => (complete) => ({
1318
1323
  onAuthorized: (paymentId) => {
package/dist/index.mjs CHANGED
@@ -542,14 +542,12 @@ var ExpressApi = class {
542
542
  } else if (res.status === AuthorizedStatus.THREE_DS_INITIATED && res.required_action?.type === "REDIRECT_TO_URL") {
543
543
  handlers.onThreeDs(res.required_action.redirect_url, res.id);
544
544
  } else {
545
- const code = declineCodeOf(
546
- res.status_reason?.decline_code,
547
- res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED"
548
- );
545
+ const fallback = res.status === AuthorizedStatus.DECLINED ? "PAYMENT_DECLINED" : "PAYMENT_FAILED";
546
+ const code = declineCodeOf(res.status_reason?.decline_code, fallback);
549
547
  const isHard = res.status_reason?.decline_type === "HARD_DECLINE" || res.status_reason?.decline_type == null && res.session_retryable === false;
550
548
  handlers.onFailed(
551
549
  code,
552
- res.status_reason?.message ?? code,
550
+ res.status_reason?.message ?? (fallback === "PAYMENT_DECLINED" ? "Payment declined." : "Payment failed."),
553
551
  res.id,
554
552
  res.status === AuthorizedStatus.DECLINED && !isHard ? PaymentAttemptResult.SOFT_DECLINE : PaymentAttemptResult.HARD_DECLINE
555
553
  );
@@ -1254,17 +1252,24 @@ function createFlintNExpressButtons(options) {
1254
1252
  if (status === "AUTHORIZED") succeed(threeDsMethod, paymentId);
1255
1253
  else {
1256
1254
  const code = declineCode ?? (status === "DECLINED" ? "PAYMENT_DECLINED" : "PAYMENT_FAILED");
1257
- fail(threeDsMethod, code, code, { paymentId });
1255
+ const message = status === "DECLINED" ? "Payment declined." : "Payment failed.";
1256
+ fail(threeDsMethod, code, message, { paymentId });
1258
1257
  }
1259
1258
  },
1260
- onClose: () => fail(threeDsMethod, "THREE_DS_CANCELLED", "THREE_DS_CANCELLED", {
1261
- event: PaymentAttemptResult.CANCELLED
1262
- }),
1259
+ onClose: () => fail(
1260
+ threeDsMethod,
1261
+ "THREE_DS_CANCELLED",
1262
+ "3DS verification was cancelled by the buyer.",
1263
+ { event: PaymentAttemptResult.CANCELLED }
1264
+ ),
1263
1265
  // Routed through fail() so analytics get a terminal attempt event too
1264
1266
  // (NETWORK_ERROR — a client-side technical failure, not a decline).
1265
- onPopupBlocked: () => fail(threeDsMethod, "THREE_DS_POPUP_BLOCKED", "THREE_DS_POPUP_BLOCKED", {
1266
- event: PaymentAttemptResult.NETWORK_ERROR
1267
- })
1267
+ onPopupBlocked: () => fail(
1268
+ threeDsMethod,
1269
+ "THREE_DS_POPUP_BLOCKED",
1270
+ "The 3DS verification window was blocked by the browser.",
1271
+ { event: PaymentAttemptResult.NETWORK_ERROR }
1272
+ )
1268
1273
  });
1269
1274
  const walletHandlers = (method) => (complete) => ({
1270
1275
  onAuthorized: (paymentId) => {
package/dist/react.d.mts CHANGED
@@ -80,6 +80,14 @@ interface FlintNConfig {
80
80
  styles?: FormStyles;
81
81
  placeholders?: FormPlaceholders;
82
82
  successRedirectUrl?: string;
83
+ /**
84
+ * Whether the iframe renders express buttons (Apple Pay / Google Pay /
85
+ * PayPal) inside the checkout form. Default true. Set false to render the
86
+ * card form only and mount express buttons in your own page via
87
+ * `createFlintNExpressButtons` — note `formFields.formVariant` LIST/RADIO
88
+ * are ignored in that case.
89
+ */
90
+ expressButtons?: boolean;
83
91
  }
84
92
  declare const PaymentMethod: {
85
93
  readonly PAYMENT_CARD: "PAYMENT_CARD";
@@ -231,6 +239,18 @@ interface UseFlintNPaymentReturn {
231
239
  paymentResult: PaymentResult | null;
232
240
  events: Array<CheckoutEvent>;
233
241
  error: PaymentError | null;
242
+ /**
243
+ * Express-button mount points — used only with
244
+ * `config.expressButtons: false`: the iframe renders the card form alone
245
+ * and the hook mounts wallet buttons into these refs on the merchant page.
246
+ * Wallet results and attempt events arrive through the same
247
+ * `onPayment` / `onEvent` as the card form.
248
+ */
249
+ applePayRef: React.RefObject<HTMLDivElement | null>;
250
+ googlePayRef: React.RefObject<HTMLDivElement | null>;
251
+ payPalRef: React.RefObject<HTMLDivElement | null>;
252
+ /** True once at least one wallet is available on this device/session. */
253
+ expressAvailable: boolean;
234
254
  }
235
255
  declare function useFlintNPayment(options: UseFlintNPaymentOptions): UseFlintNPaymentReturn;
236
256
 
package/dist/react.d.ts CHANGED
@@ -80,6 +80,14 @@ interface FlintNConfig {
80
80
  styles?: FormStyles;
81
81
  placeholders?: FormPlaceholders;
82
82
  successRedirectUrl?: string;
83
+ /**
84
+ * Whether the iframe renders express buttons (Apple Pay / Google Pay /
85
+ * PayPal) inside the checkout form. Default true. Set false to render the
86
+ * card form only and mount express buttons in your own page via
87
+ * `createFlintNExpressButtons` — note `formFields.formVariant` LIST/RADIO
88
+ * are ignored in that case.
89
+ */
90
+ expressButtons?: boolean;
83
91
  }
84
92
  declare const PaymentMethod: {
85
93
  readonly PAYMENT_CARD: "PAYMENT_CARD";
@@ -231,6 +239,18 @@ interface UseFlintNPaymentReturn {
231
239
  paymentResult: PaymentResult | null;
232
240
  events: Array<CheckoutEvent>;
233
241
  error: PaymentError | null;
242
+ /**
243
+ * Express-button mount points — used only with
244
+ * `config.expressButtons: false`: the iframe renders the card form alone
245
+ * and the hook mounts wallet buttons into these refs on the merchant page.
246
+ * Wallet results and attempt events arrive through the same
247
+ * `onPayment` / `onEvent` as the card form.
248
+ */
249
+ applePayRef: React.RefObject<HTMLDivElement | null>;
250
+ googlePayRef: React.RefObject<HTMLDivElement | null>;
251
+ payPalRef: React.RefObject<HTMLDivElement | null>;
252
+ /** True once at least one wallet is available on this device/session. */
253
+ expressAvailable: boolean;
234
254
  }
235
255
  declare function useFlintNPayment(options: UseFlintNPaymentOptions): UseFlintNPaymentReturn;
236
256