@frak-labs/core-sdk 1.0.0-beta.0cd79998 → 1.0.0-beta.10dada3f

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 (49) hide show
  1. package/cdn/bundle.js +3 -3
  2. package/dist/actions-Bsl5ub7_.js +1 -0
  3. package/dist/actions-C_B0fn1P.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-BCwGNRmk.d.cts → index-BSGP3dbi.d.ts} +113 -22
  13. package/dist/{index-DZuYiI2M.d.ts → index-Bh0TuKYS.d.ts} +1 -1
  14. package/dist/{index-CVnwk1E_.d.cts → index-DW8xes2o.d.cts} +1 -1
  15. package/dist/{index-BfmJnxzo.d.ts → index-TRJNS6B5.d.cts} +113 -22
  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-BQ-q-_Y9.d.ts → openSso-Bvhy_urG.d.cts} +24 -6
  21. package/dist/{openSso-CMBCbhvP.d.cts → openSso-D2kTUv0-.d.ts} +24 -6
  22. package/dist/sdkConfigStore-B6CkorsU.cjs +1 -0
  23. package/dist/sdkConfigStore-Dx0oAVEO.js +1 -0
  24. package/dist/src-CLF8o8WB.cjs +13 -0
  25. package/dist/src-al3X6r-n.js +13 -0
  26. package/package.json +2 -2
  27. package/src/actions/referral/processReferral.test.ts +73 -8
  28. package/src/actions/referral/processReferral.ts +15 -12
  29. package/src/clients/createIFrameFrakClient.ts +72 -2
  30. package/src/index.ts +4 -1
  31. package/src/types/context.ts +16 -4
  32. package/src/types/lifecycle/client.ts +7 -0
  33. package/src/utils/FrakContext.test.ts +41 -7
  34. package/src/utils/FrakContext.ts +24 -15
  35. package/src/utils/analytics/events/component.ts +58 -0
  36. package/src/utils/analytics/events/index.ts +20 -0
  37. package/src/utils/analytics/events/lifecycle.ts +26 -0
  38. package/src/utils/analytics/events/referral.ts +11 -0
  39. package/src/utils/analytics/index.ts +8 -0
  40. package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
  41. package/src/utils/analytics/trackEvent.ts +34 -0
  42. package/src/utils/index.ts +1 -1
  43. package/dist/actions-BMTVobuH.js +0 -1
  44. package/dist/actions-ukNCM0d7.cjs +0 -1
  45. package/dist/src-Cx0RZEA3.js +0 -13
  46. package/dist/src-DmYZ4ZLk.cjs +0 -13
  47. package/dist/trackEvent-B5xo_5K3.cjs +0 -1
  48. package/dist/trackEvent-DdykyX0U.js +0 -1
  49. package/src/utils/trackEvent.ts +0 -41
