flintn-checkout 0.0.9 → 0.0.11

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
@@ -6,12 +6,14 @@ FlintN Payment SDK — Embed payment forms via iframe checkout or headless hoste
6
6
  ```bash
7
7
  npm install flintn-checkout
8
8
  ```
9
+ > **Note:** for local/non-production use, pass `origin` (e.g. `origin: 'http://localhost:3000'`).
9
10
 
10
11
  ## Iframe Checkout
11
12
 
12
13
  Full checkout UI rendered inside a single iframe. Includes card form, express payments (Apple Pay, Google Pay, PayPal), and 3DS handling.
13
14
 
14
- ### React
15
+ **React**
16
+
15
17
  ```tsx
16
18
  import { useFlintNPayment } from 'flintn-checkout/react';
17
19
 
@@ -37,11 +39,12 @@ function Checkout() {
37
39
  }
38
40
  ```
39
41
 
40
- ### Vanilla JavaScript
42
+ **Vanilla JavaScript**
43
+
41
44
  ```javascript
42
- import { FlintNPayment } from 'flintn-checkout';
45
+ import { createFlintNPayment } from 'flintn-checkout';
43
46
 
44
- const payment = new FlintNPayment({
47
+ const payment = createFlintNPayment({
45
48
  config: {
46
49
  clientSessionId: 'your_client_session_id',
47
50
  },
@@ -60,11 +63,12 @@ const payment = new FlintNPayment({
60
63
 
61
64
  payment.mount('#payment-container');
62
65
 
63
- // Cleanup when done
64
- payment.unmount();
66
+ // Later, when you want to tear the widget down (e.g. on route change):
67
+ // payment.unmount();
65
68
  ```
66
69
 
67
- ### HTML
70
+ **HTML**
71
+
68
72
  ```html
69
73
  <!DOCTYPE html>
70
74
  <html>
@@ -75,9 +79,9 @@ payment.unmount();
75
79
  <div id="payment-container" style="max-width: 440px; margin: 0 auto;"></div>
76
80
 
77
81
  <script type="module">
78
- import { FlintNPayment } from 'flintn-checkout';
82
+ import { createFlintNPayment } from 'flintn-checkout';
79
83
 
80
- const payment = new FlintNPayment({
84
+ const payment = createFlintNPayment({
81
85
  config: {
82
86
  clientSessionId: 'your_client_session_id',
83
87
  },
@@ -107,7 +111,7 @@ Do **not** set a fixed `height` on the container — it will leave empty space b
107
111
  | Option | Type | Required | Default | Description |
108
112
  |--------|------|----------|---------|-------------|
109
113
  | `config` | `FlintNConfig` | Yes | — | Checkout configuration |
110
- | `onPayment` | `(result: PaymentResult) => void` | No | — | Card payment result callback |
114
+ | `onPayment` | `(result: PaymentResult) => void` | No | — | Payment result callback (success, error, cancelled) |
111
115
  | `onReady` | `() => void` | No | — | Widget loaded and ready |
112
116
  | `onError` | `(error: PaymentError) => void` | No | — | SDK initialization error |
113
117
  | `debug` | `boolean` | No | `false` | Enable console debug logs |
@@ -138,7 +142,8 @@ Individual PCI-compliant input fields rendered as separate iframes. You control
138
142
 
139
143
  Each field (card number, expiry, CVV) is a separate iframe. Raw card data never touches your page.
140
144
 
141
- ### React
145
+ **React**
146
+
142
147
  ```tsx
143
148
  import { useState } from 'react';
144
149
  import { useFlintNFields } from 'flintn-checkout/react';
@@ -223,11 +228,12 @@ function CheckoutForm() {
223
228
  }
224
229
  ```
225
230
 
226
- ### Vanilla JavaScript
231
+ **Vanilla JavaScript**
232
+
227
233
  ```javascript
228
- import { FlintNFields } from 'flintn-checkout';
234
+ import { createFlintNFields } from 'flintn-checkout';
229
235
 
