@frak-labs/core-sdk 0.2.1 → 1.0.0-beta.0cd79998
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.
- package/README.md +1 -2
- package/cdn/bundle.js +3 -3
- package/dist/actions-BMTVobuH.js +1 -0
- package/dist/actions-ukNCM0d7.cjs +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -3
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-BCwGNRmk.d.cts} +144 -58
- package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → index-BfmJnxzo.d.ts} +144 -58
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-CVnwk1E_.d.cts} +122 -8
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-DZuYiI2M.d.ts} +122 -8
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{openSso-B0g7-807.d.cts → openSso-BQ-q-_Y9.d.ts} +373 -46
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-CMBCbhvP.d.cts} +372 -45
- package/dist/src-Cx0RZEA3.js +13 -0
- package/dist/src-DmYZ4ZLk.cjs +13 -0
- package/dist/trackEvent-B5xo_5K3.cjs +1 -0
- package/dist/trackEvent-DdykyX0U.js +1 -0
- package/package.json +12 -13
- package/src/actions/displayEmbeddedWallet.ts +6 -2
- package/src/actions/displayModal.ts +6 -2
- package/src/actions/displaySharingPage.ts +49 -0
- package/src/actions/ensureIdentity.ts +2 -2
- package/src/actions/getMerchantInformation.test.ts +13 -1
- package/src/actions/getMerchantInformation.ts +20 -5
- package/src/actions/getMergeToken.ts +33 -0
- package/src/actions/getUserReferralStatus.ts +42 -0
- package/src/actions/index.ts +8 -1
- package/src/actions/referral/setupReferral.test.ts +79 -0
- package/src/actions/referral/setupReferral.ts +32 -0
- package/src/actions/trackPurchaseStatus.test.ts +32 -20
- package/src/actions/trackPurchaseStatus.ts +3 -5
- package/src/actions/wrapper/modalBuilder.test.ts +4 -2
- package/src/actions/wrapper/modalBuilder.ts +6 -8
- package/src/clients/createIFrameFrakClient.ts +162 -27
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +35 -53
- package/src/index.ts +21 -4
- package/src/stubs/rrweb.ts +9 -0
- package/src/types/config.ts +19 -3
- package/src/types/index.ts +15 -1
- package/src/types/lifecycle/client.ts +22 -27
- package/src/types/lifecycle/iframe.ts +7 -8
- package/src/types/resolvedConfig.ts +138 -0
- package/src/types/rpc/displaySharingPage.ts +100 -0
- package/src/types/rpc/embedded/index.ts +1 -1
- package/src/types/rpc/interaction.ts +4 -0
- package/src/types/rpc/userReferralStatus.ts +20 -0
- package/src/types/rpc.ts +54 -5
- package/src/types/tracking.ts +36 -0
- package/src/utils/FrakContext.test.ts +144 -0
- package/src/utils/FrakContext.ts +67 -1
- package/src/utils/backendUrl.test.ts +2 -2
- package/src/utils/backendUrl.ts +1 -1
- package/src/utils/cache/index.ts +7 -0
- package/src/utils/cache/lruMap.test.ts +55 -0
- package/src/utils/cache/lruMap.ts +38 -0
- package/src/utils/cache/withCache.test.ts +168 -0
- package/src/utils/cache/withCache.ts +124 -0
- package/src/utils/inAppBrowser.ts +60 -0
- package/src/utils/index.ts +10 -4
- package/src/utils/mergeAttribution.test.ts +153 -0
- package/src/utils/mergeAttribution.ts +75 -0
- package/src/utils/sdkConfigStore.test.ts +405 -0
- package/src/utils/sdkConfigStore.ts +263 -0
- package/src/utils/sso.ts +3 -7
- package/dist/setupClient-BduY6Sym.cjs +0 -13
- package/dist/setupClient-ftmdQ-I8.js +0 -13
- package/dist/siweAuthenticate-BWmI2_TN.cjs +0 -1
- package/dist/siweAuthenticate-zczqxm0a.js +0 -1
- package/dist/trackEvent-CeLFVzZn.js +0 -1
- package/dist/trackEvent-Ew5r5zfI.cjs +0 -1
- package/src/utils/merchantId.test.ts +0 -653
- package/src/utils/merchantId.ts +0 -143
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
|
|
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
|
|
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
|
|
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
|
];
|
package/src/types/tracking.ts
CHANGED
|
@@ -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,150 @@ 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 not add 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
|
+
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=");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should apply default attribution params when attribution is an empty object", () => {
|
|
144
|
+
const result = FrakContextManager.update({
|
|
145
|
+
url,
|
|
146
|
+
context: v2Context,
|
|
147
|
+
attribution: {},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(result).toBeDefined();
|
|
151
|
+
const parsedUrl = new URL(result!);
|
|
152
|
+
expect(parsedUrl.searchParams.get("utm_source")).toBe(
|
|
153
|
+
"frak"
|
|
154
|
+
);
|
|
155
|
+
expect(parsedUrl.searchParams.get("utm_medium")).toBe(
|
|
156
|
+
"referral"
|
|
157
|
+
);
|
|
158
|
+
expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
|
|
159
|
+
v2Context.m
|
|
160
|
+
);
|
|
161
|
+
expect(parsedUrl.searchParams.get("via")).toBe("frak");
|
|
162
|
+
expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
|
|
163
|
+
expect(
|
|
164
|
+
parsedUrl.searchParams.get("utm_content")
|
|
165
|
+
).toBeNull();
|
|
166
|
+
expect(parsedUrl.searchParams.get("utm_term")).toBeNull();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should honor overrides over defaults", () => {
|
|
170
|
+
const result = FrakContextManager.update({
|
|
171
|
+
url,
|
|
172
|
+
context: v2Context,
|
|
173
|
+
attribution: {
|
|
174
|
+
utmSource: "newsletter",
|
|
175
|
+
utmMedium: "email",
|
|
176
|
+
utmCampaign: "spring-sale",
|
|
177
|
+
utmContent: "hero-banner",
|
|
178
|
+
utmTerm: "wallet",
|
|
179
|
+
via: "partner",
|
|
180
|
+
ref: "alice",
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const parsedUrl = new URL(result!);
|
|
185
|
+
expect(parsedUrl.searchParams.get("utm_source")).toBe(
|
|
186
|
+
"newsletter"
|
|
187
|
+
);
|
|
188
|
+
expect(parsedUrl.searchParams.get("utm_medium")).toBe(
|
|
189
|
+
"email"
|
|
190
|
+
);
|
|
191
|
+
expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
|
|
192
|
+
"spring-sale"
|
|
193
|
+
);
|
|
194
|
+
expect(parsedUrl.searchParams.get("utm_content")).toBe(
|
|
195
|
+
"hero-banner"
|
|
196
|
+
);
|
|
197
|
+
expect(parsedUrl.searchParams.get("utm_term")).toBe(
|
|
198
|
+
"wallet"
|
|
199
|
+
);
|
|
200
|
+
expect(parsedUrl.searchParams.get("via")).toBe("partner");
|
|
201
|
+
expect(parsedUrl.searchParams.get("ref")).toBe("alice");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should preserve merchant-provided UTMs on the base URL (gap-fill)", () => {
|
|
205
|
+
const baseUrl =
|
|
206
|
+
"https://example.com/product?utm_source=google&utm_campaign=merchant-spring";
|
|
207
|
+
const result = FrakContextManager.update({
|
|
208
|
+
url: baseUrl,
|
|
209
|
+
context: v2Context,
|
|
210
|
+
attribution: {},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const parsedUrl = new URL(result!);
|
|
214
|
+
// Merchant-provided values preserved
|
|
215
|
+
expect(parsedUrl.searchParams.get("utm_source")).toBe(
|
|
216
|
+
"google"
|
|
217
|
+
);
|
|
218
|
+
expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
|
|
219
|
+
"merchant-spring"
|
|
220
|
+
);
|
|
221
|
+
// Missing ones filled by Frak defaults
|
|
222
|
+
expect(parsedUrl.searchParams.get("utm_medium")).toBe(
|
|
223
|
+
"referral"
|
|
224
|
+
);
|
|
225
|
+
expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should skip fields with empty-string overrides", () => {
|
|
229
|
+
const result = FrakContextManager.update({
|
|
230
|
+
url,
|
|
231
|
+
context: v2Context,
|
|
232
|
+
attribution: { utmContent: "", utmTerm: "" },
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const parsedUrl = new URL(result!);
|
|
236
|
+
expect(parsedUrl.searchParams.has("utm_content")).toBe(
|
|
237
|
+
false
|
|
238
|
+
);
|
|
239
|
+
expect(parsedUrl.searchParams.has("utm_term")).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("should skip context-derived defaults for V1 (no merchantId/clientId)", () => {
|
|
243
|
+
const v1Context: FrakContextV1 = {
|
|
244
|
+
r: "0x1234567890123456789012345678901234567890" as Address,
|
|
245
|
+
};
|
|
246
|
+
const result = FrakContextManager.update({
|
|
247
|
+
url,
|
|
248
|
+
context: v1Context,
|
|
249
|
+
attribution: {},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const parsedUrl = new URL(result!);
|
|
253
|
+
// Static defaults still applied
|
|
254
|
+
expect(parsedUrl.searchParams.get("utm_source")).toBe(
|
|
255
|
+
"frak"
|
|
256
|
+
);
|
|
257
|
+
expect(parsedUrl.searchParams.get("utm_medium")).toBe(
|
|
258
|
+
"referral"
|
|
259
|
+
);
|
|
260
|
+
expect(parsedUrl.searchParams.get("via")).toBe("frak");
|
|
261
|
+
// No derivable values from V1
|
|
262
|
+
expect(parsedUrl.searchParams.has("utm_campaign")).toBe(
|
|
263
|
+
false
|
|
264
|
+
);
|
|
265
|
+
expect(parsedUrl.searchParams.has("ref")).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
124
268
|
});
|
|
125
269
|
});
|
|
126
270
|
|
package/src/utils/FrakContext.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { type Address, bytesToHex, hexToBytes, isAddress } from "viem";
|
|
2
|
-
import type {
|
|
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
|
+
* 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.
|
|
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. Omit to skip UTM/ref params.
|
|
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
|
|
|
@@ -15,13 +15,13 @@ describe("getBackendUrl", () => {
|
|
|
15
15
|
describe("with explicit walletUrl", () => {
|
|
16
16
|
test("should return localhost backend for localhost:3000", () => {
|
|
17
17
|
expect(getBackendUrl("https://localhost:3000")).toBe(
|
|
18
|
-
"
|
|
18
|
+
"https://localhost:3030"
|
|
19
19
|
);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
test("should return localhost backend for localhost:3010", () => {
|
|
23
23
|
expect(getBackendUrl("https://localhost:3010")).toBe(
|
|
24
|
-
"
|
|
24
|
+
"https://localhost:3030"
|
|
25
25
|
);
|
|
26
26
|
});
|
|
27
27
|
|
package/src/utils/backendUrl.ts
CHANGED
|
@@ -19,7 +19,7 @@ function isLocalDevelopment(walletUrl: string): boolean {
|
|
|
19
19
|
*/
|
|
20
20
|
function deriveBackendUrl(walletUrl: string): string {
|
|
21
21
|
if (isLocalDevelopment(walletUrl)) {
|
|
22
|
-
return "
|
|
22
|
+
return "https://localhost:3030";
|
|
23
23
|
}
|
|
24
24
|
// Dev environment
|
|
25
25
|
if (
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "../../../tests/vitest-fixtures";
|
|
2
|
+
import { LruMap } from "./lruMap";
|
|
3
|
+
|
|
4
|
+
describe("LruMap", () => {
|
|
5
|
+
it("should store and retrieve values", () => {
|
|
6
|
+
const map = new LruMap<number>(3);
|
|
7
|
+
map.set("a", 1);
|
|
8
|
+
map.set("b", 2);
|
|
9
|
+
|
|
10
|
+
expect(map.get("a")).toBe(1);
|
|
11
|
+
expect(map.get("b")).toBe(2);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("should evict least recently used when exceeding max size", () => {
|
|
15
|
+
const map = new LruMap<number>(2);
|
|
16
|
+
map.set("a", 1);
|
|
17
|
+
map.set("b", 2);
|
|
18
|
+
map.set("c", 3); // Should evict "a"
|
|
19
|
+
|
|
20
|
+
expect(map.get("a")).toBeUndefined();
|
|
21
|
+
expect(map.get("b")).toBe(2);
|
|
22
|
+
expect(map.get("c")).toBe(3);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should promote accessed keys to most recently used", () => {
|
|
26
|
+
const map = new LruMap<number>(2);
|
|
27
|
+
map.set("a", 1);
|
|
28
|
+
map.set("b", 2);
|
|
29
|
+
|
|
30
|
+
// Access "a" to promote it
|
|
31
|
+
map.get("a");
|
|
32
|
+
|
|
33
|
+
// "b" is now least recently used, should be evicted
|
|
34
|
+
map.set("c", 3);
|
|
35
|
+
|
|
36
|
+
expect(map.get("a")).toBe(1);
|
|
37
|
+
expect(map.get("b")).toBeUndefined();
|
|
38
|
+
expect(map.get("c")).toBe(3);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should overwrite existing keys without increasing size", () => {
|
|
42
|
+
const map = new LruMap<number>(2);
|
|
43
|
+
map.set("a", 1);
|
|
44
|
+
map.set("b", 2);
|
|
45
|
+
map.set("a", 10); // Overwrite, not a new entry
|
|
46
|
+
|
|
47
|
+
expect(map.size).toBe(2);
|
|
48
|
+
expect(map.get("a")).toBe(10);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return undefined for missing keys", () => {
|
|
52
|
+
const map = new LruMap<number>(2);
|
|
53
|
+
expect(map.get("missing")).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map with a LRU (Least Recently Used) eviction policy.
|
|
3
|
+
*
|
|
4
|
+
* When the map exceeds `maxSize`, the least recently accessed entry is removed.
|
|
5
|
+
* Accessing a key via `get()` promotes it to "most recently used".
|
|
6
|
+
*
|
|
7
|
+
* Adapted from viem's LruMap utility.
|
|
8
|
+
* @link https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU
|
|
9
|
+
*/
|
|
10
|
+
export class LruMap<TValue = unknown> extends Map<string, TValue> {
|
|
11
|
+
maxSize: number;
|
|
12
|
+
|
|
13
|
+
constructor(size: number) {
|
|
14
|
+
super();
|
|
15
|
+
this.maxSize = size;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override get(key: string) {
|
|
19
|
+
const value = super.get(key);
|
|
20
|
+
if (super.has(key)) {
|
|
21
|
+
// Move to end (most recently used)
|
|
22
|
+
super.delete(key);
|
|
23
|
+
super.set(key, value as TValue);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
override set(key: string, value: TValue) {
|
|
29
|
+
if (super.has(key)) super.delete(key);
|
|
30
|
+
super.set(key, value);
|
|
31
|
+
// Evict least recently used if over capacity
|
|
32
|
+
if (this.maxSize && this.size > this.maxSize) {
|
|
33
|
+
const firstKey = super.keys().next().value;
|
|
34
|
+
if (firstKey !== undefined) super.delete(firstKey);
|
|
35
|
+
}
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
}
|