@@ -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",
@@ -111,7 +109,14 @@ function isSelfReferral(
111
109
  walletStatus?: WalletStatusReturnType
112
110
  ): boolean {
113
111
  if (isV2Context(frakContext)) {
114
- return getClientId() === frakContext.c;
112
+ // Wallet match takes precedence — it's the strongest signal we have.
113
+ if (frakContext.w && walletStatus?.wallet) {
114
+ return isAddressEqual(frakContext.w, walletStatus.wallet);
115
+ }
116
+ if (frakContext.c) {
117
+ return getClientId() === frakContext.c;
118
+ }
119
+ return false;
115
120
  }
116
121
  if (isV1Context(frakContext) && walletStatus?.wallet) {
117
122
  return isAddressEqual(frakContext.r, walletStatus.wallet);
@@ -177,9 +182,7 @@ export function processReferral(
177
182
  });
178
183
 
179
184
  trackEvent(client, "user_referred_completed", {
180
- properties: {
181
- status: "success",
182
- },
185
+ status: "success",
183
186
  });
184
187
 
185
188
  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
 
@@ -342,7 +381,11 @@ async function postConnectionSetup({
342
381
  );
343
382
  };
344
383
 
345
- // 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.
346
389
  let mergeTokenConsumed = false;
347
390
  const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
348
391
  const token = mergeTokenConsumed ? undefined : pendingMergeToken;
@@ -365,6 +408,17 @@ async function postConnectionSetup({
365
408
  ? { attribution: resolved.attribution }
366
409
  : undefined;
367
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
+ }
421
+
368
422
  rpcClient.sendLifecycle({
369
423
  clientLifecycle: "resolved-config",
370
424
  data: {
@@ -372,6 +426,7 @@ async function postConnectionSetup({
372
426
  domain: resolved.domain ?? "",
373
427
  allowedDomains: resolved.allowedDomains ?? [],
374
428
  sourceUrl: window.location.href,
429
+ ...(sdkAnonymousId && { sdkAnonymousId }),
375
430
  ...(token && { pendingMergeToken: token }),
376
431
  ...(sdkConfig && { sdkConfig }),
377
432
  },
@@ -423,5 +478,20 @@ async function postConnectionSetup({
423
478
  });
424
479
  }
425
480
 
426
- 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
+ }
427
497
  }
package/src/index.ts CHANGED
@@ -102,7 +102,6 @@ export {
102
102
  type DeepLinkFallbackOptions,
103
103
  decompressJsonFromB64,
104
104
  FrakContextManager,
105
- type FrakEvent,
106
105
  type FullSsoParams,
107
106
  findIframeInOpener,
108
107
  formatAmount,
@@ -126,4 +125,8 @@ export {
126
125
  triggerDeepLinkWithFallback,
127
126
  withCache,
128
127
  } from "./utils";
128
+ export type {
129
+ SdkEventMap,
130
+ SdkHandshakeFailureReason,
131
+ } from "./utils/analytics";
129
132
  export { computeLegacyProductId } from "./utils/computeLegacyProductId";
@@ -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
  /**
@@ -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
  };
@@ -42,7 +42,7 @@ describe("FrakContextManager", () => {
42
42
  expect(result).not.toMatch(/[+/=]/);
43
43
  });
44
44
 
45
- it("should return undefined when v2 context is missing clientId", () => {
45
+ it("should return undefined when v2 context has neither clientId nor wallet", () => {
46
46
  const partial = { v: 2 as const, m: "m", t: 123 };
47
47
  const result = FrakContextManager.compress(
48
48
  partial as FrakContextV2
@@ -50,6 +50,33 @@ describe("FrakContextManager", () => {
50
50
  expect(result).toBeUndefined();
51
51
  });
52
52
 
53
+ it("should compress v2 context with wallet only (no clientId)", () => {
54
+ const v2WithWalletOnly: FrakContextV2 = {
55
+ v: 2,
56
+ m: "merchant-uuid-1234",
57
+ t: 1709654400,
58
+ w: "0x1234567890123456789012345678901234567890" as Address,
59
+ };
60
+ const result = FrakContextManager.compress(v2WithWalletOnly);
61
+ expect(result).toBeDefined();
62
+ const decompressed = FrakContextManager.decompress(result);
63
+ expect(decompressed).toEqual(v2WithWalletOnly);
64
+ });
65
+
66
+ it("should compress v2 context with both clientId and wallet", () => {
67
+ const v2Hybrid: FrakContextV2 = {
68
+ v: 2,
69
+ c: "test-client-id-uuid",
70
+ m: "merchant-uuid-1234",
71
+ t: 1709654400,
72
+ w: "0x1234567890123456789012345678901234567890" as Address,
73
+ };
74
+ const result = FrakContextManager.compress(v2Hybrid);
75
+ expect(result).toBeDefined();
76
+ const decompressed = FrakContextManager.decompress(result);
77
+ expect(decompressed).toEqual(v2Hybrid);
78
+ });
79
+
53
80
  it("should return undefined when v2 context is missing merchantId", () => {
54
81
  const partial = { v: 2 as const, c: "c", t: 123 };
55
82
  const result = FrakContextManager.compress(
@@ -125,7 +152,7 @@ describe("FrakContextManager", () => {
125
152
  describe("update with attribution", () => {
126
153
  const url = "https://example.com/product";
127
154
 
128
- it("should not add attribution params when attribution is omitted", () => {
155
+ it("should apply default attribution params when attribution is omitted", () => {
129
156
  const result = FrakContextManager.update({
130
157
  url,
131
158
  context: v2Context,
@@ -133,11 +160,18 @@ describe("FrakContextManager", () => {
133
160
 
134
161
  expect(result).toBeDefined();
135
162
  expect(result).toContain("fCtx=");
136
- expect(result).not.toContain("utm_source");
137
- expect(result).not.toContain("utm_medium");
138
- expect(result).not.toContain("utm_campaign");
139
- expect(result).not.toContain("ref=");
140
- expect(result).not.toContain("via=");
163
+ const parsedUrl = new URL(result!);
164
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
165
+ "frak"
166
+ );
167
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
168
+ "referral"
169
+ );
170
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
171
+ v2Context.m
172
+ );
173
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
174
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
141
175
  });
142
176
 
143
177
  it("should apply default attribution params when attribution is an empty object", () => {
@@ -28,13 +28,16 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
28
28
  if (!context) return;
29
29
  try {
30
30
  if (isV2Context(context)) {
31
- // Runtime validation: all V2 fields must be present and truthy
32
- if (!context.c || !context.m || !context.t) return undefined;
31
+ // Runtime validation: m + t are always required, and at least one of
32
+ // c (anonymous fingerprint) or w (wallet) must be present.
33
+ if (!context.m || !context.t) return undefined;
34
+ if (!context.c && !context.w) return undefined;
33
35
  return compressJsonToB64({
34
36
  v: 2,
35
- c: context.c,
36
37
  m: context.m,
37
38
  t: context.t,
39
+ ...(context.c ? { c: context.c } : {}),
40
+ ...(context.w ? { w: context.w } : {}),
38
41
  });
39
42
  }
40
43
 
@@ -61,10 +64,15 @@ function decompress(context?: string): FrakContext | undefined {
61
64
  // Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
62
65
  const json = decompressJsonFromB64<FrakContextV2>(context);
63
66
  if (json && typeof json === "object" && json.v === 2) {
64
- if (json.c && json.m && json.t) {
65
- return { v: 2, c: json.c, m: json.m, t: json.t };
66
- }
67
- return undefined;
67
+ if (!json.m || !json.t) return undefined;
68
+ if (!json.c && !json.w) return undefined;
69
+ return {
70
+ v: 2,
71
+ m: json.m,
72
+ t: json.t,
73
+ ...(json.c ? { c: json.c } : {}),
74
+ ...(json.w ? { w: json.w } : {}),
75
+ };
68
76
  }
69
77
 
70
78
  // Fall back to V1: raw 20-byte address
@@ -109,9 +117,10 @@ const DEFAULT_ATTRIBUTION_SOURCE = "frak";
109
117
  /**
110
118
  * Resolve attribution defaults from the provided context.
111
119
  *
112
- * V2 contexts expose the merchantId (`m`) and clientId (`c`), which feed
113
- * `utm_campaign` and `ref` respectively. V1 contexts have no equivalent, so
114
- * only the static defaults (`utm_source`, `utm_medium`, `via`) apply.
120
+ * V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId
121
+ * (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries
122
+ * a wallet (`w`), `ref` is intentionally left unset — we don't want wallet
123
+ * addresses leaking into UTM params. V1 contexts have no equivalent.
115
124
  */
116
125
  function resolveAttributionValues(
117
126
  context: FrakContextV1 | FrakContextV2,
@@ -151,15 +160,15 @@ function applyAttributionParams(
151
160
  /**
152
161
  * Add or replace the `fCtx` query parameter in a URL with the given context.
153
162
  *
154
- * When `attribution` is provided (even as an empty object), standard affiliation
155
- * params (`utm_source`, `utm_medium`, `utm_campaign`, `ref`, `via`, ...) are
156
- * also appended using gap-fill semantics: pre-existing params on the URL are
157
- * preserved, and defaults are derived from the context when applicable.
163
+ * Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`,
164
+ * `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing
165
+ * params on the URL are preserved, defaults are derived from the context when
166
+ * applicable, and `attribution` overrides take precedence when provided.
158
167
  *
159
168
  * @param args
160
169
  * @param args.url - The URL to update
161
170
  * @param args.context - The context to embed (V1 or V2)
162
- * @param args.attribution - Optional attribution overrides. Omit to skip UTM/ref params.
171
+ * @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.
163
172
  * @returns The updated URL string, or null on failure
164
173
  */
165
174
  function update({
@@ -0,0 +1,58 @@
1
+ type ButtonBaseProps = {
2
+ placement?: string;
3
+ target_interaction?: string;
4
+ has_reward?: boolean;
5
+ };
6
+
7
+ type BannerVariant = "referral" | "inapp";
8
+ type BannerOutcome = "clicked" | "dismissed";
9
+ type PostPurchaseVariant = "referrer" | "referee";
10
+ type ShareClickAction = "share-modal" | "embedded-wallet" | "sharing-page";
11
+
12
+ export type SdkComponentEventMap = {
13
+ // Share button — click carries the resolved action + reward presence so
14
+ // we can compare per-merchant configuration impact on conversion.
15
+ share_button_clicked: ButtonBaseProps & {
16
+ click_action: ShareClickAction;
17
+ };
18
+ share_modal_error: ButtonBaseProps & {
19
+ error?: string;
20
+ };
21
+
22
+ // Wallet button (floating) — NOT actively used in production. No tracking.
23
+
24
+ // Open in app — path lets us compare deep-link destinations once we add more.
25
+ open_in_app_clicked: {
26
+ placement?: string;
27
+ path: string;
28
+ };
29
+ app_not_installed: {
30
+ placement?: string;
31
+ path: string;
32
+ };
33
+
34
+ // Banner — referral vs in-app variants share the funnel shape.
35
+ banner_impression: {
36
+ placement?: string;
37
+ variant: BannerVariant;
38
+ has_reward?: boolean;
39
+ };
40
+ banner_resolved: {
41
+ placement?: string;
42
+ variant: BannerVariant;
43
+ outcome: BannerOutcome;
44
+ };
45
+
46
+ // Post-purchase — the card drives the highest-intent entry into the
47
+ // referral loop; variant tells us whether we upsold a new share or
48
+ // celebrated an existing referee.
49
+ post_purchase_impression: {
50
+ placement?: string;
51
+ variant: PostPurchaseVariant;
52
+ has_reward?: boolean;
53
+ };
54
+ post_purchase_clicked: {
55
+ placement?: string;
56
+ variant: PostPurchaseVariant;
57
+ };
58
+ };
@@ -0,0 +1,20 @@
1
+ import type { SdkComponentEventMap } from "./component";
2
+ import type { SdkLifecycleEventMap } from "./lifecycle";
3
+ import type { SdkReferralEventMap } from "./referral";
4
+
5
+ export type { SdkComponentEventMap } from "./component";
6
+ export type {
7
+ SdkHandshakeFailureReason,
8
+ SdkLifecycleEventMap,
9
+ } from "./lifecycle";
10
+ export type { SdkReferralEventMap } from "./referral";
11
+
12
+ /**
13
+ * Merged SDK event map. Consumed by the SDK's typed `trackEvent`.
14
+ * Stays isolated from wallet-shared because the SDK ships in partner
15
+ * bundles (different OpenPanel client id, no wallet-shared dependency
16
+ * allowed — see `packages/wallet-shared/AGENTS.md`).
17
+ */
18
+ export type SdkEventMap = SdkLifecycleEventMap &
19
+ SdkComponentEventMap &
20
+ SdkReferralEventMap;
@@ -0,0 +1,26 @@
1
+ export type SdkHandshakeFailureReason =
2
+ | "timeout"
3
+ | "origin"
4
+ | "asset_push"
5
+ | "unknown";
6
+
7
+ export type SdkLifecycleEventMap = {
8
+ sdk_initialized: {
9
+ sdkVersion?: string;
10
+ };
11
+ sdk_iframe_connected: {
12
+ handshake_duration_ms: number;
13
+ };
14
+ sdk_iframe_handshake_failed: {
15
+ reason: SdkHandshakeFailureReason;
16
+ };
17
+ /**
18
+ * Emitted by the CDN bootstrap when `initFrakSdk()` throws before a
19
+ * client is available. Uses a transient OpenPanel instance so broken
20
+ * partner integrations are still captured.
21
+ */
22
+ sdk_init_failed: {
23
+ reason: string;
24
+ config_missing?: boolean;
25
+ };
26
+ };
@@ -0,0 +1,11 @@
1
+ export type SdkReferralEventMap = {
2
+ user_referred_started: {
3
+ referrer?: string;
4
+ referrerClientId?: string;
5
+ referrerWallet?: string;
6
+ walletStatus?: string;
7
+ };
8
+ user_referred_completed: {
9
+ status: "success";
10
+ };
11
+ };
@@ -0,0 +1,8 @@
1
+ export type {
2
+ SdkComponentEventMap,
3
+ SdkEventMap,
4
+ SdkHandshakeFailureReason,
5
+ SdkLifecycleEventMap,
6
+ SdkReferralEventMap,
7
+ } from "./events";
8
+ export { trackEvent } from "./trackEvent";
@@ -10,8 +10,8 @@ import {
10
10
  expect,
11
11
  it,
12
12
  vi,
13
- } from "../../tests/vitest-fixtures";
14
- import type { FrakClient } from "../types";
13
+ } from "../../../tests/vitest-fixtures";
14
+ import type { FrakClient } from "../../types";
15
15
  import { trackEvent } from "./trackEvent";
16
16
 
17
17
  describe("trackEvent", () => {
@@ -42,16 +42,19 @@ describe("trackEvent", () => {
42
42
 
43
43
  expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
44
44
  "share_button_clicked",
45
- {}
45
+ undefined
46
46
  );
47
47
  });
48
48
 
49
49
  it("should track event with props", () => {
50
- const props = { userId: "123", page: "home" };
51
- trackEvent(mockClient, "wallet_button_clicked", props);
50
+ const props = {
51
+ placement: "footer",
52
+ click_action: "share-modal",
53
+ } as const;
54
+ trackEvent(mockClient, "share_button_clicked", props);
52
55
 
53
56
  expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
54
- "wallet_button_clicked",
57
+ "share_button_clicked",
55
58
  props
56
59
  );
57
60
  });
@@ -71,25 +74,18 @@ describe("trackEvent", () => {
71
74
 
72
75
  expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
73
76
  "user_referred_started",
74
- {}
77
+ undefined
75
78
  );
76
79
  });
77
80
 
78
81
  it("should track user_referred_completed event", () => {
79
- trackEvent(mockClient, "user_referred_completed");
82
+ trackEvent(mockClient, "user_referred_completed", {
83
+ status: "success",
84
+ });
80
85
 
81
86
  expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
82
87
  "user_referred_completed",
83
- {}
84
- );
85
- });
86
-
87
- it("should track user_referred_error event", () => {
88
- trackEvent(mockClient, "user_referred_error");
89
-
90
- expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
91
- "user_referred_error",
92
- {}
88
+ { status: "success" }
93
89
  );
94
90
  });
95
91
  });
@@ -102,7 +98,7 @@ describe("trackEvent", () => {
102
98
  });
103
99
 
104
100
  it("should log debug message when client is undefined", () => {
105
- trackEvent(undefined, "wallet_button_clicked");
101
+ trackEvent(undefined, "share_button_clicked");
106
102
 
107
103
  expect(consoleDebugSpy).toHaveBeenCalledWith(
108
104
  "[Frak] No client provided, skipping event tracking"
@@ -143,30 +139,26 @@ describe("trackEvent", () => {
143
139
  const clientWithoutPanel = {} as FrakClient;
144
140
 
145
141
  expect(() => {
146
- trackEvent(clientWithoutPanel, "wallet_button_clicked");
142
+ trackEvent(clientWithoutPanel, "share_button_clicked");
147
143
  }).not.toThrow();
148
144
  });
149
145
  });
150
146
 
151
147
  describe("edge cases", () => {
152
- it("should handle empty props object", () => {
153
- trackEvent(mockClient, "share_button_clicked", {});
148
+ it("should handle typed props object", () => {
149
+ trackEvent(mockClient, "share_button_clicked", {
150
+ click_action: "share-modal",
151
+ });
154
152
 
155
153
  expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
156
154
  "share_button_clicked",
157
- {}
155
+ { click_action: "share-modal" }
158
156
  );
159
157
  });
160
158
 
161
159
  it("should handle complex props object", () => {
162
160
  const complexProps = {
163
- userId: "123",
164
- metadata: {
165
- page: "home",
166
- section: "header",
167
- },
168
- tags: ["tag1", "tag2"],
169
- timestamp: Date.now(),
161
+ status: "success" as const,
170
162
  };
171
163
 
172
164
  trackEvent(mockClient, "user_referred_completed", complexProps);