@frak-labs/core-sdk 0.2.0 → 0.2.1-beta.06c52c98

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.
Files changed (67) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +55 -3
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +2 -2
  5. package/dist/actions.d.ts +2 -2
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -1
  8. package/dist/bundle.d.cts +4 -4
  9. package/dist/bundle.d.ts +4 -4
  10. package/dist/bundle.js +1 -1
  11. package/dist/{computeLegacyProductId-Raks6FXg.d.cts → computeLegacyProductId-BP-ciVsp.d.cts} +73 -88
  12. package/dist/{computeLegacyProductId-BkyJ4rEY.d.ts → computeLegacyProductId-DiJd7RNo.d.ts} +73 -88
  13. package/dist/index.cjs +1 -1
  14. package/dist/index.d.cts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +1 -1
  17. package/dist/{openSso-BCJGchIb.d.cts → openSso-B8v3Vtnh.d.ts} +157 -52
  18. package/dist/{openSso-DG-_9CED.d.ts → openSso-n_B4LSuW.d.cts} +157 -52
  19. package/dist/setupClient-Dr_UYfTD.cjs +13 -0
  20. package/dist/setupClient-TuhDjVJx.js +13 -0
  21. package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
  22. package/dist/{siweAuthenticate-Btem4QHs.d.ts → siweAuthenticate-CDCsp8EJ.d.ts} +35 -36
  23. package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
  24. package/dist/{siweAuthenticate-BH7Dn7nZ.d.cts → siweAuthenticate-yITE-iKh.d.cts} +35 -36
  25. package/dist/trackEvent-5j5kkOCj.js +1 -0
  26. package/dist/trackEvent-B2uom25e.cjs +1 -0
  27. package/package.json +8 -8
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/ensureIdentity.ts +2 -2
  31. package/src/actions/referral/processReferral.test.ts +109 -125
  32. package/src/actions/referral/processReferral.ts +134 -180
  33. package/src/actions/referral/referralInteraction.test.ts +3 -5
  34. package/src/actions/referral/referralInteraction.ts +2 -7
  35. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  36. package/src/actions/trackPurchaseStatus.ts +3 -5
  37. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  38. package/src/actions/wrapper/modalBuilder.ts +6 -8
  39. package/src/clients/createIFrameFrakClient.ts +146 -25
  40. package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
  41. package/src/clients/transports/iframeLifecycleManager.ts +0 -44
  42. package/src/index.ts +8 -3
  43. package/src/types/config.ts +10 -3
  44. package/src/types/context.ts +48 -6
  45. package/src/types/index.ts +8 -2
  46. package/src/types/lifecycle/client.ts +22 -27
  47. package/src/types/lifecycle/iframe.ts +0 -8
  48. package/src/types/resolvedConfig.ts +104 -0
  49. package/src/types/rpc/interaction.ts +9 -0
  50. package/src/types/rpc.ts +7 -5
  51. package/src/types/tracking.ts +5 -34
  52. package/src/utils/FrakContext.test.ts +270 -186
  53. package/src/utils/FrakContext.ts +78 -56
  54. package/src/utils/backendUrl.test.ts +2 -2
  55. package/src/utils/backendUrl.ts +1 -1
  56. package/src/utils/index.ts +1 -5
  57. package/src/utils/sdkConfigStore.test.ts +405 -0
  58. package/src/utils/sdkConfigStore.ts +277 -0
  59. package/src/utils/sso.ts +3 -7
  60. package/dist/setupClient-CQrMDGyZ.js +0 -13
  61. package/dist/setupClient-Ccv3XxwL.cjs +0 -13
  62. package/dist/siweAuthenticate-BJHbtty4.js +0 -1
  63. package/dist/siweAuthenticate-Cwj3HP0m.cjs +0 -1
  64. package/dist/trackEvent-M2RLTQ2p.js +0 -1
  65. package/dist/trackEvent-T_R9ER2S.cjs +0 -1
  66. package/src/utils/merchantId.test.ts +0 -653
  67. package/src/utils/merchantId.ts +0 -143
