@frak-labs/core-sdk 1.0.0-beta.7f085fb7 → 1.0.0-beta.a7645eaa

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 (57) hide show
  1. package/cdn/bundle.js +3 -3
  2. package/dist/actions-3g2MTs6f.cjs +1 -0
  3. package/dist/actions-C-vPLo1H.js +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-BphwTmKA.d.cts → index-BWic1g0J.d.cts} +1 -1
  13. package/dist/{index-Dwmo109y.d.cts → index-CNukNJzV.d.ts} +165 -42
  14. package/dist/{index-_f8EuN_1.d.ts → index-Du4nB3qO.d.ts} +1 -1
  15. package/dist/{index-BV5D9DsW.d.ts → index-tYNUZNRd.d.cts} +165 -42
  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-thuozJuB.cjs +1 -0
  23. package/dist/sdkConfigStore-ylgC-Tp0.js +1 -0
  24. package/dist/src-BhrneHv1.js +13 -0
  25. package/dist/src-DkkrSfA4.cjs +13 -0
  26. package/package.json +2 -2
  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 +235 -1
  40. package/src/utils/FrakContext.ts +87 -8
  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/index.ts +5 -1
  49. package/src/utils/mergeAttribution.test.ts +153 -0
  50. package/src/utils/mergeAttribution.ts +75 -0
  51. package/dist/actions-D4aBXbdp.cjs +0 -1
  52. package/dist/actions-Dq_uN-wn.js +0 -1
  53. package/dist/src-BorHuT1g.cjs +0 -13
  54. package/dist/src-aPlQD0ZW.js +0 -13
  55. package/dist/trackEvent-BqJqRZ-u.cjs +0 -1
  56. package/dist/trackEvent-Bqq4jd6R.js +0 -1
  57. package/src/utils/trackEvent.ts +0 -41
@@ -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(
@@ -65,6 +92,40 @@ describe("FrakContextManager", () => {
65
92
  );
66
93
  expect(result).toBeUndefined();
67
94
  });
95
+
96
+ it("should reject v2 context with a malformed wallet address", () => {
97
+ const partial = {
98
+ v: 2 as const,
99
+ m: "merchant-uuid",
100
+ t: 1709654400,
101
+ w: "0xnot-a-valid-address" as Address,
102
+ };
103
+ const result = FrakContextManager.compress(
104
+ partial as FrakContextV2
105
+ );
106
+ // Invalid wallet → falls back to clientId requirement; absent here → undefined
107
+ expect(result).toBeUndefined();
108
+ });
109
+
110
+ it("should drop a malformed wallet but keep a valid clientId", () => {
111
+ const hybrid = {
112
+ v: 2 as const,
113
+ c: "valid-client-id",
114
+ m: "merchant-uuid",
115
+ t: 1709654400,
116
+ w: "0xnot-a-valid-address" as Address,
117
+ };
118
+ const compressed = FrakContextManager.compress(
119
+ hybrid as FrakContextV2
120
+ );
121
+ const decompressed = FrakContextManager.decompress(compressed);
122
+ expect(decompressed).toEqual({
123
+ v: 2,
124
+ c: "valid-client-id",
125
+ m: "merchant-uuid",
126
+ t: 1709654400,
127
+ });
128
+ });
68
129
  });
69
130
 
70
131
  describe("decompress", () => {
@@ -74,6 +135,28 @@ describe("FrakContextManager", () => {
74
135
 
75
136
  expect(decompressed).toEqual(v2Context);
76
137
  });
138
+
139
+ it("should drop malformed `w` field on decompress (defense against crafted URLs)", async () => {
140
+ // Manually compress a payload bypassing client-side validation
141
+ // by reaching into the underlying compressJsonToB64.
142
+ const { compressJsonToB64 } = await import(
143
+ "./compression/compress"
144
+ );
145
+ const malformed = compressJsonToB64({
146
+ v: 2,
147
+ c: "valid-client-id",
148
+ m: "merchant-uuid",
149
+ t: 1709654400,
150
+ w: "0xnot-a-real-address",
151
+ });
152
+ const result = FrakContextManager.decompress(malformed);
153
+ expect(result).toEqual({
154
+ v: 2,
155
+ c: "valid-client-id",
156
+ m: "merchant-uuid",
157
+ t: 1709654400,
158
+ });
159
+ });
77
160
  });
78
161
 
