@frak-labs/core-sdk 1.0.0 → 1.0.1

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 (59) hide show
  1. package/cdn/bundle.js +3 -3
  2. package/dist/actions-BlCQVBQJ.js +1 -0
  3. package/dist/actions-Bwj4zSdB.cjs +1 -0
  4. package/dist/actions.cjs +1 -1
  5. package/dist/actions.d.cts +2 -2
  6. package/dist/actions.d.ts +2 -2
  7. package/dist/actions.js +1 -1
  8. package/dist/bundle.cjs +1 -1
  9. package/dist/bundle.d.cts +4 -4
  10. package/dist/bundle.d.ts +4 -4
  11. package/dist/bundle.js +1 -1
  12. package/dist/{index-Dwmo109y.d.cts → index-9TdOc_ub.d.ts} +169 -44
  13. package/dist/{index-BphwTmKA.d.cts → index-BWic1g0J.d.cts} +1 -1
  14. package/dist/{index-BV5D9DsW.d.ts → index-DPIqLMCR.d.cts} +169 -44
  15. package/dist/{index-_f8EuN_1.d.ts → index-Du4nB3qO.d.ts} +1 -1
  16. package/dist/index.cjs +1 -1
  17. package/dist/index.d.cts +3 -3
  18. package/dist/index.d.ts +3 -3
  19. package/dist/index.js +1 -1
  20. package/dist/{openSso-BwEK2M98.d.cts → openSso-3YqtmSkM.d.ts} +116 -10
  21. package/dist/{openSso-C1Wzl5-i.d.ts → openSso-SP6T9cHA.d.cts} +115 -9
  22. package/dist/sdkConfigStore-BXzz5PlK.js +1 -0
  23. package/dist/sdkConfigStore-DDL_fjYX.cjs +1 -0
  24. package/dist/src-CqKED785.cjs +13 -0
  25. package/dist/src-u4vW9qh0.js +13 -0
  26. package/package.json +1 -1
  27. package/src/actions/referral/processReferral.test.ts +129 -8
  28. package/src/actions/referral/processReferral.ts +27 -17
  29. package/src/clients/createIFrameFrakClient.ts +84 -3
  30. package/src/index.ts +8 -1
  31. package/src/types/config.ts +9 -0
  32. package/src/types/context.ts +16 -4
  33. package/src/types/index.ts +2 -0
  34. package/src/types/lifecycle/client.ts +7 -0
  35. package/src/types/resolvedConfig.ts +10 -0
  36. package/src/types/rpc/displaySharingPage.ts +18 -0
  37. package/src/types/rpc/interaction.ts +1 -1
  38. package/src/types/tracking.ts +37 -1
  39. package/src/utils/FrakContext.test.ts +239 -9
  40. package/src/utils/FrakContext.ts +83 -21
  41. package/src/utils/analytics/events/component.ts +58 -0
  42. package/src/utils/analytics/events/index.ts +20 -0
  43. package/src/utils/analytics/events/lifecycle.ts +26 -0
  44. package/src/utils/analytics/events/referral.ts +11 -0
  45. package/src/utils/analytics/index.ts +8 -0
  46. package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
  47. package/src/utils/analytics/trackEvent.ts +34 -0
  48. package/src/utils/frakContextV2Codec.test.ts +241 -0
  49. package/src/utils/frakContextV2Codec.ts +197 -0
  50. package/src/utils/index.ts +5 -1
  51. package/src/utils/mergeAttribution.test.ts +153 -0
  52. package/src/utils/mergeAttribution.ts +75 -0
  53. package/dist/actions-D4aBXbdp.cjs +0 -1
  54. package/dist/actions-Dq_uN-wn.js +0 -1
  55. package/dist/src-B1eliIi6.cjs +0 -13
  56. package/dist/src-C0UH1GsN.js +0 -13
  57. package/dist/trackEvent-BqJqRZ-u.cjs +0 -1
  58. package/dist/trackEvent-Bqq4jd6R.js +0 -1
  59. package/src/utils/trackEvent.ts +0 -41