@@ -1,232 +1,186 @@
1
- import { FrakRpcError, RpcErrorCodes } from "@frak-labs/frame-connector";
2
- import { type Address, isAddressEqual } from "viem";
1
+ import { isAddressEqual } from "viem";
3
2
  import type {
4
- DisplayEmbeddedWalletParamsType,
5
3
  FrakClient,
6
4
  FrakContext,
5
+ FrakContextV2,
7
6
  WalletStatusReturnType,
8
7
  } from "../../types";
9
- import { FrakContextManager, trackEvent } from "../../utils";
10
- import { displayEmbeddedWallet } from "../displayEmbeddedWallet";
8
+ import { isV1Context, isV2Context } from "../../types";
9
+ import { FrakContextManager, getClientId, trackEvent } from "../../utils";
11
10
  import { sendInteraction } from "../sendInteraction";
12
11
 
13
12
  /**
14
- * The different states of the referral process
13
+ * Options for the referral auto-interaction process.
14
+ */
15
+ export type ProcessReferralOptions = {
16
+ /**
17
+ * If true, always replace the URL with the current user's referral context
18
+ * so the next visitor gets referred by this user.
19
+ * @defaultValue false
20
+ */
21
+ alwaysAppendUrl?: boolean;
22
+ /**
23
+ * Merchant ID for building the current user's referral context.
24
+ * Required when `alwaysAppendUrl` is true and the incoming context is V1.
25
+ * For V2 contexts, the merchantId is already embedded in the context.
26
+ */
27
+ merchantId?: string;
28
+ };
29
+
30
+ /**
31
+ * The different states of the referral process.
15
32
  * @inline
16
33
  */
17
34
  type ReferralState =
18
35
  | "idle"
19
36
  | "processing"
20
37
  | "success"
21
- | "no-wallet"
22
- | "error"
23
38
  | "no-referrer"
24
39
  | "self-referral";
25
40
 
26
41
  /**
27
- * Options for the referral auto-interaction process
28
- */
29
- export type ProcessReferralOptions = {
30
- /**
31
- * If we want to always append the url with the frak context or not
32
- * @defaultValue false
33
- */
34
- alwaysAppendUrl?: boolean;
35
- };
36
-
37
- /**
38
- * This function handle all the heavy lifting of the referral interaction process
39
- * 1. Check if the user has been referred or not (if not, early exit)
40
- * 2. Then check if the user is logged in or not
41
- * 2.1 If not logged in, try a soft login, if it fail, display a modal for the user to login
42
- * 3. Check if that's not a self-referral (if yes, early exit)
43
- * 4. Track the referral event
44
- * 5. Update the current url with the right data
45
- * 6. Return the resulting referral state
46
- *
47
- * If any error occurs during the process, the function will catch it and return an error state
42
+ * Track an arrival event if the context version is recognized.
43
+ * Sends both tracking analytics and the arrival interaction RPC.
48
44
  *
49
- * @param client - The current Frak Client
50
- * @param args
51
- * @param args.walletStatus - The current user wallet status
52
- * @param args.frakContext - The current frak context
53
- * @param args.modalConfig - The modal configuration to display if the user is not logged in
54
- * @param args.options - Some options for the referral interaction
55
- * @returns A promise with the resulting referral state
56
- *
57
- * @see {@link displayModal} for more details about the displayed modal
58
- * @see {@link @frak-labs/core-sdk!ModalStepTypes} for more details on each modal steps types
45
+ * @returns true if the context was valid and tracked, false otherwise
59
46
  */