79
162
  describe("parse", () => {
@@ -121,6 +204,157 @@ describe("FrakContextManager", () => {
121
204
  expect(result).toContain("baz=qux");
122
205
  expect(result).toContain("fCtx=");
123
206
  });
207
+
208
+ describe("update with attribution", () => {
209
+ const url = "https://example.com/product";
210
+
211
+ it("should apply default attribution params when attribution is omitted", () => {
212
+ const result = FrakContextManager.update({
213
+ url,
214
+ context: v2Context,
215
+ });
216
+
217
+ expect(result).toBeDefined();
218
+ expect(result).toContain("fCtx=");
219
+ const parsedUrl = new URL(result!);
220
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
221
+ "frak"
222
+ );
223
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
224
+ "referral"
225
+ );
226
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
227
+ v2Context.m
228
+ );
229
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
230
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
231
+ });
232
+
233
+ it("should apply default attribution params when attribution is an empty object", () => {
234
+ const result = FrakContextManager.update({
235
+ url,
236
+ context: v2Context,
237
+ attribution: {},
238
+ });
239
+
240
+ expect(result).toBeDefined();
241
+ const parsedUrl = new URL(result!);
242
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
243
+ "frak"
244
+ );
245
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
246
+ "referral"
247
+ );
248
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
249
+ v2Context.m
250
+ );
251
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
252
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
253
+ expect(
254
+ parsedUrl.searchParams.get("utm_content")
255
+ ).toBeNull();
256
+ expect(parsedUrl.searchParams.get("utm_term")).toBeNull();
257
+ });
258
+
259
+ it("should honor overrides over defaults", () => {
260
+ const result = FrakContextManager.update({
261
+ url,
262
+ context: v2Context,
263
+ attribution: {
264
+ utmSource: "newsletter",
265
+ utmMedium: "email",
266
+ utmCampaign: "spring-sale",
267
+ utmContent: "hero-banner",
268
+ utmTerm: "wallet",
269
+ via: "partner",
270
+ ref: "alice",
271
+ },
272
+ });
273
+
274
+ const parsedUrl = new URL(result!);
275
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
276
+ "newsletter"
277
+ );
278
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
279
+ "email"
280
+ );
281
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
282
+ "spring-sale"
283
+ );
284
+ expect(parsedUrl.searchParams.get("utm_content")).toBe(
285
+ "hero-banner"
286
+ );
287
+ expect(parsedUrl.searchParams.get("utm_term")).toBe(
288
+ "wallet"
289
+ );
290
+ expect(parsedUrl.searchParams.get("via")).toBe("partner");
291
+ expect(parsedUrl.searchParams.get("ref")).toBe("alice");
292
+ });
293
+
294
+ it("should preserve merchant-provided UTMs on the base URL (gap-fill)", () => {
295
+ const baseUrl =
296
+ "https://example.com/product?utm_source=google&utm_campaign=merchant-spring";
297
+ const result = FrakContextManager.update({
298
+ url: baseUrl,
299
+ context: v2Context,
300
+ attribution: {},
301
+ });
302
+
303
+ const parsedUrl = new URL(result!);
304
+ // Merchant-provided values preserved
305
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
306
+ "google"
307
+ );
308
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
309
+ "merchant-spring"
310
+ );
311
+ // Missing ones filled by Frak defaults
312
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
313
+ "referral"
314
+ );
315
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
316
+ });
317
+
318
+ it("should skip fields with empty-string overrides", () => {
319
+ const result = FrakContextManager.update({
320
+ url,
321
+ context: v2Context,
322
+ attribution: { utmContent: "", utmTerm: "" },
323
+ });
324
+
325
+ const parsedUrl = new URL(result!);
326
+ expect(parsedUrl.searchParams.has("utm_content")).toBe(
327
+ false
328
+ );
329
+ expect(parsedUrl.searchParams.has("utm_term")).toBe(false);
330
+ });
331
+
332
+ it("should skip context-derived defaults for V1 (no merchantId/clientId)", () => {
333
+ const v1Context: FrakContextV1 = {
334
+ r: "0x1234567890123456789012345678901234567890" as Address,
335
+ };
336
+ const result = FrakContextManager.update({
337
+ url,
338
+ context: v1Context,
339
+ attribution: {},
340
+ });
341
+
342
+ const parsedUrl = new URL(result!);
343
+ // Static defaults still applied
344
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
345
+ "frak"
346
+ );
347
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
348
+ "referral"
349
+ );
350
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
351
+ // No derivable values from V1
352
+ expect(parsedUrl.searchParams.has("utm_campaign")).toBe(
353
+ false
354
+ );
355
+ expect(parsedUrl.searchParams.has("ref")).toBe(false);
356
+ });
357
+ });
124
358
  });
125
359
  });
126
360
 
@@ -1,5 +1,10 @@
1
1
  import { type Address, bytesToHex, hexToBytes, isAddress } from "viem";
2
- import type { FrakContext, FrakContextV1, FrakContextV2 } from "../types";
2
+ import type {
3
+ AttributionParams,
4
+ FrakContext,
5
+ FrakContextV1,
6
+ FrakContextV2,
7
+ } from "../types";
3
8
  import { isV2Context } from "../types";
4
9
  import { base64urlDecode, base64urlEncode } from "./compression/b64";
5
10
  import { compressJsonToB64 } from "./compression/compress";
@@ -23,13 +28,17 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
23
28
  if (!context) return;