@@ -100,10 +100,9 @@ describe("processReferral", () => {
100
100
  mockClient,
101
101
  "user_referred_started",
102
102
  {
103
- properties: {
104
- referrerClientId: "referrer-client-id",
105
- walletStatus: "connected",
106
- },
103
+ referrerClientId: "referrer-client-id",
104
+ referrerWallet: undefined,
105
+ walletStatus: "connected",
107
106
  }
108
107
  );
109
108
 
@@ -111,6 +110,7 @@ describe("processReferral", () => {
111
110
  type: "arrival",
112
111
  referrerClientId: "referrer-client-id",
113
112
  referrerMerchantId: "merchant-uuid",
113
+ referrerWallet: undefined,
114
114
  referralTimestamp: 1709654400,
115
115
  landingUrl: "https://example.com/test",
116
116
  });
@@ -135,6 +135,73 @@ describe("processReferral", () => {
135
135
  expect(result).toBe("self-referral");
136
136
  vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
137
137
  });
138
+
139
+ it("should successfully process v2 referral with wallet only (no clientId)", async () => {
140
+ await import("../../utils");
141
+ const { sendInteraction } = await import("../sendInteraction");
142
+
143
+ const referrerWallet =
144
+ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" as Address;
145
+ const v2WithWalletOnly: FrakContextV2 = {
146
+ v: 2,
147
+ m: "merchant-uuid",
148
+ t: 1709654400,
149
+ w: referrerWallet,
150
+ };
151
+
152
+ const result = await processReferral(mockClient, {
153
+ walletStatus: mockWalletStatus,
154
+ frakContext: v2WithWalletOnly,
155
+ });
156
+
157
+ expect(result).toBe("success");
158
+ expect(sendInteraction).toHaveBeenCalledWith(mockClient, {
159
+ type: "arrival",
160
+ referrerClientId: undefined,
161
+ referrerMerchantId: "merchant-uuid",
162
+ referrerWallet,
163
+ referralTimestamp: 1709654400,
164
+ landingUrl: "https://example.com/test",
165
+ });
166
+ });
167
+
168
+ it("should return 'self-referral' when v2 wallet matches current wallet", async () => {
169
+ const v2SelfReferralByWallet: FrakContextV2 = {
170
+ v: 2,
171
+ m: "merchant-uuid",
172
+ t: 1709654400,
173
+ w: mockAddress,
174
+ };
175
+
176
+ const result = await processReferral(mockClient, {
177
+ walletStatus: mockWalletStatus,
178
+ frakContext: v2SelfReferralByWallet,
179
+ });
180
+
181
+ expect(result).toBe("self-referral");
182
+ });
183
+
184
+ it("should prefer wallet over clientId for self-referral when both are present", async () => {
185
+ const utils = await import("../../utils");
186
+ // clientId does NOT match current user, but wallet does → still self-referral
187
+ vi.mocked(utils.getClientId).mockReturnValue("some-other-client");
188
+
189
+ const v2Hybrid: FrakContextV2 = {
190
+ v: 2,
191
+ c: "referrer-client-id",
192
+ m: "merchant-uuid",
193
+ t: 1709654400,
194
+ w: mockAddress,
195
+ };
196
+
197
+ const result = await processReferral(mockClient, {
198
+ walletStatus: mockWalletStatus,
199
+ frakContext: v2Hybrid,
200
+ });
201
+
202
+ expect(result).toBe("self-referral");
203
+ vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
204
+ });
138
205
  });
139
206
 
140
207
  describe("V1 context (backward compat)", () => {
@@ -156,10 +223,8 @@ describe("processReferral", () => {
156
223
  mockClient,
157
224
  "user_referred_started",
158
225
  {
159
- properties: {
160
- referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
161
- walletStatus: "connected",
162
- },
226
+ referrer: "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd",
227
+ walletStatus: "connected",
163
228
  }
164
229
  );
165
230
  });
@@ -202,6 +267,7 @@ describe("processReferral", () => {
202
267
  v: 2,
203
268
  c: "test-client-id",
204
269
  m: "merchant-uuid",
270
+ w: mockAddress,
205
271
  }),
206
272
  });