60
- export async function processReferral(
47
+ function trackArrivalIfValid(
61
48
  client: FrakClient,
62
- {
63
- walletStatus,
64
- frakContext,
65
- modalConfig,
66
- options,
67
- }: {
68
- walletStatus?: WalletStatusReturnType;
69
- frakContext?: Partial<FrakContext> | null;
70
- modalConfig?: DisplayEmbeddedWalletParamsType;
71
- options?: ProcessReferralOptions;
72
- }
73
- ) {
74
- // Early exit if we don't have any referral informations
75
- if (!frakContext?.r) {
76
- return "no-referrer";
77
- }
78
-
79
- // If we got a context, log an event
80
- trackEvent(client, "user_referred_started", {
81
- properties: {
82
- referrer: frakContext?.r,
83
- walletStatus: walletStatus?.key,
84
- },
85
- });
86
-
87
- sendInteraction(client, {
88
- type: "arrival",
89
- referrerWallet: frakContext.r,
90
- landingUrl:
91
- typeof window !== "undefined" ? window.location.href : undefined,
92
- });
93
-
94
- // Helper to fetch a fresh wallet status
95
- let walletRequest = false;
96
- async function getFreshWalletStatus() {
97
- if (walletRequest) {
98
- return;
99
- }
100
- walletRequest = true;
101
- return ensureWalletConnected(client, {
102
- modalConfig: {
103
- ...modalConfig,
104
- loggedIn: {
105
- action: {
106
- key: "referred",
107
- },
108
- },
49
+ frakContext: FrakContext,
50
+ walletStatus?: WalletStatusReturnType
51
+ ): boolean {
52
+ const landingUrl =
53
+ typeof window !== "undefined" ? window.location.href : undefined;
54
+
55
+ if (isV2Context(frakContext)) {
56
+ trackEvent(client, "user_referred_started", {
57
+ properties: {
58
+ referrerClientId: frakContext.c,
59
+ walletStatus: walletStatus?.key,
109
60
  },
110
- walletStatus,
111
- });
112
- }
113
-
114
- try {
115
- // Do the core processing logic
116
- const { status, currentWallet } = await processReferralLogic({
117
- initialWalletStatus: walletStatus,
118
- getFreshWalletStatus,
119
- // We can enforce this type cause of the condition at the start
120
- frakContext: frakContext as Pick<FrakContext, "r">,
121
61
  });
122
-
123
- // Update the current url with the right data
124
- FrakContextManager.replaceUrl({
125
- url: window.location?.href,
126
- context: options?.alwaysAppendUrl ? { r: currentWallet } : null,
62
+ sendInteraction(client, {
63
+ type: "arrival",
64
+ referrerClientId: frakContext.c,
65
+ referrerMerchantId: frakContext.m,
66
+ referralTimestamp: frakContext.t,
67
+ landingUrl,
127
68
  });
69
+ return true;
70
+ }
128
71
 
129
- // Track the event
130
- trackEvent(client, "user_referred_completed", {
72
+ if (isV1Context(frakContext)) {
73
+ trackEvent(client, "user_referred_started", {
131
74
  properties: {
132
- status,
133
- referrer: frakContext?.r,
134
- wallet: currentWallet,
75
+ referrer: frakContext.r,
76
+ walletStatus: walletStatus?.key,
135
77
  },
136
78
  });
137
-
138
- return status;
139
- } catch (error) {
140
- console.log("Error processing referral", { error });
141
-
142
- // Track the error event
143
- trackEvent(client, "user_referred_error", {
144
- properties: {
145
- referrer: frakContext?.r,
146
- error:
147
- error instanceof FrakRpcError
148
- ? `[${error.code}] ${error.name} - ${error.message}`
149
- : error instanceof Error
150
- ? error.message
151
- : "undefined",
152
- },
79
+ sendInteraction(client, {
80
+ type: "arrival",
81
+ referrerWallet: frakContext.r,
82
+ landingUrl,
153
83
  });
84
+ return true;
85
+ }
154
86
 
155
- // Update the current url with the right data
156
- FrakContextManager.replaceUrl({
157
- url: window.location?.href,
158
- context: options?.alwaysAppendUrl
159
- ? { r: walletStatus?.wallet }
160
- : null,
161
- });
87
+ return false;
88
+ }
162
89
 
163
- // And map the error a state
164
- return mapErrorToState(error);
165
- }
90
+ /**
91
+ * Build a V2 context representing the current user for URL replacement.
92
+ * @returns A V2 context, or null if clientId or merchantId is unavailable
93
+ */
94
+ function buildCurrentUserContext(merchantId: string): FrakContextV2 | null {
95
+ const clientId = getClientId();
96
+ if (!clientId) return null;
97
+ return {
98
+ v: 2,
99
+ c: clientId,
100
+ m: merchantId,
101
+ t: Math.floor(Date.now() / 1000),
102
+ };
166
103
  }
167
104
 
168
105
  /**
169
- * Process referral logic - ensure user is logged in and check for self-referral
106
+ * Client-side self-referral preflight check.
107
+ * Prevents unnecessary backend round-trips for obvious self-referrals.
170
108
  */
171
- async function processReferralLogic({
172
- initialWalletStatus,
173
- getFreshWalletStatus,
174
- frakContext,
175
- }: {
176
- initialWalletStatus?: WalletStatusReturnType;
177
- getFreshWalletStatus: () => Promise<Address | undefined>;
178
- frakContext: Pick<FrakContext, "r">;
179
- }) {
180
- // Get the current wallet, without auto displaying the modal
181
- let currentWallet = initialWalletStatus?.wallet;
182
-
183
- // If we don't have a current wallet, display the modal to log in
184
- if (!currentWallet) {
185
- currentWallet = await getFreshWalletStatus();
109
+ function isSelfReferral(
110
+ frakContext: FrakContext,
111
+ walletStatus?: WalletStatusReturnType
112
+ ): boolean {
113
+ if (isV2Context(frakContext)) {
114
+ return getClientId() === frakContext.c;
186
115
  }
187
-
188
- // Check for self-referral
189
- if (currentWallet && isAddressEqual(frakContext.r, currentWallet)) {
190
- return { status: "self-referral", currentWallet } as const;
116
+ if (isV1Context(frakContext) && walletStatus?.wallet) {
117
+ return isAddressEqual(frakContext.r, walletStatus.wallet);
191
118
  }
192
-
193
- return { status: "success", currentWallet } as const;
119
+ return false;
194
120
  }
195
121
 
196
122
  /**
197
- * Helper to ensure a wallet is connected, and display a modal if we got everything needed
123
+ * Handle the full referral interaction flow:
124
+ *
125
+ * 1. Check if the user has been referred (if not, early exit)
126
+ * 2. Preflight self-referral check (if yes, early exit)
127
+ * 3. Track the arrival event
128
+ * 4. Replace the current URL with the user's own referral context
129
+ * 5. Return the resulting referral state
130
+ *
131
+ * @param client - The current Frak Client
132
+ * @param args
133
+ * @param args.walletStatus - The current user wallet status
134
+ * @param args.frakContext - The referral context parsed from the URL
135
+ * @param args.options - Options for URL replacement and merchant context
136
+ * @returns The referral state
137
+ *
138
+ * @see {@link @frak-labs/core-sdk!ModalStepTypes} for modal step types
198
139
  */
199
- async function ensureWalletConnected(
140
+ export function processReferral(
200
141
  client: FrakClient,
201
142
  {
202
- modalConfig,
203
143
  walletStatus,
144
+ frakContext,
145
+ options,
204
146
  }: {
205
- modalConfig?: DisplayEmbeddedWalletParamsType;
206
147
  walletStatus?: WalletStatusReturnType;
148
+ frakContext?: FrakContext | null;
149
+ options?: ProcessReferralOptions;
207
150
  }
208
- ) {
209
- // If wallet not connected, display modal
210
- if (walletStatus?.key !== "connected") {
211
- const result = await displayEmbeddedWallet(client, modalConfig ?? {});
212
- return result?.wallet ?? undefined;
151
+ ): ReferralState {
152
+ if (!frakContext) {
153
+ return "no-referrer";
213
154
  }
214
155
 
215
- return walletStatus.wallet ?? undefined;
216
- }
156
+ if (isSelfReferral(frakContext, walletStatus)) {
157
+ return "self-referral";
158
+ }
217
159
 
218
- /**
219
- * Helper to map an error to a state
220
- * @param error
221
- */
222
- function mapErrorToState(error: unknown): ReferralState {
223
- if (error instanceof FrakRpcError) {
224
- switch (error.code) {
225
- case RpcErrorCodes.walletNotConnected:
226
- return "no-wallet";
227
- default:
228
- return "error";
229
- }
160
+ if (!trackArrivalIfValid(client, frakContext, walletStatus)) {
161
+ return "no-referrer";
230
162
  }
231
- return "error";
163
+
164
+ // V2 context embeds merchantId; V1 falls back to options
165
+ const contextMerchantId = isV2Context(frakContext)
166
+ ? frakContext.m
167
+ : options?.merchantId;
168
+
169
+ const replaceContext =
170
+ options?.alwaysAppendUrl && contextMerchantId
171
+ ? buildCurrentUserContext(contextMerchantId)
172
+ : null;
173
+
174
+ FrakContextManager.replaceUrl({
175
+ url: window.location?.href,
176
+ context: replaceContext,
177
+ });
178
+
179
+ trackEvent(client, "user_referred_completed", {
180
+ properties: {
181
+ status: "success",
182
+ },
183
+ });
184
+
185
+ return "success";
232
186
  }
@@ -69,7 +69,6 @@ describe("referralInteraction", () => {
69
69
 
70
70
  const mockContext = { r: "0xreferrer" as Hex };
71
71
  const mockWalletStatus = { wallet: "0x123" as Hex };
72
- const mockModalConfig = { type: "login" };
73
72
  const mockOptions = { alwaysAppendUrl: true };
74
73
 
75
74
  vi.mocked(FrakContextManager.parse).mockReturnValue(mockContext as any);
@@ -77,14 +76,12 @@ describe("referralInteraction", () => {
77
76
  vi.mocked(processReferral).mockResolvedValue("success");
78
77
 
79
78
  await referralInteraction(mockClient, {
80
- modalConfig: mockModalConfig as any,
81
79
  options: mockOptions,
82
80
  });
83
81
 
84
82
  expect(processReferral).toHaveBeenCalledWith(mockClient, {
85
83
  walletStatus: mockWalletStatus,
86
84
  frakContext: mockContext,
87
- modalConfig: mockModalConfig,
88
85
  options: mockOptions,
89
86
  });
90
87
  });
@@ -110,7 +107,9 @@ describe("referralInteraction", () => {
110
107
 
111
108
  vi.mocked(FrakContextManager.parse).mockReturnValue({} as any);
112
109
  vi.mocked(watchWalletStatus).mockResolvedValue(null as any);
113
- vi.mocked(processReferral).mockRejectedValue(new Error("Test error"));
110
+ vi.mocked(processReferral).mockImplementation(() => {
111
+ throw new Error("Test error");
112
+ });
114
113
 
115
114
  const consoleSpy = vi
116
115
  .spyOn(console, "warn")
@@ -139,7 +138,6 @@ describe("referralInteraction", () => {
139
138
  expect(processReferral).toHaveBeenCalledWith(
140
139
  mockClient,
141
140
  expect.objectContaining({
142
- modalConfig: undefined,
143
141
  options: undefined,
144
142
  })
145
143
  );
@@ -1,4 +1,4 @@
1
- import type { DisplayEmbeddedWalletParamsType, FrakClient } from "../../types";
1
+ import type { FrakClient } from "../../types";
2
2
  import { FrakContextManager } from "../../utils";
3
3
  import { watchWalletStatus } from "../index";
4
4
  import {
@@ -10,7 +10,6 @@ import {
10
10
  * Function used to handle referral interactions
11
11
  * @param client - The current Frak Client
12
12
  * @param args
13
- * @param args.modalConfig - The modal configuration to display if the user is not logged in
14
13
  * @param args.options - Some options for the referral interaction
15
14
  *
16
15
  * @returns A promise with the resulting referral state, or undefined in case of an error
@@ -18,15 +17,12 @@ import {
18
17
  * @description This function will automatically handle the referral interaction process
19
18
  *
20
19
  * @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
20
  */
23
21
  export async function referralInteraction(
24
22
  client: FrakClient,
25
23
  {
26
- modalConfig,
27
24
  options,
28
25
  }: {
29
- modalConfig?: DisplayEmbeddedWalletParamsType;
30
26
  options?: ProcessReferralOptions;
31
27
  } = {}
32
28
  ) {
@@ -39,10 +35,9 @@ export async function referralInteraction(
39
35
  const currentWalletStatus = await watchWalletStatus(client);
40
36
 
41
37
  try {
42
- return await processReferral(client, {
38
+ return processReferral(client, {
43
39
  walletStatus: currentWalletStatus,
44
40
  frakContext,
45
- modalConfig,
46
41
  options,
47
42
  });
48
43
  } catch (error) {
@@ -11,12 +11,14 @@ vi.mock("../utils/clientId", () => ({
11
11
  getClientId: vi.fn().mockReturnValue("test-client-id"),
12
12
  }));
13
13
 
14
- vi.mock("../utils/merchantId", () => ({
15
- fetchMerchantId: vi.fn().mockResolvedValue(undefined),
14
+ vi.mock("../utils/sdkConfigStore", () => ({
15
+ sdkConfigStore: {
16
+ resolveMerchantId: vi.fn().mockResolvedValue(undefined),
17
+ },
16
18
  }));
17
19
 
18
20
  import { getClientId } from "../utils/clientId";
19
- import { fetchMerchantId } from "../utils/merchantId";
21
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
20
22
  import { trackPurchaseStatus } from "./trackPurchaseStatus";
21
23
 
22
24
  describe.sequential("trackPurchaseStatus", () => {
@@ -100,7 +102,9 @@ describe.sequential("trackPurchaseStatus", () => {
100
102
  });
101
103
 
102
104
  vi.mocked(getClientId).mockReturnValue("test-client-id");
103
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
105
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
106
+ undefined
107
+ );
104
108
 
105
109
  fetchSpy = vi.fn().mockResolvedValue({
106
110
  ok: true,
@@ -228,12 +232,15 @@ describe.sequential("trackPurchaseStatus", () => {
228
232
  test("should resolve merchantId from explicit param first", async () => {
229
233
  setupStorage({
230
234
  interactionToken: "token-123",
231
- merchantId: "session-merchant-id",
235
+ merchantId: null,
232
236
  clientId: "test-client-id",
233
237
  });
234
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
235
- const merchantLookupCallsBefore =
236
- vi.mocked(fetchMerchantId).mock.calls.length;
238
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
239
+ "fetched-merchant-id"
240
+ );
241
+ const merchantLookupCallsBefore = vi.mocked(
242
+ sdkConfigStore.resolveMerchantId
243
+ ).mock.calls.length;
237
244
 
238
245
  await trackPurchaseStatus({
239
246
  customerId: "cust-1",
@@ -253,19 +260,20 @@ describe.sequential("trackPurchaseStatus", () => {
253
260
  merchantId: "explicit-merchant-id",
254
261
  })
255
262
  );
256
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
257
- merchantLookupCallsBefore
258
- );
263
+ expect(
264
+ vi.mocked(sdkConfigStore.resolveMerchantId).mock.calls.length
265
+ ).toBe(merchantLookupCallsBefore);
259
266
  });
260
267
 
261
268
  test("should fall back to sessionStorage for merchantId", async () => {
269
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
270
+ "session-merchant-id"
271
+ );
262
272
  setupStorage({
263
273
  interactionToken: "token-123",
264
- merchantId: "session-merchant-id",
274
+ merchantId: null,
265
275
  clientId: "test-client-id",
266
276
  });
267
- const merchantLookupCallsBefore =
268
- vi.mocked(fetchMerchantId).mock.calls.length;
269
277
 
270
278
  await trackPurchaseStatus({
271
279
  customerId: "cust-1",
@@ -284,18 +292,20 @@ describe.sequential("trackPurchaseStatus", () => {
284
292
  merchantId: "session-merchant-id",
285
293
  })
286
294
  );
287
- expect(vi.mocked(fetchMerchantId).mock.calls.length).toBe(
288
- merchantLookupCallsBefore
289
- );
295
+ expect(
296
+ vi.mocked(sdkConfigStore.resolveMerchantId)
297
+ ).toHaveBeenCalled();
290
298
  });
291
299
 
292
- test("should fall back to fetchMerchantId when no explicit or sessionStorage", async () => {
300
+ test("should fall back to resolveMerchantId when no explicit merchantId", async () => {
293
301
  setupStorage({
294
302
  interactionToken: "token-123",
295
303
  merchantId: null,
296
304
  clientId: "test-client-id",
297
305
  });
298
- vi.mocked(fetchMerchantId).mockResolvedValue("fetched-merchant-id");
306
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
307
+ "fetched-merchant-id"
308
+ );
299
309
 
300
310
  await trackPurchaseStatus({
301
311
  customerId: "cust-1",
@@ -322,7 +332,9 @@ describe.sequential("trackPurchaseStatus", () => {
322
332
  merchantId: null,
323
333
  clientId: "test-client-id",
324
334
  });
325
- vi.mocked(fetchMerchantId).mockResolvedValue(undefined);
335
+ vi.mocked(sdkConfigStore.resolveMerchantId).mockResolvedValue(
336
+ undefined
337
+ );
326
338
  const callCountBefore = getTrackingRequests().length;
327
339
 
328
340
  await trackPurchaseStatus({
@@ -1,6 +1,6 @@
1
1
  import { getBackendUrl } from "../utils/backendUrl";
2
2
  import { getClientId } from "../utils/clientId";
3
- import { fetchMerchantId } from "../utils/merchantId";
3
+ import { sdkConfigStore } from "../utils/sdkConfigStore";
4
4
 
5
5
  /**
6
6
  * Function used to track the status of a purchase
@@ -26,7 +26,7 @@ import { fetchMerchantId } from "../utils/merchantId";
26
26
  * }
27
27
  *
28
28
  * @remarks
29
- * - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
29
+ * - Merchant id is resolved in this order: explicit `args.merchantId`, then `sdkConfigStore.resolveMerchantId()` (config store sessionStorage → backend fetch).
30
30
  * - This function supports anonymous users and will use the `x-frak-client-id` header when available.
31
31
  * - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
32
32
  * - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
@@ -52,10 +52,8 @@ export async function trackPurchaseStatus(args: {
52
52
  return;
53
53
  }
54
54
 
55
- const merchantIdFromStorage =
56
- window.sessionStorage.getItem("frak-merchant-id");
57
55
  const merchantId =
58
- args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
56
+ args.merchantId ?? (await sdkConfigStore.resolveMerchantId());
59
57
 
60
58
  if (!merchantId) {
61
59
  console.warn("[Frak] No merchant id found, skipping purchase check");
@@ -196,7 +196,8 @@ describe("modalBuilder", () => {
196
196
 
197
197
  expect(displayModal).toHaveBeenCalledWith(
198
198
  mockClient,
199
- builder.params
199
+ builder.params,
200
+ undefined
200
201
  );
201
202
  });
202
203
 
@@ -216,7 +217,8 @@ describe("modalBuilder", () => {
216
217
  mockClient,
217
218
  expect.objectContaining({
218
219
  metadata: { header: { title: "Overridden" } },
219
- })
220
+ }),
221
+ undefined
220
222
  );
221
223
  });
222
224
 
@@ -46,11 +46,13 @@ export type ModalStepBuilder<
46
46
  /**
47
47
  * Display the modal
48
48
  * @param metadataOverride - Function returning optional metadata to override the current modal metadata
49
+ * @param placement - Optional placement ID to associate with this modal display
49
50
  */
50
51
  display: (
51
52
  metadataOverride?: (
52
53
  current?: ModalRpcMetadata
53
- ) => ModalRpcMetadata | undefined
54
+ ) => ModalRpcMetadata | undefined,
55
+ placement?: string
54
56
  ) => Promise<ModalRpcStepsResultType<Steps>>;
55
57
  };
56
58
 
@@ -177,27 +179,23 @@ function modalStepsBuilder<CurrentSteps extends ModalStepTypes[]>(
177
179
  );
178
180
  }
179
181
 
180
- // Function to display it
181
182
  async function display(
182
183
  metadataOverride?: (
183
184
  current?: ModalRpcMetadata
184
- ) => ModalRpcMetadata | undefined
185
+ ) => ModalRpcMetadata | undefined,
186
+ placement?: string
185
187
  ) {
186
- // If we have a metadata override, apply it
187
188
  if (metadataOverride) {
188
189
  params.metadata = metadataOverride(params.metadata ?? {});
189
190
  }
190
- return await displayModal(client, params);
191
+ return await displayModal(client, params, placement);
191
192
  }
192
193
 
193
194
  return {
194
- // Access current modal params
195
195
  params,
196
- // Function to add new steps
197
196
  sendTx,
198
197
  reward,
199
198
  sharing,
200
- // Display the modal
201
199
  display,
202
200
  };
203
201
  }