@fat-zebra/sdk 2.0.6-beta.1 → 2.0.6

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/CHANGELOG.md CHANGED
@@ -1,6 +1,63 @@
1
- # Fat Zebra SDK
1
+ # Changelog
2
2
 
3
- ## Change log
3
+ All notable changes to the Fat Zebra JS SDK will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.6] - 2026-05-27
9
+
10
+ ### Added
11
+ - Improved reliability of the ThreeDSecure flow to prevent duplicate processing when a payment is retried or initiated more than once
12
+
13
+ ### Changed
14
+ - React SDK ThreeDSecure flow brought to parity with the core SDK
15
+
16
+ ## [2.0.5] - 2026-05-21
17
+
18
+ ### Fixed
19
+ - Fixed an issue where the device profiling step in the ThreeDSecure flow could trigger card enrollment multiple times if the device profile event fired more than once
20
+
21
+ ## [2.0.4] - 2026-05-20
22
+
23
+ ### Fixed
24
+ - Fixed an issue where triggering the ThreeDSecure flow multiple times (e.g. in a single-page application) could register duplicate event listeners, leading to multiple enrollment attempts
25
+
26
+ ## [2.0.3] - 2026-05-19
27
+
28
+ ### Fixed
29
+ - Fixed an issue in SPA environments where the Cardinal SCA `payments.validated` handler could accumulate across payment flows, causing stale callbacks to fire on subsequent transactions
30
+
31
+ ## [2.0.2] - 2026-05-14
32
+
33
+ ### Changed
34
+ - Default ThreeDSecure challenge window size changed from full-page to 500×600px
35
+
36
+ ### Fixed
37
+ - ThreeDSecure challenge window iframe errors are now correctly reported rather than silently ignored
38
+
39
+ ## [2.0.1] - 2026-05-11
40
+
41
+ ### Fixed
42
+ - Resolved an issue causing delayed rendering of the payment form in `renderPaymentsPage`
43
+ - Payment source is now correctly included in SDK tokenisation request events
44
+
45
+ ### Security
46
+ - Bumped axios from 1.13.5 to 1.16.0 to address known vulnerabilities
47
+
48
+ ## [2.0.0] - 2026-04-02
49
+
50
+ ### Changed
51
+ - **Upgraded 3DS2 authentication flow** — replaced the previous CyberSource Cardinal/Songbird library with a new SDK-native implementation backed directly by Fat Zebra's infrastructure. The new flow supports a configurable challenge window (250×400, 390×400, 500×600, 600×400, or full-page) and handles device fingerprinting internally. No changes are required to existing integrations; the correct flow is selected automatically.
52
+
53
+ ### Security
54
+ - Bumped handlebars from 4.7.8 to 4.7.9 to address known vulnerabilities
55
+ - Bumped axios to 1.15.0
56
+
57
+ ## [1.5.14] - 2026-03-10
58
+
59
+ ### Security
60
+ - Addressed known vulnerabilities in transitive dependencies (axios, minimatch, glob)
4
61
 
5
62
  ## [1.5.12] - 2025-10-14
6
63
  ## Updated
@@ -48,4 +105,3 @@ Support for 3-D Secure 2.x PARes Status: CHALLENGED
48
105
 
49
106
  ### Security
50
107
  - Patched routine package updates
51
-
@@ -1,5 +1,5 @@
1
1
  import axios from "axios";
2
- import { getFlowId, getLoggerUsername, getTransactionReference } from "./logger-context";
2
+ import { getFlowId, getIsReactSdk, getLoggerUsername, getTransactionReference } from "./logger-context";
3
3
  let loggerConfig = {};