207
273
  });
@@ -229,4 +295,59 @@ describe("processReferral", () => {
229
295
  context: null,
230
296
  });
231
297
  });
298
+
299
+ it("should emit wallet in replacement context when alwaysAppendUrl is true and user is connected", async () => {
300
+ const utils = await import("../../utils");
301
+ vi.mocked(utils.getClientId).mockReturnValue(null as never);
302
+
303
+ const v2Context: FrakContextV2 = {
304
+ v: 2,
305
+ c: "referrer-client-id",
306
+ m: "merchant-uuid",
307
+ t: 1709654400,
308
+ };
309
+
310
+ await processReferral(mockClient, {
311
+ walletStatus: mockWalletStatus,
312
+ frakContext: v2Context,
313
+ options: { alwaysAppendUrl: true },
314
+ });
315
+
316
+ // clientId is null, but wallet is available — should still emit {w, m}
317
+ expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
318
+ url: window.location.href,
319
+ context: expect.objectContaining({
320
+ v: 2,
321
+ m: "merchant-uuid",
322
+ w: mockAddress,
323
+ }),
324
+ });
325
+
326
+ vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
327
+ });
328
+
329
+ it("should return null replacement context when both clientId and wallet are missing", async () => {
330
+ const utils = await import("../../utils");
331
+ vi.mocked(utils.getClientId).mockReturnValue(null as never);
332
+
333
+ const v2Context: FrakContextV2 = {
334
+ v: 2,
335
+ c: "referrer-client-id",
336
+ m: "merchant-uuid",
337
+ t: 1709654400,
338
+ };
339
+
340
+ await processReferral(mockClient, {
341
+ walletStatus: { key: "not-connected" as const },
342
+ frakContext: v2Context,
343
+ options: { alwaysAppendUrl: true },
344
+ });
345
+
346
+ expect(utils.FrakContextManager.replaceUrl).toHaveBeenCalledWith({
347
+ url: window.location.href,
348
+ context: null,
349
+ });
350
+
351
+ vi.mocked(utils.getClientId).mockReturnValue("test-client-id");
352
+ });
232
353
  });