24
29
  try {
25
30
  if (isV2Context(context)) {
26
- // Runtime validation: all V2 fields must be present and truthy
27
- 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 (valid wallet) must be present.
33
+ if (!context.m || !context.t) return undefined;
34
+ const hasValidWallet = context.w && isAddress(context.w);
35
+ if (!context.c && !hasValidWallet) return undefined;
28
36
  return compressJsonToB64({
29
37
  v: 2,
30
- c: context.c,
31
38
  m: context.m,
32
39
  t: context.t,
40
+ ...(context.c ? { c: context.c } : {}),
41
+ ...(hasValidWallet ? { w: context.w } : {}),
33
42
  });
34
43
  }
35
44
 
@@ -56,10 +65,18 @@ function decompress(context?: string): FrakContext | undefined {
56
65
  // Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
57
66
  const json = decompressJsonFromB64<FrakContextV2>(context);
58
67
  if (json && typeof json === "object" && json.v === 2) {
59
- if (json.c && json.m && json.t) {
60
- return { v: 2, c: json.c, m: json.m, t: json.t };
61
- }
62
- return undefined;
68
+ if (!json.m || !json.t) return undefined;
69
+ // Validate `w` with isAddress() protects self-referral checks and
70
+ // URL replacement from crafted payloads carrying a malformed wallet.
71
+ const hasValidWallet = json.w && isAddress(json.w);
72
+ if (!json.c && !hasValidWallet) return undefined;
73
+ return {
74
+ v: 2,
75
+ m: json.m,
76
+ t: json.t,
77
+ ...(json.c ? { c: json.c } : {}),
78
+ ...(hasValidWallet ? { w: json.w } : {}),
79
+ };
63
80
  }
64
81
 
65
82
  // Fall back to V1: raw 20-byte address
@@ -91,20 +108,81 @@ function parse({ url }: { url: string }): FrakContext | null | undefined {
91
108
  return decompress(frakContext);
92
109
  }
93
110
 
111
+ /**
112
+ * Default UTM medium value when attribution is requested.
113
+ */
114
+ const DEFAULT_UTM_MEDIUM = "referral";
115
+
116
+ /**
117
+ * Default utm_source / via value when attribution is requested.
118
+ */
119
+ const DEFAULT_ATTRIBUTION_SOURCE = "frak";
120
+
121
+ /**
122
+ * Resolve attribution defaults from the provided context.
123
+ *
124
+ * V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId
125
+ * (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries
126
+ * a wallet (`w`), `ref` is intentionally left unset — we don't want wallet
127
+ * addresses leaking into UTM params. V1 contexts have no equivalent.
128
+ */
129
+ function resolveAttributionValues(
130
+ context: FrakContextV1 | FrakContextV2,
131
+ overrides: AttributionParams
132
+ ): Record<string, string | undefined> {
133
+ const isV2 = isV2Context(context);
134
+ return {
135
+ utm_source: overrides.utmSource ?? DEFAULT_ATTRIBUTION_SOURCE,
136
+ utm_medium: overrides.utmMedium ?? DEFAULT_UTM_MEDIUM,
137
+ utm_campaign: overrides.utmCampaign ?? (isV2 ? context.m : undefined),
138
+ utm_content: overrides.utmContent,
139
+ utm_term: overrides.utmTerm,
140
+ via: overrides.via ?? DEFAULT_ATTRIBUTION_SOURCE,
141
+ ref: overrides.ref ?? (isV2 ? context.c : undefined),
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Append attribution query params to a URL using gap-fill semantics.
147
+ *
148
+ * Existing params on the URL are preserved untouched (so merchant-provided
149
+ * UTMs take precedence). Only missing keys are populated.
150
+ */
151
+ function applyAttributionParams(
152
+ urlObj: URL,
153
+ context: FrakContextV1 | FrakContextV2,
154
+ attribution?: AttributionParams
155
+ ): void {
156
+ const values = resolveAttributionValues(context, attribution ?? {});
157
+ for (const [key, value] of Object.entries(values)) {
158
+ if (value === undefined || value === "") continue;
159
+ if (urlObj.searchParams.has(key)) continue;
160
+ urlObj.searchParams.set(key, value);
161
+ }
162
+ }
163
+
94
164
  /**
95
165
  * Add or replace the `fCtx` query parameter in a URL with the given context.
96
166
  *
167
+ * Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`,
168
+ * `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing
169
+ * params on the URL are preserved, defaults are derived from the context when
170
+ * applicable, and `attribution` overrides take precedence when provided.
171
+ *
97
172
  * @param args
98
173
  * @param args.url - The URL to update
99
174
  * @param args.context - The context to embed (V1 or V2)
175
+ * @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.
100
176
  * @returns The updated URL string, or null on failure
101
177
  */
102
178
  function update({
103
179
  url,
104
180
  context,
181
+ attribution,
105
182
  }: {
106
183
  url?: string;
107
184
  context: FrakContextV1 | FrakContextV2;
185
+ attribution?: AttributionParams;
108
186
  }): string | null {
109
187
  if (!url) return null;
110
188
 
@@ -113,6 +191,7 @@ function update({
113
191
 
114
192
  const urlObj = new URL(url);
115
193
  urlObj.searchParams.set(contextKey, compressedContext);
194
+ applyAttributionParams(urlObj, context, attribution);
116
195
  return urlObj.toString();
117
196
  }
118
197
 
@@ -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);