@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.1e44255d
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 +58 -0
- package/cdn/bundle.js +3 -8
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -1400
- package/dist/actions.d.ts +3 -1400
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -13
- package/dist/bundle.d.cts +4 -1927
- package/dist/bundle.d.ts +4 -1927
- package/dist/bundle.js +1 -13
- package/dist/computeLegacyProductId-BkyJ4rEY.d.ts +538 -0
- package/dist/computeLegacyProductId-Raks6FXg.d.cts +538 -0
- package/dist/index.cjs +1 -13
- package/dist/index.d.cts +3 -1269
- package/dist/index.d.ts +3 -1269
- package/dist/index.js +1 -13
- package/dist/openSso-BCJGchIb.d.cts +1022 -0
- package/dist/openSso-DG-_9CED.d.ts +1022 -0
- package/dist/setupClient-Cfwpu08d.js +13 -0
- package/dist/setupClient-Dh8ljuhV.cjs +13 -0
- package/dist/siweAuthenticate-BH7Dn7nZ.d.cts +536 -0
- package/dist/siweAuthenticate-BJHbtty4.js +1 -0
- package/dist/siweAuthenticate-Btem4QHs.d.ts +536 -0
- package/dist/siweAuthenticate-Cwj3HP0m.cjs +1 -0
- package/dist/trackEvent-M2RLTQ2p.js +1 -0
- package/dist/trackEvent-T_R9ER2S.cjs +1 -0
- package/package.json +25 -31
- package/src/actions/displayEmbeddedWallet.test.ts +194 -0
- package/src/actions/displayEmbeddedWallet.ts +21 -0
- package/src/actions/displayModal.test.ts +388 -0
- package/src/actions/displayModal.ts +120 -0
- package/src/actions/ensureIdentity.ts +68 -0
- package/src/actions/getMerchantInformation.test.ts +116 -0
- package/src/actions/getMerchantInformation.ts +16 -0
- package/src/actions/index.ts +30 -0
- package/src/actions/openSso.ts +118 -0
- package/src/actions/prepareSso.test.ts +223 -0
- package/src/actions/prepareSso.ts +48 -0
- package/src/actions/referral/processReferral.test.ts +248 -0
- package/src/actions/referral/processReferral.ts +232 -0
- package/src/actions/referral/referralInteraction.test.ts +147 -0
- package/src/actions/referral/referralInteraction.ts +52 -0
- package/src/actions/sendInteraction.ts +56 -0
- package/src/actions/trackPurchaseStatus.test.ts +500 -0
- package/src/actions/trackPurchaseStatus.ts +90 -0
- package/src/actions/watchWalletStatus.test.ts +372 -0
- package/src/actions/watchWalletStatus.ts +93 -0
- package/src/actions/wrapper/modalBuilder.test.ts +239 -0
- package/src/actions/wrapper/modalBuilder.ts +203 -0
- package/src/actions/wrapper/sendTransaction.test.ts +164 -0
- package/src/actions/wrapper/sendTransaction.ts +62 -0
- package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
- package/src/actions/wrapper/siweAuthenticate.ts +94 -0
- package/src/bundle.ts +2 -0
- package/src/clients/DebugInfo.test.ts +418 -0
- package/src/clients/DebugInfo.ts +182 -0
- package/src/clients/createIFrameFrakClient.ts +292 -0
- package/src/clients/index.ts +3 -0
- package/src/clients/setupClient.test.ts +343 -0
- package/src/clients/setupClient.ts +73 -0
- package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
- package/src/clients/transports/iframeLifecycleManager.ts +229 -0
- package/src/constants/interactionTypes.ts +15 -0
- package/src/constants/locales.ts +14 -0
- package/src/index.ts +109 -0
- package/src/types/client.ts +14 -0
- package/src/types/compression.ts +22 -0
- package/src/types/config.ts +117 -0
- package/src/types/context.ts +13 -0
- package/src/types/index.ts +74 -0
- package/src/types/lifecycle/client.ts +69 -0
- package/src/types/lifecycle/iframe.ts +41 -0
- package/src/types/lifecycle/index.ts +2 -0
- package/src/types/rpc/displayModal.ts +82 -0
- package/src/types/rpc/embedded/index.ts +68 -0
- package/src/types/rpc/embedded/loggedIn.ts +55 -0
- package/src/types/rpc/embedded/loggedOut.ts +28 -0
- package/src/types/rpc/interaction.ts +30 -0
- package/src/types/rpc/merchantInformation.ts +77 -0
- package/src/types/rpc/modal/final.ts +46 -0
- package/src/types/rpc/modal/generic.ts +46 -0
- package/src/types/rpc/modal/index.ts +16 -0
- package/src/types/rpc/modal/login.ts +36 -0
- package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
- package/src/types/rpc/modal/transaction.ts +33 -0
- package/src/types/rpc/sso.ts +80 -0
- package/src/types/rpc/walletStatus.ts +29 -0
- package/src/types/rpc.ts +150 -0
- package/src/types/tracking.ts +60 -0
- package/src/types/transport.ts +34 -0
- package/src/utils/FrakContext.test.ts +407 -0
- package/src/utils/FrakContext.ts +158 -0
- package/src/utils/backendUrl.test.ts +83 -0
- package/src/utils/backendUrl.ts +62 -0
- package/src/utils/clientId.test.ts +41 -0
- package/src/utils/clientId.ts +43 -0
- package/src/utils/compression/b64.test.ts +181 -0
- package/src/utils/compression/b64.ts +29 -0
- package/src/utils/compression/compress.test.ts +123 -0
- package/src/utils/compression/compress.ts +11 -0
- package/src/utils/compression/decompress.test.ts +149 -0
- package/src/utils/compression/decompress.ts +11 -0
- package/src/utils/compression/index.ts +3 -0
- package/src/utils/computeLegacyProductId.ts +11 -0
- package/src/utils/constants.test.ts +23 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/deepLinkWithFallback.test.ts +243 -0
- package/src/utils/deepLinkWithFallback.ts +103 -0
- package/src/utils/formatAmount.test.ts +113 -0
- package/src/utils/formatAmount.ts +24 -0
- package/src/utils/getCurrencyAmountKey.test.ts +44 -0
- package/src/utils/getCurrencyAmountKey.ts +15 -0
- package/src/utils/getSupportedCurrency.test.ts +51 -0
- package/src/utils/getSupportedCurrency.ts +14 -0
- package/src/utils/getSupportedLocale.test.ts +64 -0
- package/src/utils/getSupportedLocale.ts +16 -0
- package/src/utils/iframeHelper.test.ts +463 -0
- package/src/utils/iframeHelper.ts +150 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/merchantId.test.ts +653 -0
- package/src/utils/merchantId.ts +143 -0
- package/src/utils/sso.ts +126 -0
- package/src/utils/ssoUrlListener.test.ts +252 -0
- package/src/utils/ssoUrlListener.ts +60 -0
- package/src/utils/trackEvent.test.ts +180 -0
- package/src/utils/trackEvent.ts +41 -0
- package/cdn/bundle.js.LICENSE.txt +0 -10
- package/dist/interactions.cjs +0 -1
- package/dist/interactions.d.cts +0 -182
- package/dist/interactions.d.ts +0 -182
- package/dist/interactions.js +0 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { DisplayEmbeddedWalletParamsType, FrakClient } from "../../types";
|
|
2
|
+
import { FrakContextManager } from "../../utils";
|
|
3
|
+
import { watchWalletStatus } from "../index";
|
|
4
|
+
import {
|
|
5
|
+
type ProcessReferralOptions,
|
|
6
|
+
processReferral,
|
|
7
|
+
} from "./processReferral";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Function used to handle referral interactions
|
|
11
|
+
* @param client - The current Frak Client
|
|
12
|
+
* @param args
|
|
13
|
+
* @param args.modalConfig - The modal configuration to display if the user is not logged in
|
|
14
|
+
* @param args.options - Some options for the referral interaction
|
|
15
|
+
*
|
|
16
|
+
* @returns A promise with the resulting referral state, or undefined in case of an error
|
|
17
|
+
*
|
|
18
|
+
* @description This function will automatically handle the referral interaction process
|
|
19
|
+
*
|
|
20
|
+
* @see {@link processReferral} for more details on the automatic referral handling process
|
|
21
|
+
* @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
|
|
22
|
+
*/
|
|
23
|
+
export async function referralInteraction(
|
|
24
|
+
client: FrakClient,
|
|
25
|
+
{
|
|
26
|
+
modalConfig,
|
|
27
|
+
options,
|
|
28
|
+
}: {
|
|
29
|
+
modalConfig?: DisplayEmbeddedWalletParamsType;
|
|
30
|
+
options?: ProcessReferralOptions;
|
|
31
|
+
} = {}
|
|
32
|
+
) {
|
|
33
|
+
// Get the current frak context
|
|
34
|
+
const frakContext = FrakContextManager.parse({
|
|
35
|
+
url: window.location.href,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Get the current wallet status
|
|
39
|
+
const currentWalletStatus = await watchWalletStatus(client);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await processReferral(client, {
|
|
43
|
+
walletStatus: currentWalletStatus,
|
|
44
|
+
frakContext,
|
|
45
|
+
modalConfig,
|
|
46
|
+
options,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.warn("Error processing referral", { error });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { FrakClient } from "../types";
|
|
2
|
+
import type { SendInteractionParamsType } from "../types/rpc/interaction";
|
|
3
|
+
import { getClientId } from "../utils/clientId";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Send an interaction to the backend via the listener RPC.
|
|
7
|
+
* Fire-and-forget: errors are caught and logged, not thrown.
|
|
8
|
+
*
|
|
9
|
+
* @param client - The Frak client instance
|
|
10
|
+
* @param params - The interaction parameters
|
|
11
|
+
*
|
|
12
|
+
* @description Sends a user interaction event through the wallet iframe RPC. Supports three interaction types: arrival tracking, sharing events, and custom interactions.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* Track a user arrival with referral attribution:
|
|
16
|
+
* ```ts
|
|
17
|
+
* await sendInteraction(client, {
|
|
18
|
+
* type: "arrival",
|
|
19
|
+
* referrerWallet: "0x1234...abcd",
|
|
20
|
+
* landingUrl: window.location.href,
|
|
21
|
+
* utmSource: "twitter",
|
|
22
|
+
* utmMedium: "social",
|
|
23
|
+
* utmCampaign: "launch-2026",
|
|
24
|
+
* });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* Track a sharing event:
|
|
29
|
+
* ```ts
|
|
30
|
+
* await sendInteraction(client, { type: "sharing" });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* Send a custom interaction:
|
|
35
|
+
* ```ts
|
|
36
|
+
* await sendInteraction(client, {
|
|
37
|
+
* type: "custom",
|
|
38
|
+
* customType: "newsletter_signup",
|
|
39
|
+
* data: { email: "user@example.com" },
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
export async function sendInteraction(
|
|
44
|
+
client: FrakClient,
|
|
45
|
+
params: SendInteractionParamsType
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
await client.request({
|
|
49
|
+
method: "frak_sendInteraction",
|
|
50
|
+
params: [params, { clientId: getClientId() }],
|
|
51
|
+
});
|
|
52
|
+
} catch {
|
|
53
|
+
// Silent failure - fire-and-forget
|
|
54
|
+
console.warn("[Frak SDK] Failed to send interaction:", params.type);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
test,
|
|
8
|
+
} from "../../tests/vitest-fixtures";
|
|
9
|
+
|
|
10
|
+
vi.mock("../utils/clientId", () => ({
|
|
11
|
+
getClientId: vi.fn().mockReturnValue("test-client-id"),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
vi.mock("../utils/merchantId", () => ({
|
|
15
|
+
fetchMerchantId: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { getClientId } from "../utils/clientId";
|
|
19
|
+
import { fetchMerchantId } from "../utils/merchantId";
|
|
20
|
+
import { trackPurchaseStatus } from "./trackPurchaseStatus";
|
|
21
|
+
|
|
22
|
+
describe.sequential("trackPurchaseStatus", () => {
|
|
23
|
+
const TRACK_PURCHASE_URL = "https://backend.frak.id/user/track/purchase";
|
|
24
|
+
|
|
25
|
+
let mockSessionStorage: {
|
|
26
|
+
getItem: ReturnType<typeof vi.fn>;
|
|
27
|
+
setItem: ReturnType<typeof vi.fn>;
|
|
28
|
+
removeItem: ReturnType<typeof vi.fn>;
|
|
29
|
+
};
|
|
30
|
+
let mockLocalStorage: {
|
|
31
|
+
getItem: ReturnType<typeof vi.fn>;
|
|
32
|
+
setItem: ReturnType<typeof vi.fn>;
|
|
33
|
+
removeItem: ReturnType<typeof vi.fn>;
|
|
34
|
+
};
|
|
35
|
+
let fetchSpy: ReturnType<typeof vi.fn>;
|
|
36
|
+
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
|
37
|
+
|
|
38
|
+
function setupStorage(values: {
|
|
39
|
+
interactionToken?: string | null;
|
|
40
|
+
merchantId?: string | null;
|
|
41
|
+
clientId?: string | null;
|
|
42
|
+
}) {
|
|
43
|
+
mockSessionStorage.getItem.mockImplementation((key: string) => {
|
|
44
|
+
if (key === "frak-wallet-interaction-token") {
|
|
45
|
+
return values.interactionToken ?? null;
|
|
46
|
+
}
|
|
47
|
+
if (key === "frak-merchant-id") {
|
|
48
|
+
return values.merchantId ?? null;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mockLocalStorage.getItem.mockImplementation((key: string) => {
|
|
54
|
+
if (key === "frak-client-id") {
|
|
55
|
+
return values.clientId ?? null;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getTrackingRequests() {
|
|
62
|
+
return fetchSpy.mock.calls.filter(
|
|
63
|
+
([url]) => url === TRACK_PURCHASE_URL
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getLastTrackingRequest() {
|
|
68
|
+
return getTrackingRequests().at(-1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
mockSessionStorage = {
|
|
73
|
+
getItem: vi.fn(),
|
|
74
|
+
setItem: vi.fn(),
|
|
75
|
+
removeItem: vi.fn(),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
mockLocalStorage = {
|
|
79
|
+
getItem: vi.fn(),
|
|
80
|
+
setItem: vi.fn(),
|
|
81
|
+
removeItem: vi.fn(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
Object.defineProperty(window, "sessionStorage", {
|
|
85
|
+
value: mockSessionStorage,
|
|
86
|
+
writable: true,
|
|
87
|
+
configurable: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
Object.defineProperty(window, "localStorage", {
|
|
91
|
+
value: mockLocalStorage,
|
|
92
|
+
writable: true,
|
|
93
|
+
configurable: true,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
setupStorage({
|
|
97
|
+
interactionToken: "token-123",
|
|
98
|
+
merchantId: null,
|
|
99
|
+
clientId: "test-client-id",
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
vi.mocked(getClientId).mockReturnValue("test-client-id");
|
|
103
|
+
vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
|
|
104
|
+
|
|
105
|
+
fetchSpy = vi.fn().mockResolvedValue({
|
|
106
|
+
ok: true,
|
|
107
|
+
status: 200,
|
|
108
|
+
});
|
|
109
|
+
global.fetch = fetchSpy as typeof fetch;
|
|
110
|
+
|
|
111
|
+
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
consoleWarnSpy.mockRestore();
|
|
116
|
+
vi.clearAllMocks();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("successful tracking", () => {
|
|
120
|
+
test("should send POST request with correct parameters including merchantId", async () => {
|
|
121
|
+
const callCountBefore = getTrackingRequests().length;
|
|
122
|
+
|
|
123
|
+
await trackPurchaseStatus({
|
|
124
|
+
customerId: "cust-456",
|
|
125
|
+
orderId: "order-789",
|
|
126
|
+
token: "purchase-token",
|
|
127
|
+
merchantId: "merchant-explicit",
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(getTrackingRequests().length).toBe(callCountBefore + 1);
|
|
131
|
+
expect(getLastTrackingRequest()).toEqual([
|
|
132
|
+
TRACK_PURCHASE_URL,
|
|
133
|
+
{
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
Accept: "application/json",
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
"x-wallet-sdk-auth": "token-123",
|
|
139
|
+
"x-frak-client-id": "test-client-id",
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
customerId: "cust-456",
|
|
143
|
+
orderId: "order-789",
|
|
144
|
+
token: "purchase-token",
|
|
145
|
+
merchantId: "merchant-explicit",
|
|
146
|
+
}),
|
|
147
|
+
},
|
|
148
|
+
]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("should include x-frak-client-id header", async () => {
|
|
152
|
+
setupStorage({
|
|
153
|
+
interactionToken: null,
|
|
154
|
+
merchantId: null,
|
|
155
|
+
clientId: "test-client-id",
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await trackPurchaseStatus({
|
|
159
|
+
customerId: "cust-1",
|
|
160
|
+
orderId: "order-1",
|
|
161
|
+
token: "token-1",
|
|
162
|
+
merchantId: "merchant-1",
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
166
|
+
headers: Record<string, string>;
|
|
167
|
+
};
|
|
168
|
+
expect(requestInit.headers).toEqual({
|
|
169
|
+
Accept: "application/json",
|
|
170
|
+
"Content-Type": "application/json",
|
|
171
|
+
"x-frak-client-id": "test-client-id",
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("should include x-wallet-sdk-auth header when interaction token exists", async () => {
|
|
176
|
+
await trackPurchaseStatus({
|
|
177
|
+
customerId: "cust-1",
|
|
178
|
+
orderId: "order-1",
|
|
179
|
+
token: "token-1",
|
|
180
|
+
merchantId: "merchant-1",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
184
|
+
headers: Record<string, string>;
|
|
185
|
+
};
|
|
186
|
+
expect(requestInit.headers).toEqual({
|
|
187
|
+
Accept: "application/json",
|
|
188
|
+
"Content-Type": "application/json",
|
|
189
|
+
"x-wallet-sdk-auth": "token-123",
|
|
190
|
+
"x-frak-client-id": "test-client-id",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("should handle numeric customerId and orderId", async () => {
|
|
195
|
+
await trackPurchaseStatus({
|
|
196
|
+
customerId: 12345,
|
|
197
|
+
orderId: 67890,
|
|
198
|
+
token: "purchase-token",
|
|
199
|
+
merchantId: "merchant-1",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
203
|
+
body: string;
|
|
204
|
+
};
|
|
205
|
+
expect(requestInit.body).toBe(
|
|
206
|
+
JSON.stringify({
|
|
207
|
+
customerId: 12345,
|
|
208
|
+
orderId: 67890,
|
|
209
|
+
token: "purchase-token",
|
|
210
|
+
merchantId: "merchant-1",
|
|
211
|
+
})
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("should use new endpoint URL /user/track/purchase", async () => {
|
|
216
|
+
await trackPurchaseStatus({
|
|
217
|
+
customerId: "cust-1",
|
|
218
|
+
orderId: "order-1",
|
|
219
|
+
token: "token-1",
|
|
220
|
+
merchantId: "merchant-1",
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(getLastTrackingRequest()?.[0]).toBe(TRACK_PURCHASE_URL);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe("merchantId resolution", () => {
|
|
228
|
+
test("should resolve merchantId from explicit param first", async () => {
|
|
229
|
+
setupStorage({
|
|
230
|
+
interactionToken: "token-123",
|
|
231
|
+
merchantId: "session-merchant-id",
|
|
232
|
+
clientId: "test-client-id",
|
|
233
|
+
});
|
|
234
|
+
vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
|
|
235
|
+
const merchantLookupCallsBefore =
|
|
236
|
+
vi.mocked(fetchMerchantId).mock.calls.length;
|
|
237
|
+
|
|
238
|
+
await trackPurchaseStatus({
|
|
239
|
+
customerId: "cust-1",
|
|
240
|
+
orderId: "order-1",
|
|
241
|
+
token: "token-1",
|
|
242
|
+
merchantId: "explicit-merchant-id",
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
246
|
+
body: string;
|
|
247
|
+
};
|
|
248
|
+
expect(requestInit.body).toBe(
|
|
249
|
+
JSON.stringify({
|
|
250
|
+
customerId: "cust-1",
|
|
251
|
+
orderId: "order-1",
|
|
252
|
+
token: "token-1",
|
|
253
|
+
merchantId: "explicit-merchant-id",
|
|
254
|
+
})
|
|
255
|
+
);
|
|
256
|
+
expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
|
|
257
|
+
merchantLookupCallsBefore
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("should fall back to sessionStorage for merchantId", async () => {
|
|
262
|
+
setupStorage({
|
|
263
|
+
interactionToken: "token-123",
|
|
264
|
+
merchantId: "session-merchant-id",
|
|
265
|
+
clientId: "test-client-id",
|
|
266
|
+
});
|
|
267
|
+
const merchantLookupCallsBefore =
|
|
268
|
+
vi.mocked(fetchMerchantId).mock.calls.length;
|
|
269
|
+
|
|
270
|
+
await trackPurchaseStatus({
|
|
271
|
+
customerId: "cust-1",
|
|
272
|
+
orderId: "order-1",
|
|
273
|
+
token: "token-1",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
277
|
+
body: string;
|
|
278
|
+
};
|
|
279
|
+
expect(requestInit.body).toBe(
|
|
280
|
+
JSON.stringify({
|
|
281
|
+
customerId: "cust-1",
|
|
282
|
+
orderId: "order-1",
|
|
283
|
+
token: "token-1",
|
|
284
|
+
merchantId: "session-merchant-id",
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
|
|
288
|
+
merchantLookupCallsBefore
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("should fall back to fetchMerchantId when no explicit or sessionStorage", async () => {
|
|
293
|
+
setupStorage({
|
|
294
|
+
interactionToken: "token-123",
|
|
295
|
+
merchantId: null,
|
|
296
|
+
clientId: "test-client-id",
|
|
297
|
+
});
|
|
298
|
+
vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
|
|
299
|
+
|
|
300
|
+
await trackPurchaseStatus({
|
|
301
|
+
customerId: "cust-1",
|
|
302
|
+
orderId: "order-1",
|
|
303
|
+
token: "token-1",
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
307
|
+
body: string;
|
|
308
|
+
};
|
|
309
|
+
expect(requestInit.body).toBe(
|
|
310
|
+
JSON.stringify({
|
|
311
|
+
customerId: "cust-1",
|
|
312
|
+
orderId: "order-1",
|
|
313
|
+
token: "token-1",
|
|
314
|
+
merchantId: "fetched-merchant-id",
|
|
315
|
+
})
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("should warn and skip when no merchantId available", async () => {
|
|
320
|
+
setupStorage({
|
|
321
|
+
interactionToken: "token-123",
|
|
322
|
+
merchantId: null,
|
|
323
|
+
clientId: "test-client-id",
|
|
324
|
+
});
|
|
325
|
+
vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
|
|
326
|
+
const callCountBefore = getTrackingRequests().length;
|
|
327
|
+
|
|
328
|
+
await trackPurchaseStatus({
|
|
329
|
+
customerId: "cust-1",
|
|
330
|
+
orderId: "order-1",
|
|
331
|
+
token: "token-1",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
335
|
+
"[Frak] No merchant id found, skipping purchase check"
|
|
336
|
+
);
|
|
337
|
+
expect(getTrackingRequests().length).toBe(callCountBefore);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
describe("anonymous user support", () => {
|
|
342
|
+
test("should send request with only x-frak-client-id when no interaction token", async () => {
|
|
343
|
+
setupStorage({
|
|
344
|
+
interactionToken: null,
|
|
345
|
+
merchantId: null,
|
|
346
|
+
clientId: "test-client-id",
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
await trackPurchaseStatus({
|
|
350
|
+
customerId: "cust-1",
|
|
351
|
+
orderId: "order-1",
|
|
352
|
+
token: "token-1",
|
|
353
|
+
merchantId: "merchant-1",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
357
|
+
headers: Record<string, string>;
|
|
358
|
+
};
|
|
359
|
+
expect(requestInit.headers).toEqual({
|
|
360
|
+
Accept: "application/json",
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
"x-frak-client-id": "test-client-id",
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("should send request with both headers when both available", async () => {
|
|
367
|
+
setupStorage({
|
|
368
|
+
interactionToken: "token-123",
|
|
369
|
+
merchantId: null,
|
|
370
|
+
clientId: "test-client-id",
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await trackPurchaseStatus({
|
|
374
|
+
customerId: "cust-1",
|
|
375
|
+
orderId: "order-1",
|
|
376
|
+
token: "token-1",
|
|
377
|
+
merchantId: "merchant-1",
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const requestInit = getLastTrackingRequest()?.[1] as {
|
|
381
|
+
headers: Record<string, string>;
|
|
382
|
+
};
|
|
383
|
+
expect(requestInit.headers).toEqual({
|
|
384
|
+
Accept: "application/json",
|
|
385
|
+
"Content-Type": "application/json",
|
|
386
|
+
"x-wallet-sdk-auth": "token-123",
|
|
387
|
+
"x-frak-client-id": "test-client-id",
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("should skip when no identity available", async () => {
|
|
392
|
+
setupStorage({
|
|
393
|
+
interactionToken: null,
|
|
394
|
+
merchantId: "merchant-1",
|
|
395
|
+
clientId: null,
|
|
396
|
+
});
|
|
397
|
+
vi.mocked(getClientId).mockReturnValue("");
|
|
398
|
+
const callCountBefore = getTrackingRequests().length;
|
|
399
|
+
|
|
400
|
+
await trackPurchaseStatus({
|
|
401
|
+
customerId: "cust-1",
|
|
402
|
+
orderId: "order-1",
|
|
403
|
+
token: "token-1",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
407
|
+
"[Frak] No identity found, skipping purchase check"
|
|
408
|
+
);
|
|
409
|
+
expect(getTrackingRequests().length).toBe(callCountBefore);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("missing identity", () => {
|
|
414
|
+
test("should warn when no identity sources available", async () => {
|
|
415
|
+
setupStorage({
|
|
416
|
+
interactionToken: null,
|
|
417
|
+
merchantId: "merchant-1",
|
|
418
|
+
clientId: null,
|
|
419
|
+
});
|
|
420
|
+
vi.mocked(getClientId).mockReturnValue("");
|
|
421
|
+
const callCountBefore = getTrackingRequests().length;
|
|
422
|
+
|
|
423
|
+
await trackPurchaseStatus({
|
|
424
|
+
customerId: "cust-456",
|
|
425
|
+
orderId: "order-789",
|
|
426
|
+
token: "purchase-token",
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
430
|
+
"[Frak] No identity found, skipping purchase check"
|
|
431
|
+
);
|
|
432
|
+
expect(getTrackingRequests().length).toBe(callCountBefore);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("non-browser environment", () => {
|
|
437
|
+
test("should warn and skip when window is undefined", async () => {
|
|
438
|
+
const savedWindow = globalThis.window;
|
|
439
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
440
|
+
const callCountBefore = getTrackingRequests().length;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
await trackPurchaseStatus({
|
|
444
|
+
customerId: "cust-1",
|
|
445
|
+
orderId: "order-1",
|
|
446
|
+
token: "token-1",
|
|
447
|
+
merchantId: "merchant-1",
|
|
448
|
+
});
|
|
449
|
+
} finally {
|
|
450
|
+
Object.defineProperty(globalThis, "window", {
|
|
451
|
+
value: savedWindow,
|
|
452
|
+
writable: true,
|
|
453
|
+
configurable: true,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
|
458
|
+
"[Frak] No window found, can't track purchase"
|
|
459
|
+
);
|
|
460
|
+
expect(getTrackingRequests().length).toBe(callCountBefore);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
describe("network errors", () => {
|
|
465
|
+
test("should handle fetch rejection", async () => {
|
|
466
|
+
vi.mocked(getClientId).mockReturnValue("test-client-id");
|
|
467
|
+
setupStorage({
|
|
468
|
+
interactionToken: "token-123",
|
|
469
|
+
merchantId: null,
|
|
470
|
+
clientId: "test-client-id",
|
|
471
|
+
});
|
|
472
|
+
fetchSpy.mockRejectedValueOnce(new Error("Network error"));
|
|
473
|
+
|
|
474
|
+
await expect(
|
|
475
|
+
trackPurchaseStatus({
|
|
476
|
+
customerId: "cust-456",
|
|
477
|
+
orderId: "order-789",
|
|
478
|
+
token: "purchase-token",
|
|
479
|
+
merchantId: "merchant-1",
|
|
480
|
+
})
|
|
481
|
+
).rejects.toThrow("Network error");
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("should handle fetch with error response", async () => {
|
|
485
|
+
fetchSpy.mockResolvedValue({
|
|
486
|
+
ok: false,
|
|
487
|
+
status: 500,
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
await trackPurchaseStatus({
|
|
491
|
+
customerId: "cust-456",
|
|
492
|
+
orderId: "order-789",
|
|
493
|
+
token: "purchase-token",
|
|
494
|
+
merchantId: "merchant-1",
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
expect(fetchSpy).toHaveBeenCalled();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { getBackendUrl } from "../utils/backendUrl";
|
|
2
|
+
import { getClientId } from "../utils/clientId";
|
|
3
|
+
import { fetchMerchantId } from "../utils/merchantId";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Function used to track the status of a purchase
|
|
7
|
+
* when a purchase is tracked, the `purchaseCompleted` interactions will be automatically send for the user when we receive the purchase confirmation via webhook.
|
|
8
|
+
*
|
|
9
|
+
* @param args.customerId - The customer id that made the purchase (on your side)
|
|
10
|
+
* @param args.orderId - The order id of the purchase (on your side)
|
|
11
|
+
* @param args.token - The token of the purchase
|
|
12
|
+
* @param args.merchantId - Optional explicit merchant id to use for the tracking request
|
|
13
|
+
*
|
|
14
|
+
* @description This function will send a request to the backend to listen for the purchase status.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* async function trackPurchase(checkout) {
|
|
18
|
+
* const payload = {
|
|
19
|
+
* customerId: checkout.order.customer.id,
|
|
20
|
+
* orderId: checkout.order.id,
|
|
21
|
+
* token: checkout.token,
|
|
22
|
+
* merchantId: "your-merchant-id",
|
|
23
|
+
* };
|
|
24
|
+
*
|
|
25
|
+
* await trackPurchaseStatus(payload);
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
|
|
30
|
+
* - This function supports anonymous users and will use the `x-frak-client-id` header when available.
|
|
31
|
+
* - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
|
|
32
|
+
* - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
|
|
33
|
+
*/
|
|
34
|
+
export async function trackPurchaseStatus(args: {
|
|
35
|
+
customerId: string | number;
|
|
36
|
+
orderId: string | number;
|
|
37
|
+
token: string;
|
|
38
|
+
merchantId?: string;
|
|
39
|
+
}) {
|
|
40
|
+
if (typeof window === "undefined") {
|
|
41
|
+
console.warn("[Frak] No window found, can't track purchase");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const interactionToken = window.sessionStorage.getItem(
|
|
46
|
+
"frak-wallet-interaction-token"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const clientId = getClientId();
|
|
50
|
+
if (!interactionToken && !clientId) {
|
|
51
|
+
console.warn("[Frak] No identity found, skipping purchase check");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const merchantIdFromStorage =
|
|
56
|
+
window.sessionStorage.getItem("frak-merchant-id");
|
|
57
|
+
const merchantId =
|
|
58
|
+
args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
|
|
59
|
+
|
|
60
|
+
if (!merchantId) {
|
|
61
|
+
console.warn("[Frak] No merchant id found, skipping purchase check");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const headers: Record<string, string> = {
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (interactionToken) {
|
|
71
|
+
headers["x-wallet-sdk-auth"] = interactionToken;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (clientId) {
|
|
75
|
+
headers["x-frak-client-id"] = clientId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Submit the listening request
|
|
79
|
+
const backendUrl = getBackendUrl();
|
|
80
|
+
await fetch(`${backendUrl}/user/track/purchase`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
customerId: args.customerId,
|
|
85
|
+
orderId: args.orderId,
|
|
86
|
+
token: args.token,
|
|
87
|
+
merchantId,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
}
|