@@ -54,15 +54,15 @@ function trackArrivalIfValid(
54
54
 
55
55
  if (isV2Context(frakContext)) {
56
56
  trackEvent(client, "user_referred_started", {
57
- properties: {
58
- referrerClientId: frakContext.c,
59
- walletStatus: walletStatus?.key,
60
- },
57
+ referrerClientId: frakContext.c,
58
+ referrerWallet: frakContext.w,
59
+ walletStatus: walletStatus?.key,
61
60
  });
62
61
  sendInteraction(client, {
63
62
  type: "arrival",
64
63
  referrerClientId: frakContext.c,
65
64
  referrerMerchantId: frakContext.m,
65
+ referrerWallet: frakContext.w,
66
66
  referralTimestamp: frakContext.t,
67
67
  landingUrl,
68
68
  });
@@ -71,10 +71,8 @@ function trackArrivalIfValid(
71
71
 
72
72
  if (isV1Context(frakContext)) {
73
73
  trackEvent(client, "user_referred_started", {
74
- properties: {
75
- referrer: frakContext.r,
76
- walletStatus: walletStatus?.key,
77
- },
74
+ referrer: frakContext.r,
75
+ walletStatus: walletStatus?.key,
78
76
  });
79
77
  sendInteraction(client, {
80
78
  type: "arrival",
@@ -89,16 +87,23 @@ function trackArrivalIfValid(
89
87
 
90
88
  /**
91
89
  * Build a V2 context representing the current user for URL replacement.
92
- * @returns A V2 context, or null if clientId or merchantId is unavailable
90
+ *
91
+ * Emits both `c` (anonymous clientId) and `w` (wallet) when available — wallet
92
+ * is the preferred identity signal and takes attribution precedence downstream.
93
+ * Returns null when neither identifier is available.
93
94
  */
94
- function buildCurrentUserContext(merchantId: string): FrakContextV2 | null {
95
+ function buildCurrentUserContext(
96
+ merchantId: string,
97
+ wallet?: WalletStatusReturnType["wallet"]
98
+ ): FrakContextV2 | null {
95
99
  const clientId = getClientId();
96
- if (!clientId) return null;
100
+ if (!clientId && !wallet) return null;
97
101
  return {
98
102
  v: 2,
99
- c: clientId,
100
103
  m: merchantId,
101
104
  t: Math.floor(Date.now() / 1000),
105
+ ...(clientId ? { c: clientId } : {}),
106
+ ...(wallet ? { w: wallet } : {}),
102
107
  };
103
108
  }
104
109
 
@@ -111,7 +116,14 @@ function isSelfReferral(
111
116
  walletStatus?: WalletStatusReturnType
112
117
  ): boolean {
113
118
  if (isV2Context(frakContext)) {
114
- return getClientId() === frakContext.c;
119
+ // Wallet match takes precedence — it's the strongest signal we have.
120
+ if (frakContext.w && walletStatus?.wallet) {
121
+ return isAddressEqual(frakContext.w, walletStatus.wallet);
122
+ }
123
+ if (frakContext.c) {
124
+ return getClientId() === frakContext.c;
125
+ }
126
+ return false;
115
127
  }
116
128
  if (isV1Context(frakContext) && walletStatus?.wallet) {
117
129
  return isAddressEqual(frakContext.r, walletStatus.wallet);
@@ -168,7 +180,7 @@ export function processReferral(
168
180
 
169
181
  const replaceContext =
170
182
  options?.alwaysAppendUrl && contextMerchantId
171
- ? buildCurrentUserContext(contextMerchantId)
183
+ ? buildCurrentUserContext(contextMerchantId, walletStatus?.wallet)
172
184
  : null;
173
185
 
174
186
  FrakContextManager.replaceUrl({
@@ -177,9 +189,7 @@ export function processReferral(
177
189
  });
178
190
 
179
191
  trackEvent(client, "user_referred_completed", {
180
- properties: {
181
- status: "success",
182
- },
192
+ status: "success",
183
193
  });
184
194
 
185
195
  return "success";
@@ -80,6 +80,10 @@ export function createIFrameFrakClient({
80
80
  // Resolved after first resolved-config is sent to iframe (prevents RPC before context exists)
81
81
  const contextSent = new Deferred<void>();
82
82
 
83
+ // Handshake timing: measured from client creation until the iframe
84
+ // lifecycle manager resolves the `isConnected` promise.
85
+ const handshakeStartedAt = Date.now();
86
+
83
87
  // Create our debug info gatherer
84
88
  const debugInfo = new DebugInfoGatherer(config, iframe);
85
89
 
@@ -180,6 +184,38 @@ export function createIFrameFrakClient({
180
184
  userAnonymousClientId: getClientId(),
181
185
  });
182
186
  openPanel.init();
187
+ openPanel.track("sdk_initialized", {
188
+ sdkVersion: process.env.SDK_VERSION,
189
+ });
190
+
191
+ // Race the connection against the heartbeat timeout so we can
192
+ // distinguish "connected" from "timeout" cleanly without touching
193
+ // the heartbeat plumbing. 30s matches `HEARTBEAT_TIMEOUT`.
194
+ let settled = false;
195
+ const timeoutHandle = setTimeout(() => {
196
+ if (settled) return;
197
+ settled = true;
198
+ openPanel?.track("sdk_iframe_handshake_failed", {
199
+ reason: "timeout",
200
+ });
201
+ }, 30_000);
202
+ lifecycleManager.isConnected
203
+ .then(() => {
204
+ if (settled) return;
205
+ settled = true;
206
+ clearTimeout(timeoutHandle);
207
+ openPanel?.track("sdk_iframe_connected", {
208
+ handshake_duration_ms: Date.now() - handshakeStartedAt,
209
+ });
210
+ })
211
+ .catch(() => {
212
+ if (settled) return;
213
+ settled = true;
214
+ clearTimeout(timeoutHandle);
215
+ openPanel?.track("sdk_iframe_handshake_failed", {
216
+ reason: "unknown",
217
+ });
218
+ });
183
219
  }
184
220
 
185
221
  // Perform the post connection setup
@@ -189,6 +225,7 @@ export function createIFrameFrakClient({
189
225
  lifecycleManager,
190
226
  configPromise,
191
227
  contextSent,
228
+ openPanel,
192
229
  })
193
230
  .then(() => debugInfo.updateSetupStatus(true))
194
231
  .catch((err) => {
@@ -273,12 +310,14 @@ async function postConnectionSetup({
273
310
  lifecycleManager,
274
311
  configPromise,
275
312
  contextSent,
313
+ openPanel,
276
314
  }: {
277
315
  config: FrakWalletSdkConfig;
278
316
  rpcClient: SdkRpcClient;
279
317
  lifecycleManager: IframeLifecycleManager;
280
318
  configPromise: Promise<MerchantConfigResult> | undefined;
281
319
  contextSent: Deferred<void>;
320
+ openPanel: OpenPanel | undefined;
282
321
  }): Promise<void> {
283
322
  await lifecycleManager.isConnected;
284
323
 
@@ -300,6 +339,12 @@ async function postConnectionSetup({
300
339
  const allowedDomains = merchantConfig?.allowedDomains ?? [];
301
340
  const raw = merchantConfig?.sdkConfig;
302
341
 
342
+ // Per-field merge: backend wins over SDK static config.
343
+ const mergedAttribution =
344
+ raw?.attribution || config.attribution
345
+ ? { ...config.attribution, ...raw?.attribution }
346
+ : undefined;
347
+
303
348
  sdkConfigStore.setConfig(
304
349
  raw
305
350
  ? {
@@ -319,6 +364,7 @@ async function postConnectionSetup({
319
364
  translations: raw.translations,
320
365
  placements: raw.placements,
321
366
  components: raw.components,
367
+ attribution: mergedAttribution,
322
368
  }
323
369
  : {
324
370
  isResolved: true,
@@ -330,11 +376,16 @@ async function postConnectionSetup({
330
376
  homepageLink: config.metadata.homepageLink,
331
377
  lang: config.metadata.lang,
332
378
  currency: config.metadata.currency,
379
+ attribution: mergedAttribution,
333
380
  }
334
381
  );
335
382
  };
336
383
 
337
- // Send the resolved-config lifecycle event to the iframe
384
+ // Send the resolved-config lifecycle event to the iframe.
385
+ // This is where we also update SDK-side OpenPanel global props with
386
+ // `merchantId` + `domain` (first time they are known) so every
387
+ // subsequent SDK event is merchant-attributed. We pass
388
+ // `sdkAnonymousId` through so the listener can join SDK funnels.
338
389
  let mergeTokenConsumed = false;
339
390
  const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
340
391
  const token = mergeTokenConsumed ? undefined : pendingMergeToken;
@@ -351,8 +402,22 @@ async function postConnectionSetup({
351
402
  css: resolved.css,
352
403
  translations: resolved.translations,
353
404
  placements: resolved.placements,
405
+ attribution: resolved.attribution,
354
406
  }
355
- : undefined;
407
+ : resolved.attribution
408
+ ? { attribution: resolved.attribution }
409
+ : undefined;
410
+
411
+ const sdkAnonymousId = getClientId();
412
+
413
+ if (openPanel) {
414
+ const current = openPanel.global ?? {};
415
+ openPanel.setGlobalProperties({
416
+ ...current,
417
+ merchantId: resolved.merchantId,
418
+ domain: resolved.domain ?? "",
419
+ });
420
+ }
356
421
 
357
422
  rpcClient.sendLifecycle({
358
423
  clientLifecycle: "resolved-config",
@@ -361,6 +426,7 @@ async function postConnectionSetup({
361
426
  domain: resolved.domain ?? "",
362
427
  allowedDomains: resolved.allowedDomains ?? [],
363
428
  sourceUrl: window.location.href,
429
+ ...(sdkAnonymousId && { sdkAnonymousId }),
364
430
  ...(token && { pendingMergeToken: token }),
365
431
  ...(sdkConfig && { sdkConfig }),
366
432
  },
@@ -412,5 +478,20 @@ async function postConnectionSetup({
412
478
  });
413
479
  }
414
480
 
415
- await Promise.allSettled([pushCss(), pushI18n(), pushBackup()]);
481
+ // Inspect each setup result — a failed CSS/i18n/backup push leaves the
482
+ // partner UI in a broken-but-connected state (iframe reports
483
+ // `sdk_iframe_connected`, user sees no modal styles / wrong locale).
484
+ // Surface it as a distinct handshake reason so dashboards can
485
+ // distinguish timeout vs. asset-push failures.
486
+ const results = await Promise.allSettled([
487
+ pushCss(),
488
+ pushI18n(),
489
+ pushBackup(),
490
+ ]);
491
+ const hasFailedAssetPush = results.some((r) => r.status === "rejected");
492
+ if (hasFailedAssetPush) {
493
+ openPanel?.track("sdk_iframe_handshake_failed", {
494
+ reason: "asset_push",
495
+ });
496
+ }
416
497
  }
package/src/index.ts CHANGED
@@ -12,6 +12,8 @@ export { type LocalesKey, locales } from "./constants/locales";
12
12
 
13
13
  // Types
14
14
  export type {
15
+ AttributionDefaults,
16
+ AttributionParams,
15
17
  ClientLifecycleEvent,
16
18
  CompressedData,
17
19
  Currency,
@@ -100,7 +102,6 @@ export {
100
102
  type DeepLinkFallbackOptions,
101
103
  decompressJsonFromB64,
102
104
  FrakContextManager,
103
- type FrakEvent,
104
105
  type FullSsoParams,
105
106
  findIframeInOpener,
106
107
  formatAmount,
@@ -115,6 +116,8 @@ export {
115
116
  isFrakDeepLink,
116
117
  isInAppBrowser,
117
118
  isIOS,
119
+ type MergeAttributionInput,
120
+ mergeAttribution,
118
121
  redirectToExternalBrowser,
119
122
  sdkConfigStore,
120
123
  toAndroidIntentUrl,
@@ -122,4 +125,8 @@ export {
122
125
  triggerDeepLinkWithFallback,
123
126
  withCache,
124
127
  } from "./utils";
128
+ export type {
129
+ SdkEventMap,
130
+ SdkHandshakeFailureReason,
131
+ } from "./utils/analytics";
125
132
  export { computeLegacyProductId } from "./utils/computeLegacyProductId";
@@ -1,3 +1,5 @@
1
+ import type { AttributionDefaults } from "./tracking";
2
+
1
3
  /**
2
4
  * All the currencies available
3
5
  * @category Config
@@ -78,6 +80,13 @@ export type FrakWalletSdkConfig = {
78
80
  * @defaultValue true
79
81
  */
80
82
  waitForBackendConfig?: boolean;
83
+ /**
84
+ * Default attribution params (UTM / via / ref) appended to outbound
85
+ * sharing URLs. Per-call `displaySharingPage` overrides win, then backend
86
+ * config, then this SDK-level default. `utm_content` is intentionally
87
+ * excluded — it is per-content/per-product, never a merchant-wide default.
88
+ */
89
+ attribution?: AttributionDefaults;
81
90
  };
82
91
 
83
92
  /**
@@ -11,19 +11,31 @@ export type FrakContextV1 = {
11
11
  };
12
12
 
13
13
  /**
14
- * V2 Frak Context — anonymous-first referral context.
15
- * Contains the sharer's clientId, merchantId, and link creation timestamp.
14
+ * V2 Frak Context — anonymous-first referral context with optional wallet.
15
+ *
16
+ * Carries merchant context (`m`) and creation timestamp (`t`) unconditionally.
17
+ * Identifies the sharer via either the anonymous clientId (`c`) or, when the
18
+ * sharer is authenticated, the stronger wallet identifier (`w`). A valid V2
19
+ * context MUST contain at least one of `c` or `w`; both may be present when
20
+ * a logged-in user shares a link (best attribution signal).
21
+ *
22
+ * `w` takes precedence as the source of truth because the wallet is bound to
23
+ * the user's WebAuthn credential, survives localStorage clears, and is global
24
+ * across merchants — unlike `c`, which is a per-browser UUID.
25
+ *
16
26
  * @ignore
17
27
  */
18
28
  export type FrakContextV2 = {
19
29
  /** Version discriminator */
20
30
  v: 2;
21
- /** Sharer's anonymous clientId (UUID from localStorage) */
22
- c: string;
23
31
  /** Merchant ID (UUID) */
24
32
  m: string;
25
33
  /** Link creation timestamp (epoch seconds) */
26
34
  t: number;
35
+ /** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */
36
+ c?: string;
37
+ /** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */
38
+ w?: Address;
27
39
  };
28
40
 
29
41
  /**
@@ -80,6 +80,8 @@ export type { UserReferralStatusType } from "./rpc/userReferralStatus";
80
80
  export type { WalletStatusReturnType } from "./rpc/walletStatus";
81
81
  // Tracking
82
82
  export type {
83
+ AttributionDefaults,
84
+ AttributionParams,
83
85
  TrackArrivalParams,
84
86
  TrackArrivalResult,
85
87
  UtmParams,
@@ -59,6 +59,13 @@ type ResolvedConfigEvent = {
59
59
  * When present, listener should execute identity merge in background.
60
60
  */
61
61
  pendingMergeToken?: string;
62
+ /**
63
+ * Persistent per-origin anonymous id generated on the partner site
64
+ * (SDK-side localStorage). Propagated here so the listener can
65
+ * set it as an OpenPanel global property and stitch SDK events
66
+ * with listener events in the same funnel.
67
+ */
68
+ sdkAnonymousId?: string;
62
69
  sdkConfig?: ResolvedSdkConfig;
63
70
  };
64
71
  };
@@ -1,4 +1,5 @@
1
1
  import type { Currency, Language } from "./config";
2
+ import type { AttributionDefaults } from "./tracking";
2
3
 
3
4
  /**
4
5
  * Response from the merchant resolve endpoint
@@ -80,6 +81,12 @@ export type ResolvedSdkConfig = {
80
81
  placements?: Record<string, ResolvedPlacement>;
81
82
  /** Global component defaults (used when no placement override exists) */
82
83
  components?: ResolvedPlacement["components"];
84
+ /**
85
+ * Default attribution params applied when building outbound sharing URLs.
86
+ * Per-call overrides win over these backend defaults; `utm_content` is
87
+ * intentionally excluded (per-content/per-product, never a merchant default).
88
+ */
89
+ attribution?: AttributionDefaults;
83
90
  };
84
91
 
85
92
  /**
@@ -125,4 +132,7 @@ export type SdkResolvedConfig = {
125
132
 
126
133
  /** Global component defaults (fallback for placement-level overrides) */
127
134
  components?: ResolvedPlacement["components"];
135
+
136
+ /** Merged attribution defaults: backend > SDK static config */
137
+ attribution?: AttributionDefaults;
128
138
  };
@@ -1,5 +1,6 @@
1
1
  import type { InteractionTypeKey } from "../../constants/interactionTypes";
2
2
  import type { I18nConfig } from "../config";
3
+ import type { AttributionParams } from "../tracking";
3
4
 
4
5
  /**
5
6
  * Product information to display on the sharing page
@@ -19,6 +20,11 @@ export type SharingPageProduct = {
19
20
  * When provided and the product is selected, this link is used instead of the default sharing link
20
21
  */
21
22
  link?: string;
23
+ /**
24
+ * Optional `utm_content` value to apply when this product is selected.
25
+ * Falls back to the page-level `attribution.utmContent` when omitted.
26
+ */
27
+ utmContent?: string;
22
28
  };
23
29
 
24
30
  /**
@@ -37,6 +43,18 @@ export type DisplaySharingPageParamsType = {
37
43
  * If not provided, the sharing link will be generated from the current page URL + merchant context
38
44
  */
39
45
  link?: string;
46
+ /**
47
+ * Optional attribution overrides for the outbound sharing URL.
48
+ *
49
+ * When provided (even as an empty object), Frak adds standard affiliation
50
+ * params (`utm_source=frak`, `utm_medium=referral`, `utm_campaign=<merchantId>`,
51
+ * `ref=<clientId>`, `via=frak`) alongside `fCtx`. Existing UTMs on the base
52
+ * URL are preserved (gap-fill). Set this to `null` to disable attribution
53
+ * params entirely (only `fCtx` is added).
54
+ *
55
+ * @default {} — defaults applied
56
+ */
57
+ attribution?: AttributionParams | null;
40
58
  /**
41
59
  * Optional metadata overrides for the sharing page
42
60
  */
@@ -11,7 +11,7 @@ import type { Address } from "viem";
11
11
  export type SendInteractionParamsType =
12
12
  | {
13
13
  type: "arrival";
14
- /** @deprecated V1 legacy use referrerClientId for v2 */
14
+ /** Sharer wallet address. Accepted in both V1 (legacy, wallet-only) and V2 (authenticated sharer) contexts. */
15
15
  referrerWallet?: Address;
16
16
  referrerClientId?: string;
17
17
  referrerMerchantId?: string;
@@ -8,8 +8,44 @@ export type UtmParams = {
8
8
  content?: string;
9
9
  };
10
10
 
11
+ /**
12
+ * Attribution parameters appended to outbound sharing URLs.
13
+ *
14
+ * Defaults are derived from the V2 Frak context when available:
15
+ * - `utmSource`: `"frak"`
16
+ * - `utmMedium`: `"referral"`
17
+ * - `utmCampaign`: merchantId (`context.m`)
18
+ * - `via`: `"frak"`
19
+ * - `ref`: clientId (`context.c`)
20
+ *
21
+ * Fields explicitly set here override the defaults. Existing params on the
22
+ * base URL are preserved (gap-fill policy) to respect merchant-provided UTMs.
23
+ */
24
+ export type AttributionParams = {
25
+ utmSource?: string;
26
+ utmMedium?: string;
27
+ utmCampaign?: string;
28
+ utmContent?: string;
29
+ utmTerm?: string;
30
+ via?: string;
31
+ ref?: string;
32
+ };
33
+
34
+ /**
35
+ * Merchant-level attribution defaults.
36
+ *
37
+ * Same shape as {@link AttributionParams} minus `utmContent`, because
38
+ * `utm_content` describes the specific content/creative being shared and is
39
+ * inherently per-call or per-product (never a merchant-wide default).
40
+ *
41
+ * Used as the shape for both:
42
+ * - `FrakWalletSdkConfig.attribution` (SDK-side compile-time defaults)
43
+ * - Backend merchant-config attribution (dashboard-driven defaults)
44
+ */
45
+ export type AttributionDefaults = Omit<AttributionParams, "utmContent">;
46
+
11
47
  export type TrackArrivalParams = {
12
- /** @deprecated V1 legacy use referrerClientId for v2 contexts */
48
+ /** Sharer wallet address. Accepted in both V1 (legacy) and V2 (authenticated sharer) contexts. */
13
49
  referrerWallet?: Address;
14
50
  referrerClientId?: string;
15
51
  referrerMerchantId?: string;