@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.61e6fb99

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 (92) hide show
  1. package/README.md +1 -2
  2. package/cdn/bundle.js +3 -3
  3. package/dist/actions-Di4welXI.cjs +1 -0
  4. package/dist/actions-DyMkUe65.js +1 -0
  5. package/dist/actions.cjs +1 -1
  6. package/dist/actions.d.cts +3 -3
  7. package/dist/actions.d.ts +3 -3
  8. package/dist/actions.js +1 -1
  9. package/dist/bundle.cjs +1 -1
  10. package/dist/bundle.d.cts +4 -4
  11. package/dist/bundle.d.ts +4 -4
  12. package/dist/bundle.js +1 -1
  13. package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → index-B_Uj-puh.d.ts} +249 -73
  14. package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-ByVpu25D.d.cts} +249 -73
  15. package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-CGyEOo9J.d.cts} +122 -8
  16. package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Cdf5j2_W.d.ts} +122 -8
  17. package/dist/index.cjs +1 -1
  18. package/dist/index.d.cts +3 -3
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.js +1 -1
  21. package/dist/{openSso-B0g7-807.d.cts → openSso-B6pD2oA6.d.ts} +380 -46
  22. package/dist/{openSso-CMzwvaCa.d.ts → openSso-qjaccFd0.d.cts} +379 -45
  23. package/dist/sdkConfigStore-DvwFc6Ym.cjs +1 -0
  24. package/dist/sdkConfigStore-M37skmM8.js +1 -0
  25. package/dist/src-BqpqVHCq.cjs +13 -0
  26. package/dist/src-BxRYON49.js +13 -0
  27. package/package.json +12 -13
  28. package/src/actions/displayEmbeddedWallet.ts +6 -2
  29. package/src/actions/displayModal.ts +6 -2
  30. package/src/actions/displaySharingPage.ts +49 -0
  31. package/src/actions/ensureIdentity.ts +2 -2
  32. package/src/actions/getMerchantInformation.test.ts +13 -1
  33. package/src/actions/getMerchantInformation.ts +20 -5
  34. package/src/actions/getMergeToken.ts +33 -0
  35. package/src/actions/getUserReferralStatus.ts +42 -0
  36. package/src/actions/index.ts +8 -1
  37. package/src/actions/referral/processReferral.test.ts +4 -8
  38. package/src/actions/referral/processReferral.ts +5 -11
  39. package/src/actions/referral/setupReferral.test.ts +79 -0
  40. package/src/actions/referral/setupReferral.ts +32 -0
  41. package/src/actions/trackPurchaseStatus.test.ts +32 -20
  42. package/src/actions/trackPurchaseStatus.ts +3 -5
  43. package/src/actions/wrapper/modalBuilder.test.ts +4 -2
  44. package/src/actions/wrapper/modalBuilder.ts +6 -8
  45. package/src/clients/createIFrameFrakClient.ts +233 -28
  46. package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
  47. package/src/clients/transports/iframeLifecycleManager.ts +35 -53
  48. package/src/index.ts +25 -5
  49. package/src/stubs/rrweb.ts +9 -0
  50. package/src/types/config.ts +19 -3
  51. package/src/types/index.ts +15 -1
  52. package/src/types/lifecycle/client.ts +29 -27
  53. package/src/types/lifecycle/iframe.ts +7 -8
  54. package/src/types/resolvedConfig.ts +138 -0
  55. package/src/types/rpc/displaySharingPage.ts +100 -0
  56. package/src/types/rpc/embedded/index.ts +1 -1
  57. package/src/types/rpc/interaction.ts +4 -0
  58. package/src/types/rpc/userReferralStatus.ts +20 -0
  59. package/src/types/rpc.ts +54 -5
  60. package/src/types/tracking.ts +36 -0
  61. package/src/utils/FrakContext.test.ts +151 -0
  62. package/src/utils/FrakContext.ts +67 -1
  63. package/src/utils/analytics/events/component.ts +58 -0
  64. package/src/utils/analytics/events/index.ts +20 -0
  65. package/src/utils/analytics/events/lifecycle.ts +26 -0
  66. package/src/utils/analytics/events/referral.ts +10 -0
  67. package/src/utils/analytics/index.ts +8 -0
  68. package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
  69. package/src/utils/analytics/trackEvent.ts +34 -0
  70. package/src/utils/backendUrl.test.ts +2 -2
  71. package/src/utils/backendUrl.ts +1 -1
  72. package/src/utils/cache/index.ts +7 -0
  73. package/src/utils/cache/lruMap.test.ts +55 -0
  74. package/src/utils/cache/lruMap.ts +38 -0
  75. package/src/utils/cache/withCache.test.ts +168 -0
  76. package/src/utils/cache/withCache.ts +124 -0
  77. package/src/utils/inAppBrowser.ts +60 -0
  78. package/src/utils/index.ts +11 -5
  79. package/src/utils/mergeAttribution.test.ts +153 -0
  80. package/src/utils/mergeAttribution.ts +75 -0
  81. package/src/utils/sdkConfigStore.test.ts +405 -0
  82. package/src/utils/sdkConfigStore.ts +263 -0
  83. package/src/utils/sso.ts +3 -7
  84. package/dist/setupClient-BduY6Sym.cjs +0 -13
  85. package/dist/setupClient-ftmdQ-I8.js +0 -13
  86. package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
  87. package/dist/siweAuthenticate-zczqxm0a.js +0 -1
  88. package/dist/trackEvent-CeLFVzZn.js +0 -1
  89. package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
  90. package/src/utils/merchantId.test.ts +0 -653
  91. package/src/utils/merchantId.ts +0 -143
  92. package/src/utils/trackEvent.ts +0 -41
@@ -0,0 +1,100 @@
1
+ import type { InteractionTypeKey } from "../../constants/interactionTypes";
2
+ import type { I18nConfig } from "../config";
3
+ import type { AttributionParams } from "../tracking";
4
+
5
+ /**
6
+ * Product information to display on the sharing page
7
+ * @group Sharing Page
8
+ */
9
+ export type SharingPageProduct = {
10
+ /**
11
+ * The product title / name
12
+ */
13
+ title: string;
14
+ /**
15
+ * Optional product image URL
16
+ */
17
+ imageUrl?: string;
18
+ /**
19
+ * Optional product-specific sharing link
20
+ * When provided and the product is selected, this link is used instead of the default sharing link
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;
28
+ };
29
+
30
+ /**
31
+ * Parameters to display the sharing page
32
+ * @group Sharing Page
33
+ * @group RPC Schema
34
+ */
35
+ export type DisplaySharingPageParamsType = {
36
+ /**
37
+ * Products to showcase on the sharing page
38
+ * If provided, they will be displayed in a product card section
39
+ */
40
+ products?: SharingPageProduct[];
41
+ /**
42
+ * Optional link override for sharing
43
+ * If not provided, the sharing link will be generated from the current page URL + merchant context
44
+ */
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;
58
+ /**
59
+ * Optional metadata overrides for the sharing page
60
+ */
61
+ metadata?: {
62
+ /**
63
+ * Logo override for the sharing page header
64
+ */
65
+ logo?: string;
66
+ /**
67
+ * Link to the homepage of the calling website
68
+ */
69
+ homepageLink?: string;
70
+ /**
71
+ * The target interaction behind this sharing page
72
+ */
73
+ targetInteraction?: InteractionTypeKey;
74
+ /**
75
+ * i18n overrides for the sharing page
76
+ */
77
+ i18n?: I18nConfig;
78
+ };
79
+ };
80
+
81
+ /**
82
+ * Result of the sharing page display
83
+ * @group Sharing Page
84
+ * @group RPC Schema
85
+ */
86
+ export type DisplaySharingPageResultType = {
87
+ /**
88
+ * The action the user took
89
+ * - "shared": User used the native share dialog
90
+ * - "copied": User copied the link to clipboard
91
+ * - "dismissed": User dismissed the sharing page without acting
92
+ */
93
+ action: "shared" | "copied" | "dismissed";
94
+ /**
95
+ * The install URL for the Frak app
96
+ * Can be used as a fallback to redirect the user to the install page
97
+ * from the merchant's top-level page (e.g. via `window.location.href`)
98
+ */
99
+ installUrl?: string;
100
+ };
@@ -9,10 +9,10 @@ import type {
9
9
  import type { LoggedOutEmbeddedView } from "./loggedOut";
10
10
 
11
11
  export type {
12
+ EmbeddedViewActionReferred,
12
13
  EmbeddedViewActionSharing,
13
14
  LoggedInEmbeddedView,
14
15
  LoggedOutEmbeddedView,
15
- EmbeddedViewActionReferred,
16
16
  };
17
17
 
18
18
  /**
@@ -26,6 +26,10 @@ export type SendInteractionParamsType =
26
26
  }
27
27
  | {
28
28
  type: "sharing";
29
+ /** Epoch seconds timestamp matching the V2 context `t` field embedded in the referral link URL, used for backend correlation */
30
+ sharingTimestamp?: number;
31
+ /** Merchant order ID linking this sharing event to a purchase (stays server-side, never in URL) */
32
+ purchaseId?: string;
29
33
  }
30
34
  | {
31
35
  type: "custom";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * User referral status returned by `frak_getUserReferralStatus`.
3
+ *
4
+ * Generic referral context for the current user on a merchant.
5
+ * Used by components like `<frak-post-purchase>` and `<frak-referred-banner>`
6
+ * to adapt their display based on the user's referral relationship.
7
+ *
8
+ * Returns `null` when the user's identity cannot be resolved
9
+ * (e.g. no clientId and no wallet session).
10
+ *
11
+ * @group RPC Schema
12
+ */
13
+ export type UserReferralStatusType = {
14
+ /**
15
+ * Whether the user was referred to this merchant by someone else.
16
+ *
17
+ * `true` means a referral link exists where this user is the referee.
18
+ */
19
+ isReferred: boolean;
20
+ };
package/src/types/rpc.ts CHANGED
@@ -4,6 +4,10 @@ import type {
4
4
  ModalRpcStepsInput,
5
5
  ModalRpcStepsResultType,
6
6
  } from "./rpc/displayModal";
7
+ import type {
8
+ DisplaySharingPageParamsType,
9
+ DisplaySharingPageResultType,
10
+ } from "./rpc/displaySharingPage";
7
11
  import type {
8
12
  DisplayEmbeddedWalletParamsType,
9
13
  DisplayEmbeddedWalletResultType,
@@ -16,6 +20,7 @@ import type {
16
20
  PrepareSsoParamsType,
17
21
  PrepareSsoReturnType,
18
22
  } from "./rpc/sso";
23
+ import type { UserReferralStatusType } from "./rpc/userReferralStatus";
19
24
  import type { WalletStatusReturnType } from "./rpc/walletStatus";
20
25
 
21
26
  /**
@@ -38,7 +43,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
38
43
  * - Response Type: stream (emits updates when wallet status changes)
39
44
  *
40
45
  * #### frak_displayModal
41
- * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"]]
46
+ * - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
42
47
  * - Returns: {@link ModalRpcStepsResultType}
43
48
  * - Response Type: promise (one-shot)
44
49
  *
@@ -53,9 +58,14 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
53
58
  * - Response Type: promise (one-shot)
54
59
  *
55
60
  * #### frak_displayEmbeddedWallet
56
- * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"]]
61
+ * - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
57
62
  * - Returns: {@link DisplayEmbeddedWalletResultType}
58
63
  * - Response Type: promise (one-shot)
64
+ *
65
+ * #### frak_displaySharingPage
66
+ * - Params: [request: {@link DisplaySharingPageParamsType}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
67
+ * - Returns: {@link DisplaySharingPageResultType}
68
+ * - Response Type: promise (one-shot)
59
69
  */
60
70
  export type IFrameRpcSchema = [
61
71
  /**
@@ -77,6 +87,7 @@ export type IFrameRpcSchema = [
77
87
  requests: ModalRpcStepsInput,
78
88
  metadata: ModalRpcMetadata | undefined,
79
89
  configMetadata: FrakWalletSdkConfig["metadata"],
90
+ placement?: string,
80
91
  ];
81
92
  ReturnType: ModalRpcStepsResultType;
82
93
  },
@@ -89,7 +100,7 @@ export type IFrameRpcSchema = [
89
100
  Method: "frak_prepareSso";
90
101
  Parameters: [
91
102
  params: PrepareSsoParamsType,
92
- name: string,
103
+ name?: string,
93
104
  customCss?: string,
94
105
  ];
95
106
  ReturnType: PrepareSsoReturnType;
@@ -104,7 +115,7 @@ export type IFrameRpcSchema = [
104
115
  Method: "frak_openSso";
105
116
  Parameters: [
106
117
  params: OpenSsoParamsType,
107
- name: string,
118
+ name?: string,
108
119
  customCss?: string,
109
120
  ];
110
121
  ReturnType: OpenSsoReturnType;
@@ -130,6 +141,7 @@ export type IFrameRpcSchema = [
130
141
  Parameters: [
131
142
  request: DisplayEmbeddedWalletParamsType,
132
143
  metadata: FrakWalletSdkConfig["metadata"],
144
+ placement?: string,
133
145
  ];
134
146
  ReturnType: DisplayEmbeddedWalletResultType;
135
147
  },
@@ -137,7 +149,7 @@ export type IFrameRpcSchema = [
137
149
  * Method to send interactions (arrival, sharing, custom events)
138
150
  * Fire-and-forget method - no return value expected
139
151
  * merchantId is resolved from context
140
- * clientId is passed via metadata as safeguard against handshake race condition
152
+ * clientId is passed via metadata as safeguard against race conditions
141
153
  */
142
154
  {
143
155
  Method: "frak_sendInteraction";
@@ -147,4 +159,41 @@ export type IFrameRpcSchema = [
147
159
  ];
148
160
  ReturnType: undefined;
149
161
  },
162
+ /**
163
+ * Method to get the current user's referral status on this merchant.
164
+ * Returns whether the user was referred (has a referral link as referee).
165
+ * Returns null when the user's identity cannot be resolved.
166
+ * This is a one-shot request.
167
+ */
168
+ {
169
+ Method: "frak_getUserReferralStatus";
170
+ Parameters?: undefined;
171
+ ReturnType: UserReferralStatusType | null;
172
+ },
173
+ /**
174
+ * Method to display a sharing page with product info and sharing buttons
175
+ * Resolves on first user action (share/copy) but the page stays visible
176
+ * This is a one-shot request
177
+ */
178
+ {
179
+ Method: "frak_displaySharingPage";
180
+ Parameters: [
181
+ request: DisplaySharingPageParamsType,
182
+ configMetadata: FrakWalletSdkConfig["metadata"],
183
+ placement?: string,
184
+ ];
185
+ ReturnType: DisplaySharingPageResultType;
186
+ },
187
+ /**
188
+ * Method to get a merge token for the current anonymous identity.
189
+ * Used by in-app browser redirect flows to preserve identity
190
+ * when switching from a WebView to the system browser.
191
+ * Returns the merge token string, or null if unavailable.
192
+ * This is a one-shot request.
193
+ */
194
+ {
195
+ Method: "frak_getMergeToken";
196
+ Parameters?: undefined;
197
+ ReturnType: string | null;
198
+ },
150
199
  ];
@@ -8,6 +8,42 @@ 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
48
  /** @deprecated V1 legacy — use referrerClientId for v2 contexts */
13
49
  referrerWallet?: Address;
@@ -121,6 +121,157 @@ describe("FrakContextManager", () => {
121
121
  expect(result).toContain("baz=qux");
122
122
  expect(result).toContain("fCtx=");
123
123
  });
124
+
125
+ describe("update with attribution", () => {
126
+ const url = "https://example.com/product";
127
+
128
+ it("should apply default attribution params when attribution is omitted", () => {
129
+ const result = FrakContextManager.update({
130
+ url,
131
+ context: v2Context,
132
+ });
133
+
134
+ expect(result).toBeDefined();
135
+ expect(result).toContain("fCtx=");
136
+ const parsedUrl = new URL(result!);
137
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
138
+ "frak"
139
+ );
140
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
141
+ "referral"
142
+ );
143
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
144
+ v2Context.m
145
+ );
146
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
147
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
148
+ });
149
+
150
+ it("should apply default attribution params when attribution is an empty object", () => {
151
+ const result = FrakContextManager.update({
152
+ url,
153
+ context: v2Context,
154
+ attribution: {},
155
+ });
156
+
157
+ expect(result).toBeDefined();
158
+ const parsedUrl = new URL(result!);
159
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
160
+ "frak"
161
+ );
162
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
163
+ "referral"
164
+ );
165
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
166
+ v2Context.m
167
+ );
168
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
169
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
170
+ expect(
171
+ parsedUrl.searchParams.get("utm_content")
172
+ ).toBeNull();
173
+ expect(parsedUrl.searchParams.get("utm_term")).toBeNull();
174
+ });
175
+
176
+ it("should honor overrides over defaults", () => {
177
+ const result = FrakContextManager.update({
178
+ url,
179
+ context: v2Context,
180
+ attribution: {
181
+ utmSource: "newsletter",
182
+ utmMedium: "email",
183
+ utmCampaign: "spring-sale",
184
+ utmContent: "hero-banner",
185
+ utmTerm: "wallet",
186
+ via: "partner",
187
+ ref: "alice",
188
+ },
189
+ });
190
+
191
+ const parsedUrl = new URL(result!);
192
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
193
+ "newsletter"
194
+ );
195
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
196
+ "email"
197
+ );
198
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
199
+ "spring-sale"
200
+ );
201
+ expect(parsedUrl.searchParams.get("utm_content")).toBe(
202
+ "hero-banner"
203
+ );
204
+ expect(parsedUrl.searchParams.get("utm_term")).toBe(
205
+ "wallet"
206
+ );
207
+ expect(parsedUrl.searchParams.get("via")).toBe("partner");
208
+ expect(parsedUrl.searchParams.get("ref")).toBe("alice");
209
+ });
210
+
211
+ it("should preserve merchant-provided UTMs on the base URL (gap-fill)", () => {
212
+ const baseUrl =
213
+ "https://example.com/product?utm_source=google&utm_campaign=merchant-spring";
214
+ const result = FrakContextManager.update({
215
+ url: baseUrl,
216
+ context: v2Context,
217
+ attribution: {},
218
+ });
219
+
220
+ const parsedUrl = new URL(result!);
221
+ // Merchant-provided values preserved
222
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
223
+ "google"
224
+ );
225
+ expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
226
+ "merchant-spring"
227
+ );
228
+ // Missing ones filled by Frak defaults
229
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
230
+ "referral"
231
+ );
232
+ expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
233
+ });
234
+
235
+ it("should skip fields with empty-string overrides", () => {
236
+ const result = FrakContextManager.update({
237
+ url,
238
+ context: v2Context,
239
+ attribution: { utmContent: "", utmTerm: "" },
240
+ });
241
+
242
+ const parsedUrl = new URL(result!);
243
+ expect(parsedUrl.searchParams.has("utm_content")).toBe(
244
+ false
245
+ );
246
+ expect(parsedUrl.searchParams.has("utm_term")).toBe(false);
247
+ });
248
+
249
+ it("should skip context-derived defaults for V1 (no merchantId/clientId)", () => {
250
+ const v1Context: FrakContextV1 = {
251
+ r: "0x1234567890123456789012345678901234567890" as Address,
252
+ };
253
+ const result = FrakContextManager.update({
254
+ url,
255
+ context: v1Context,
256
+ attribution: {},
257
+ });
258
+
259
+ const parsedUrl = new URL(result!);
260
+ // Static defaults still applied
261
+ expect(parsedUrl.searchParams.get("utm_source")).toBe(
262
+ "frak"
263
+ );
264
+ expect(parsedUrl.searchParams.get("utm_medium")).toBe(
265
+ "referral"
266
+ );
267
+ expect(parsedUrl.searchParams.get("via")).toBe("frak");
268
+ // No derivable values from V1
269
+ expect(parsedUrl.searchParams.has("utm_campaign")).toBe(
270
+ false
271
+ );
272
+ expect(parsedUrl.searchParams.has("ref")).toBe(false);
273
+ });
274
+ });
124
275
  });
125
276
  });
126
277
 
@@ -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";
@@ -91,20 +96,80 @@ function parse({ url }: { url: string }): FrakContext | null | undefined {
91
96
  return decompress(frakContext);
92
97
  }
93
98
 
99
+ /**
100
+ * Default UTM medium value when attribution is requested.
101
+ */
102
+ const DEFAULT_UTM_MEDIUM = "referral";
103
+
104
+ /**
105
+ * Default utm_source / via value when attribution is requested.
106
+ */
107
+ const DEFAULT_ATTRIBUTION_SOURCE = "frak";
108
+
109
+ /**
110
+ * Resolve attribution defaults from the provided context.
111
+ *
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.
115
+ */
116
+ function resolveAttributionValues(
117
+ context: FrakContextV1 | FrakContextV2,
118
+ overrides: AttributionParams
119
+ ): Record<string, string | undefined> {
120
+ const isV2 = isV2Context(context);
121
+ return {
122
+ utm_source: overrides.utmSource ?? DEFAULT_ATTRIBUTION_SOURCE,
123
+ utm_medium: overrides.utmMedium ?? DEFAULT_UTM_MEDIUM,
124
+ utm_campaign: overrides.utmCampaign ?? (isV2 ? context.m : undefined),
125
+ utm_content: overrides.utmContent,
126
+ utm_term: overrides.utmTerm,
127
+ via: overrides.via ?? DEFAULT_ATTRIBUTION_SOURCE,
128
+ ref: overrides.ref ?? (isV2 ? context.c : undefined),
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Append attribution query params to a URL using gap-fill semantics.
134
+ *
135
+ * Existing params on the URL are preserved untouched (so merchant-provided
136
+ * UTMs take precedence). Only missing keys are populated.
137
+ */
138
+ function applyAttributionParams(
139
+ urlObj: URL,
140
+ context: FrakContextV1 | FrakContextV2,
141
+ attribution?: AttributionParams
142
+ ): void {
143
+ const values = resolveAttributionValues(context, attribution ?? {});
144
+ for (const [key, value] of Object.entries(values)) {
145
+ if (value === undefined || value === "") continue;
146
+ if (urlObj.searchParams.has(key)) continue;
147
+ urlObj.searchParams.set(key, value);
148
+ }
149
+ }
150
+
94
151
  /**
95
152
  * Add or replace the `fCtx` query parameter in a URL with the given context.
96
153
  *
154
+ * Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`,
155
+ * `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing
156
+ * params on the URL are preserved, defaults are derived from the context when
157
+ * applicable, and `attribution` overrides take precedence when provided.
158
+ *
97
159
  * @param args
98
160
  * @param args.url - The URL to update
99
161
  * @param args.context - The context to embed (V1 or V2)
162
+ * @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.
100
163
  * @returns The updated URL string, or null on failure
101
164
  */
102
165
  function update({
103
166
  url,
104
167
  context,
168
+ attribution,
105
169
  }: {
106
170
  url?: string;
107
171
  context: FrakContextV1 | FrakContextV2;
172
+ attribution?: AttributionParams;
108
173
  }): string | null {
109
174
  if (!url) return null;
110
175
 
@@ -113,6 +178,7 @@ function update({
113
178
 
114
179
  const urlObj = new URL(url);
115
180
  urlObj.searchParams.set(contextKey, compressedContext);
181
+ applyAttributionParams(urlObj, context, attribution);
116
182
  return urlObj.toString();
117
183
  }
118
184
 
@@ -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
+ };