230
- const fields = new FlintNFields({
236
+ const fields = createFlintNFields({
231
237
  config: {
232
238
  clientSessionId: 'your_client_session_id',
233
239
  styles: {
@@ -255,8 +261,8 @@ if (validation.isValid) {
255
261
  const result = await fields.submit({ cardholderName: 'John Doe' });
256
262
  }
257
263
 
258
- // Cleanup
259
- fields.unmount();
264
+ // Cleanup (call when you're done with the form)
265
+ // fields.unmount();
260
266
  ```
261
267
 
262
268
  ### Hosted Fields Options (Vanilla JS)
@@ -494,15 +500,6 @@ useFlintNFields({
494
500
  });
495
501
  ```
496
502
 
497
- ## Test Cards
498
-
499
- | Card Number | Result |
500
- |-------------|--------|
501
- | `4111 1111 1111 1111` | Success |
502
- | `4000 0000 0000 0002` | Declined |
503
-
504
- Use any future expiry date and any 3-digit CVV.
505
-
506
503
  ## Browser Support
507
504
 
508
505
  - Chrome (latest)
package/dist/index.d.mts CHANGED
@@ -2,7 +2,6 @@ declare const EventType: {
2
2
  readonly WIDGET_READY: "WIDGET_READY";
3
3
  readonly WIDGET_CONFIG: "WIDGET_CONFIG";
4
4
  readonly PAYMENT: "PAYMENT";
5
- readonly EXPRESS_PAYMENT: "EXPRESS_PAYMENT";
6
5
  readonly PAYMENT_SUCCESS: "PAYMENT_SUCCESS";
7
6
  readonly PAYMENT_ERROR: "PAYMENT_ERROR";
8
7
  readonly PAYMENT_CANCELLED: "PAYMENT_CANCELLED";
@@ -62,7 +61,6 @@ interface FlintNPaymentOptions {
62
61
  origin?: string;
63
62
  config: FlintNConfig;
64
63
  onPayment?: (result: PaymentResult) => void;
65
- onExpressPayment?: (result: PaymentResult) => void;
66
64
  onReady?: () => void;
67
65
  onError?: (error: PaymentError) => void;
68
66
  debug?: boolean;
@@ -147,6 +145,7 @@ interface FlintNFieldInternalCallbacks {
147
145
  onFocus: (fieldType: TFieldType) => void;
148
146
  onBlur: (fieldType: TFieldType, state: FieldState) => void;
149
147
  onHeight: (fieldType: TFieldType, height: number) => void;
148
+ onError?: (err: Error) => void;
150
149
  }
151
150
  interface FlintNField {
152
151
  mount(selector: string | HTMLElement): void;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ declare const EventType: {
2
2
  readonly WIDGET_READY: "WIDGET_READY";
3
3
  readonly WIDGET_CONFIG: "WIDGET_CONFIG";
4
4
  readonly PAYMENT: "PAYMENT";
5
- readonly EXPRESS_PAYMENT: "EXPRESS_PAYMENT";
6
5
  readonly PAYMENT_SUCCESS: "PAYMENT_SUCCESS";
7
6
  readonly PAYMENT_ERROR: "PAYMENT_ERROR";
8
7
  readonly PAYMENT_CANCELLED: "PAYMENT_CANCELLED";
@@ -62,7 +61,6 @@ interface FlintNPaymentOptions {
62
61
  origin?: string;
63
62
  config: FlintNConfig;
64
63
  onPayment?: (result: PaymentResult) => void;
65
- onExpressPayment?: (result: PaymentResult) => void;
66
64
  onReady?: () => void;
67
65
  onError?: (error: PaymentError) => void;
68
66
  debug?: boolean;
@@ -147,6 +145,7 @@ interface FlintNFieldInternalCallbacks {
147
145
  onFocus: (fieldType: TFieldType) => void;
148
146
  onBlur: (fieldType: TFieldType, state: FieldState) => void;
149
147
  onHeight: (fieldType: TFieldType, height: number) => void;
148
+ onError?: (err: Error) => void;
150
149
  }
151
150
  interface FlintNField {
152
151
  mount(selector: string | HTMLElement): void;
package/dist/index.js CHANGED
@@ -40,7 +40,6 @@ var EventType = {
40
40
  WIDGET_READY: "WIDGET_READY",
41
41
  WIDGET_CONFIG: "WIDGET_CONFIG",
42
42
  PAYMENT: "PAYMENT",
43
- EXPRESS_PAYMENT: "EXPRESS_PAYMENT",
44
43
  PAYMENT_SUCCESS: "PAYMENT_SUCCESS",
45
44
  PAYMENT_ERROR: "PAYMENT_ERROR",
46
45
  PAYMENT_CANCELLED: "PAYMENT_CANCELLED",
@@ -112,15 +111,38 @@ var sanitizeToken = (token) => {
112
111
  // src/flintn-payment.ts
113
112
  var DEFAULT_ORIGIN = "https://pay.flintn.com";
114
113
  function createFlintNPayment(options) {
115
- validateConfig(options.config);
116
- const origin = parseOrigin(options.origin || DEFAULT_ORIGIN);
114
+ const log = (...args) => {
115
+ if (options.debug) console.log("[FlintN SDK]", ...args);
116
+ };
117
+ const reportInitError = (err) => {
118
+ const error = {
119
+ code: "INIT_ERROR",
120
+ message: err instanceof Error ? err.message : "Failed to initialize SDK"
121
+ };
122
+ queueMicrotask(() => {
123
+ if (options.onError) options.onError(error);
124
+ else console.error("[FlintN SDK]", error);
125
+ });
126
+ };
127
+ let origin;
128
+ try {
129
+ validateConfig(options.config);
130
+ origin = parseOrigin(options.origin || DEFAULT_ORIGIN);
131
+ } catch (err) {
132
+ reportInitError(err);
133
+ return {
134
+ mount() {
135
+ },
136
+ unmount() {
137
+ },
138
+ getIsReady: () => false
139
+ };
140
+ }
117
141
  let iframe = null;
118
142
  let container = null;
119
143
  let isReady = false;
120
144
  let messageHandler = null;
121
- const log = (...args) => {
122
- if (options.debug) console.log("[FlintN SDK]", ...args);
123
- };
145
+ let iframeErrorHandler = null;
124
146
  log("Initialized with origin:", origin);
125
147
  const sendConfig = () => {
126
148
  if (!iframe?.contentWindow) return;
@@ -130,10 +152,6 @@ function createFlintNPayment(options) {
130
152
  );
131
153
  log("Sent config:", sanitizeToken(options.config.clientSessionId));
132
154
  };
133
- const handlePaymentResult = (payload, callback) => {
134
- if (!callback) return;
135
- callback(payload);
136
- };
137
155
  const isValidRedirectUrl = (url) => {
138
156
  try {
139
157
  const parsed = new URL(url);
@@ -166,10 +184,7 @@ function createFlintNPayment(options) {
166
184
  options.onReady?.();
167
185
  break;
168
186
  case EventType.PAYMENT:
169
- handlePaymentResult(payload, options.onPayment);
170
- break;
171
- case EventType.EXPRESS_PAYMENT:
172
- handlePaymentResult(payload, options.onExpressPayment);
187
+ options.onPayment?.(payload);
173
188
  break;
174
189
  case EventType.REDIRECT:
175
190
  if (payload?.url && isValidRedirectUrl(payload.url)) {
@@ -186,12 +201,13 @@ function createFlintNPayment(options) {
186
201
  const mount = (selector) => {
187
202
  container = typeof selector === "string" ? document.querySelector(selector) : selector;
188
203
  if (!container) {
189
- const selectorDescription = typeof selector === "string" ? selector : `<${selector.tagName.toLowerCase()}${selector.id ? ` id="${selector.id}"` : ""}${selector.className ? ` class="${selector.className}"` : ""}>`;
190
- throw new Error(
191
- `[FlintN SDK] Container not found: ${selectorDescription}`
192
- );
204
+ const description = typeof selector === "string" ? selector : `<${selector.tagName.toLowerCase()}>`;
205
+ reportInitError(new Error(`Container not found: ${description}`));
206
+ return;
193
207
  }
194
208
  iframe = createIframeElement(buildIframeSrc(origin));
209
+ iframeErrorHandler = () => reportInitError(new Error("Failed to load checkout iframe"));
210
+ iframe.addEventListener("error", iframeErrorHandler);
195
211
  container.appendChild(iframe);
196
212
  messageHandler = handleMessage;
197
213
  window.addEventListener("message", messageHandler);
@@ -202,15 +218,19 @@ function createFlintNPayment(options) {
202
218
  window.removeEventListener("message", messageHandler);
203
219
  messageHandler = null;
204
220
  }
221
+ if (iframe && iframeErrorHandler) {
222
+ iframe.removeEventListener("error", iframeErrorHandler);
223
+ iframeErrorHandler = null;
224
+ }
205
225
  if (iframe && container && container.contains(iframe)) {
206
226
  container.removeChild(iframe);
207
227
  }
208
228
  iframe = null;
229
+ container = null;
209
230
  isReady = false;
210
231
  log("Unmounted");
211
232
  };
212
- const getIsReady = () => isReady;
213
- return { mount, unmount, getIsReady };
233
+ return { mount, unmount, getIsReady: () => isReady };
214
234
  }
215
235
 
216
236
  // src/flintn-field.ts
@@ -221,6 +241,7 @@ var FIELD_TITLES = {
221
241
  };
222
242
  function createFlintNField(fieldType, origin, clientSessionId, options, callbacks, debug = false, formStyles) {
223
243
  let iframe = null;
244
+ let iframeErrorHandler = null;
224
245
  let container = null;
225
246
  let state = {
226
247
  isEmpty: true,
@@ -231,6 +252,14 @@ function createFlintNField(fieldType, origin, clientSessionId, options, callback
231
252
  const log = (...args) => {
232
253
  if (debug) console.log(`[FlintN SDK][${fieldType}]`, ...args);
233
254
  };
255
+ const reportError = (err) => {
256
+ const error = err instanceof Error ? err : new Error(String(err));
257
+ if (callbacks.onError) {
258
+ queueMicrotask(() => callbacks.onError(error));
259
+ } else {
260
+ console.error(`[FlintN SDK][${fieldType}]`, error);
261
+ }
262
+ };
234
263
  const sendMessage = (type, payload) => {
235
264
  if (!iframe?.contentWindow) return;
236
265
  iframe.contentWindow.postMessage({ type, payload }, origin);
@@ -258,18 +287,35 @@ function createFlintNField(fieldType, origin, clientSessionId, options, callback
258
287
  const mount = (selector) => {
259
288
  container = typeof selector === "string" ? document.querySelector(selector) : selector;
260
289
  if (!container) {
261
- throw new Error(
262
- `[FlintN SDK] Field container not found for "${fieldType}": ${typeof selector === "string" ? selector : "element"}`
290
+ reportError(
291
+ new Error(
292
+ `Field container not found for "${fieldType}": ${typeof selector === "string" ? selector : "element"}`
293
+ )
263
294
  );
295
+ return;
264
296
  }
265
- const src = buildIframeSrc(origin) + fieldType;
266
- iframe = createIframeElement2(src);
297
+ try {
298
+ const src = buildIframeSrc(origin) + fieldType;
299
+ iframe = createIframeElement2(src);
300
+ } catch (err) {
301
+ reportError(err);
302
+ return;
303
+ }
304
+ iframeErrorHandler = () => {
305
+ reportError(new Error(`Failed to load ${fieldType} iframe`));
306
+ };
307
+ iframe.addEventListener("error", iframeErrorHandler);
267
308
  container.appendChild(iframe);
268
309
  log("Mounted", fieldType);
269
310
  };
270
311
  const unmount = () => {
312
+ if (iframe && iframeErrorHandler) {
313
+ iframe.removeEventListener("error", iframeErrorHandler);
314
+ iframeErrorHandler = null;
315
+ }
271
316
  if (iframe && container?.contains(iframe)) container.removeChild(iframe);
272
317
  iframe = null;
318
+ container = null;
273
319
  state = { isEmpty: true, isValid: false, isFocused: false, error: null };
274
320
  log("Unmounted", fieldType);
275
321
  };
@@ -333,11 +379,46 @@ function createFlintNField(fieldType, origin, clientSessionId, options, callback
333
379
  // src/flintn-fields.ts
334
380
  var DEFAULT_ORIGIN2 = "https://pay.flintn.com";
335
381
  var OPERATION_TIMEOUT = 3e4;
382
+ function createInertFields() {
383
+ const errorResult = {
384
+ status: "PAYMENT_ERROR",
385
+ error: { code: "INIT_ERROR", message: "SDK init failed" }
386
+ };
387
+ return {
388
+ createField: () => {
389
+ throw new Error(
390
+ "[FlintN SDK] Cannot create field \u2014 SDK init failed (see onError)"
391
+ );
392
+ },
393
+ getField: () => void 0,
394
+ validate: async () => ({ isValid: false, errors: {} }),
395
+ submit: async () => errorResult,
396
+ unmount: () => {
397
+ },
398
+ getIsReady: () => false
399
+ };
400
+ }
336
401
  function createFlintNFields(options) {
337
- if (!options.config?.clientSessionId) {
338
- throw new Error("[FlintN SDK] config.clientSessionId is required");
402
+ const reportInitError = (err) => {
403
+ const error = {
404
+ code: "INIT_ERROR",
405
+ message: err instanceof Error ? err.message : "Failed to initialize fields"
406
+ };
407
+ queueMicrotask(() => {
408
+ if (options.onError) options.onError(error);
409
+ else console.error("[FlintN SDK]", error);
410
+ });
411
+ };
412
+ let origin;
413
+ try {
414
+ if (!options.config?.clientSessionId) {
415
+ throw new Error("config.clientSessionId is required");
416
+ }
417
+ origin = parseOrigin(options.origin || DEFAULT_ORIGIN2);
418
+ } catch (err) {
419
+ reportInitError(err);
420
+ return createInertFields();
339
421
  }
340
- const origin = parseOrigin(options.origin || DEFAULT_ORIGIN2);
341
422
  const fields = /* @__PURE__ */ new Map();
342
423
  const readyFields = /* @__PURE__ */ new Set();
343
424
  let allReady = false;
@@ -438,7 +519,8 @@ function createFlintNFields(options) {
438
519
  onFocus: onFieldFocus,
439
520
  onBlur: onFieldBlur,
440
521
  onHeight: () => {
441
- }
522
+ },
523
+ onError: reportInitError
442
524
  },
443
525
  options.debug,
444
526
  options.config.styles
@@ -467,12 +549,22 @@ function createFlintNFields(options) {
467
549
  };
468
550
  const submit = async (submitOptions = {}, skipValidation = false) => {
469
551
  if (!fields.has(FieldType.CARD_NUMBER)) {
470
- throw new Error(
471
- '[FlintN SDK] card-number field not initialized. Call createField("card-number") before submit().'
472
- );
552
+ return {
553
+ status: "PAYMENT_ERROR",
554
+ error: {
555
+ code: "CARD_NUMBER_NOT_INITIALIZED",
556
+ message: 'card-number field not initialized. Call createField("card-number") before submit().'
557
+ }
558
+ };
473
559
  }
474
560
  if (pendingSubmit) {
475
- throw new Error("[FlintN SDK] A submit is already in progress");
561
+ return {
562
+ status: "PAYMENT_ERROR",
563
+ error: {
564
+ code: "SUBMIT_IN_PROGRESS",
565
+ message: "A submit is already in progress"
566
+ }
567
+ };
476
568
  }
477
569
  if (!skipValidation) {
478
570
  const validation = await validate();
@@ -489,17 +581,24 @@ function createFlintNFields(options) {
489
581
  return new Promise((resolve, reject) => {
490
582
  const timer = setTimeout(() => {
491
583
  if (pendingSubmit && pendingSubmit.timer === timer) {
492
- pendingSubmit.reject(new Error("[FlintN SDK] Submit timed out"));
584
+ const timeoutResult = {
585
+ status: "PAYMENT_ERROR",
586
+ error: {
587
+ code: "SUBMIT_TIMEOUT",
588
+ message: "Submit timed out"
589
+ }
590
+ };
591
+ pendingSubmit.resolve(timeoutResult);
592
+ options.onPayment?.(timeoutResult);
493
593
  pendingSubmit = null;
494
594
  }
495
595
  }, OPERATION_TIMEOUT);
496
596
  pendingSubmit = { resolve, reject, timer };
497
- for (const field of fields.values()) {
498
- field._sendMessage(FieldEventType.FIELD_SUBMIT, {
499
- clientSessionId: options.config.clientSessionId,
500
- cardholderName: submitOptions.cardholderName
501
- });
502
- }
597
+ const coordinator = fields.get(FieldType.CARD_NUMBER);
598
+ coordinator?._sendMessage(FieldEventType.FIELD_SUBMIT, {
599
+ clientSessionId: options.config.clientSessionId,
600
+ cardholderName: submitOptions.cardholderName
601
+ });
503
602
  log("Submit initiated");
504
603
  });
505
604
  };