4
4
  export function configureLogger(config) {
5
5
  loggerConfig = config;
@@ -40,6 +40,7 @@ export function instrumentFunction(methodName, fn, opts = {}) {
40
40
  username: getLoggerUsername(),
41
41
  reference: getTransactionReference(),
42
42
  flowId: getFlowId(),
43
+ reactSdk: getIsReactSdk(),
43
44
  className: opts.className,
44
45
  method: methodName,
45
46
  arguments: mappedArgs,
@@ -4,3 +4,5 @@ export declare function getLoggerUsername(): string | undefined;
4
4
  export declare function getTransactionReference(): string | undefined;
5
5
  export declare function getFlowId(): string | undefined;
6
6
  export declare function setFlowId(flowId: string | undefined): void;
7
+ export declare function setIsReactSdk(value: boolean): void;
8
+ export declare function getIsReactSdk(): boolean;
@@ -1,6 +1,7 @@
1
1
  let globalLoggerUsername;
2
2
  let globalTransactionReference;
3
3
  let globalFlowId;
4
+ let globalIsReactSdk = false;
4
5
  export function setLoggerUsername(username) {
5
6
  globalLoggerUsername = username;
6
7
  }
@@ -19,3 +20,9 @@ export function getFlowId() {
19
20
  export function setFlowId(flowId) {
20
21
  globalFlowId = flowId;
21
22
  }
23
+ export function setIsReactSdk(value) {
24
+ globalIsReactSdk = value;
25
+ }
26
+ export function getIsReactSdk() {
27
+ return globalIsReactSdk;
28
+ }
package/dist/main.js CHANGED
@@ -367,6 +367,18 @@ __decorate([
367
367
  __decorate([
368
368
  logMethod()
369
369
  ], FatZebra.prototype, "renderClickToPay", null);
370
+ __decorate([
371
+ logMethod({
372
+ mapArgs: (args) => {
373
+ return [
374
+ {
375
+ data: args[0],
376
+ payment_intent: args[1]
377
+ },
378
+ ];
379
+ },
380
+ })
381
+ ], FatZebra.prototype, "reportThreeDSecureFetched", null);
370
382
  __decorate([
371
383
  logMethod({
372
384
  mapArgs: (args) => {
@@ -0,0 +1,13 @@
1
+ import { Environment } from "../shared/env";
2
+ import type { TokenizeCardResponse, PaymentIntent } from "../shared/types";
3
+ type UseFatZebraLogMeta = {
4
+ environment: Environment;
5
+ reference: string;
6
+ username: string;
7
+ react_sdk: boolean;
8
+ };
9
+ declare const logUseFatZebra: (args_0: UseFatZebraLogMeta) => UseFatZebraLogMeta;
10
+ declare const logReportTokenizeCardResponse: (args_0: TokenizeCardResponse) => void;
11
+ declare const logReportThreeDSecureFetched: (data: boolean, paymentIntent?: PaymentIntent) => void;
12
+ declare const logReportRequestThreedsEnabledTimeout: (paymentIntent?: PaymentIntent) => void;
13
+ export { logUseFatZebra, logReportTokenizeCardResponse, logReportThreeDSecureFetched, logReportRequestThreedsEnabledTimeout, };
@@ -0,0 +1,36 @@
1
+ import { instrumentFunction } from "../logging/instrument";
2
+ import { setLoggerUsername, setTransactionReference, setFlowId, setIsReactSdk, } from "../logging/logger-context";
3
+ import env from "../shared/env";
4
+ const logUseFatZebra = instrumentFunction("useFatZebra", (meta) => meta, {
5
+ mapArgs: ([meta]) => {
6
+ setLoggerUsername(meta.username);
7
+ setTransactionReference(meta.reference);
8
+ setFlowId(undefined);
9
+ setIsReactSdk(true);
10
+ return meta;
11
+ },
12
+ className: "useFatZebra",
13
+ endpoint: (meta) => `${env[meta.environment].payNowUrl}/log_sdk`,
14
+ });
15
+ const logReportTokenizeCardResponse = instrumentFunction("reportTokenizeCardResponse", () => {
16
+ console.log("Tokenize card response received");
17
+ }, {
18
+ mapArgs: ([data]) => ({ token: data.token, bin: data.bin }),
19
+ className: "useMessage",
20
+ });
21
+ const logReportThreeDSecureFetched = instrumentFunction("reportThreeDSecureFetched", (data) => {
22
+ console.log("three_d_secure fetched", data);
23
+ }, {
24
+ mapArgs: ([data, paymentIntent]) => ({
25
+ data,
26
+ payment_intent: paymentIntent,
27
+ }),
28
+ className: "useMessage",
29
+ });
30
+ const logReportRequestThreedsEnabledTimeout = instrumentFunction("reportRequestThreedsEnabledTimeout", () => {
31
+ console.log("3DS enabled check timed out");
32
+ }, {
33
+ mapArgs: ([paymentIntent]) => ({ payment_intent: paymentIntent }),
34
+ className: "useMessage",
35
+ });
36
+ export { logUseFatZebra, logReportTokenizeCardResponse, logReportThreeDSecureFetched, logReportRequestThreedsEnabledTimeout, };
@@ -5,12 +5,9 @@ import Sca from "../sca";
5
5
  import GatewayClient from "../shared/api-gateway-client";
6
6
  import { generateVerifyURL } from "./verifyUrl";
7
7
  import useMessage from "./useMessage";
8
- import { configureLogger, instrumentFunction } from "../logging/instrument";
8
+ import { configureLogger } from "../logging/instrument";
9
9
  import env from "../shared/env";
10
- const logUseFatZebra = instrumentFunction("useFatZebra", (meta) => meta, {
11
- mapArgs: ([meta]) => meta,
12
- endpoint: (meta) => `${env[meta.environment].payNowUrl}/log_sdk`,
13
- });
10
+ import { logUseFatZebra } from "./logging";
14
11
  const useFatZebra = ({ config, handlers, cardToken }) => {
15
12
  const { options, accessToken, paymentIntent, username } = config;
16
13
  const sca = useMemo(() => {
@@ -7,17 +7,34 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
- import { useEffect, useRef, useState } from 'react';
10
+ import { useEffect, useRef } from 'react';
11
11
  import * as FatZebra from "../shared/types";
12
12
  import ThreeDSecure from "../three_d_secure";
13
13
  import * as bridge from "../shared/bridge-client";
14
14
  import env, { Environment } from "../shared/env";
15
+ import { PostMessageClient } from "../shared/post-message-client";
15
16
  import { BridgeEvent } from "../shared/types";
17
+ import { setFlowId } from '../logging/logger-context';
18
+ import { LocalStorageAccessTokenKey } from '../shared/constants';
19
+ import { logReportRequestThreedsEnabledTimeout, logReportThreeDSecureFetched, logReportTokenizeCardResponse } from './logging';
16
20
  const useMessage = ({ paymentIntent, options, handlers, sca, config }) => {
17
- const [threeDSecureEnabled, setThreeDSecureEnabled] = useState(false);
21
+ const isThreeDSecureEnabled = useRef(false);
18
22
  const threeDSecureRef = useRef(null);
19
- const successCallback = handlers[FatZebra.PublicEvent.SCA_SUCCESS];
20
- const failureCallback = handlers[FatZebra.PublicEvent.SCA_ERROR];
23
+ const headlessRef = useRef(null);
24
+ const initAuthClientRef = useRef(null);
25
+ const listenersClientRef = useRef(null);
26
+ // defaults to a resolved promise so handleTokenizeCardResponse doesn't hang if initAuth never runs (no config)
27
+ const authReadyRef = useRef(Promise.resolve());
28
+ // ThreeDSecure is instantiated once in a [] useEffect, so it would capture stale handler
29
+ // references if callbacks were passed directly. Instead we keep refs that are updated on
30
+ // every render, and pass stable wrapper functions so ThreeDSecure always invokes the
31
+ // latest callbacks without needing to be recreated.
32
+ const successCallbackRef = useRef(handlers[FatZebra.PublicEvent.SCA_SUCCESS]);
33
+ const failureCallbackRef = useRef(handlers[FatZebra.PublicEvent.SCA_ERROR]);
34
+ successCallbackRef.current = handlers[FatZebra.PublicEvent.SCA_SUCCESS];
35
+ failureCallbackRef.current = handlers[FatZebra.PublicEvent.SCA_ERROR];
36
+ const successCallback = (e) => { var _a; return (_a = successCallbackRef.current) === null || _a === void 0 ? void 0 : _a.call(successCallbackRef, e); };
37
+ const failureCallback = (e) => { var _a; return (_a = failureCallbackRef.current) === null || _a === void 0 ? void 0 : _a.call(failureCallbackRef, e); };
21
38
  const emit = (event, data) => {
22
39
  const handler = handlers[event];
23
40
  if (handler) {
@@ -33,28 +50,29 @@ const useMessage = ({ paymentIntent, options, handlers, sca, config }) => {
33
50
  });
34
51
  return;
35
52
  }
36
- else {
37
- emit(FatZebra.PublicEvent.TOKENIZATION_SUCCESS, {
38
- message: "Card tokenization success.",
39
- data,
40
- });
41
- }
42
- if (options && options.sca_enabled && threeDSecureRef.current && threeDSecureEnabled) {
43
- const iframe = document.querySelector('[name="renderIframe"]');
44
- const flowId = crypto.randomUUID();
45
- threeDSecureRef.current.run({
46
- paymentIntent,
47
- cardToken: data.token,
48
- merchantUsername: config.username,
49
- test: config.environment !== Environment.production,
50
- tokenizeOnly: true,
51
- iframe: iframe,
52
- flowId
53
- });
54
- return;
55
- }
56
- if (options && sca && options.sca_enabled) {
57
- yield sca.run({ paymentIntent, bin: data.bin, cardToken: data.token });
53
+ emit(FatZebra.PublicEvent.TOKENIZATION_SUCCESS, {
54
+ message: "Card tokenization success.",
55
+ data,
56
+ });
57
+ if (options && options.sca_enabled) {
58
+ yield authReadyRef.current;
59
+ if (threeDSecureRef.current && isThreeDSecureEnabled.current && config) {
60
+ const iframe = document.querySelector('[name="renderIframe"]');
61
+ const flowId = crypto.randomUUID();
62
+ setFlowId(flowId);
63
+ threeDSecureRef.current.run({
64
+ paymentIntent,
65
+ cardToken: data.token,
66
+ merchantUsername: config.username,
67
+ test: config.environment !== Environment.production,
68
+ tokenizeOnly: true,
69
+ iframe: iframe,
70
+ flowId
71
+ });
72
+ }
73
+ else if (sca) {
74
+ yield sca.run({ paymentIntent, bin: data.bin, cardToken: data.token });
75
+ }
58
76
  }
59
77
  });
60
78
  const messageHandler = (event) => __awaiter(void 0, void 0, void 0, function* () {
@@ -64,35 +82,6 @@ const useMessage = ({ paymentIntent, options, handlers, sca, config }) => {
64
82
  return;
65
83
  if (event.data.subject === FatZebra.BridgeEvent.BIN_LOOKUP)
66
84
  return;
67
- if (event.data.subject === FatZebra.BridgeEvent.THREE_D_SECURE_ENABLED) {
68
- setThreeDSecureEnabled(event.data.data);
69
- if (event.data.data) {
70
- const { el } = bridge.load2(env[config.environment].bridgeUrl);
71
- const headless = el;
72
- let onMessage;
73
- headless.onload = () => {
74
- const threeDS = new ThreeDSecure({
75
- successCallback,
76
- failureCallback,
77
- bridge: headless,
78
- environment: config.environment,
79
- });
80
- threeDSecureRef.current = threeDS;
81
- const messages = threeDS.messageHandlers(); // { [subject]: (payload) => void }
82
- onMessage = (ev) => {
83
- var _a, _b;
84
- const subject = (_a = ev === null || ev === void 0 ? void 0 : ev.data) === null || _a === void 0 ? void 0 : _a.subject;
85
- const payload = (_b = ev === null || ev === void 0 ? void 0 : ev.data) === null || _b === void 0 ? void 0 : _b.data;
86
- if (!subject)
87
- return;
88
- const handler = messages[subject];
89
- if (handler)
90
- handler(payload);
91
- };
92
- window.addEventListener("message", onMessage);
93
- };
94
- }
95
- }
96
85
  if (event.data.subject === FatZebra.BridgeEvent.FORM_VALIDATION_ERROR) {
97
86
  emit(FatZebra.PublicEvent.FORM_VALIDATION_ERROR, {
98
87
  errors: event.data.data,
@@ -108,20 +97,92 @@ const useMessage = ({ paymentIntent, options, handlers, sca, config }) => {
108
97
  return;
109
98
  }
110
99
  if (event.data.subject === FatZebra.BridgeEvent.TOKENIZE_CARD_RESPONSE) {
100
+ logReportTokenizeCardResponse(event.data.data);
111
101
  yield handleTokenizeCardResponse(event.data.data);
112
102
  }
113
103
  });
114
104
  useEffect(() => {
115
- const message = {
105
+ if (!config)
106
+ return;
107
+ const { el, isExisting } = bridge.load2(env[config.environment].bridgeUrl);
108
+ headlessRef.current = el;
109
+ const initAuth = () => {
110
+ const postMessageClient = new PostMessageClient({
111
+ channel: 'sca',
112
+ target: el,
113
+ environment: config.environment,
114
+ });
115
+ initAuthClientRef.current = postMessageClient;
116
+ let resolved = false;
117
+ let resolveAuthReady;
118
+ authReadyRef.current = new Promise((resolve) => { resolveAuthReady = resolve; });
119
+ postMessageClient.setEventListeners({
120
+ [BridgeEvent.THREE_D_SECURE_ENABLED]: (data) => {
121
+ const enabled = typeof data === 'boolean' ? data : false;
122
+ if (!resolved) {
123
+ logReportThreeDSecureFetched(enabled, paymentIntent);
124
+ resolved = true;
125
+ isThreeDSecureEnabled.current = enabled;
126
+ resolveAuthReady();
127
+ if (enabled) {
128
+ const threeDS = new ThreeDSecure({
129
+ successCallback,
130
+ failureCallback,
131
+ bridge: el,
132
+ environment: config.environment,
133
+ });
134
+ threeDSecureRef.current = threeDS;
135
+ const listenersClient = new PostMessageClient({
136
+ channel: 'sca',
137
+ target: el,
138
+ environment: config.environment,
139
+ });
140
+ listenersClient.setEventListeners(threeDS.messageHandlers());
141
+ listenersClientRef.current = listenersClient;
142
+ }
143
+ }
144
+ },
145
+ }, { once: true });
146
+ const message = {
147
+ channel: 'sca',
148
+ subject: 'fzi.is-enabled-three-d-secure-req',
149
+ data: {
150
+ merchant: config.username,
151
+ access_token: window.localStorage.getItem(LocalStorageAccessTokenKey),
152
+ },
153
+ };
154
+ postMessageClient.send(message);
155
+ window.setTimeout(() => {
156
+ if (!resolved) {
157
+ logReportRequestThreedsEnabledTimeout(paymentIntent);
158
+ resolved = true;
159
+ resolveAuthReady();
160
+ }
161
+ }, 500);
162
+ };
163
+ if (isExisting) {
164
+ initAuth();
165
+ }
166
+ else {
167
+ el.addEventListener('load', initAuth, { once: true });
168
+ }
169
+ const readyMessage = {
116
170
  channel: 'sca',
117
171
  subject: BridgeEvent.READY,
118
172
  data: {}
119
173
  };
120
174
  const iframe = document.querySelector('[name="renderIframe"]');
121
- if (!iframe)
122
- return;
123
- iframe.onload = () => {
124
- iframe.contentWindow.postMessage(message, '*');
175
+ if (iframe) {
176
+ iframe.onload = () => {
177
+ var _a;
178
+ (_a = iframe.contentWindow) === null || _a === void 0 ? void 0 : _a.postMessage(readyMessage, '*');
179
+ };
180
+ }
181
+ return () => {
182
+ var _a, _b;
183
+ el.removeEventListener('load', initAuth);
184
+ (_a = initAuthClientRef.current) === null || _a === void 0 ? void 0 : _a.removeEventListeners();
185
+ (_b = listenersClientRef.current) === null || _b === void 0 ? void 0 : _b.removeEventListeners();
125
186
  };
126
187
  }, []);
127
188
  useEffect(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fat-zebra/sdk",
3
- "version": "2.0.6-beta.1",
3
+ "version": "2.0.6",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {