@frak-labs/core-sdk 0.2.1 → 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.
- package/README.md +1 -2
- package/cdn/bundle.js +3 -3
- package/dist/actions-Bsl5ub7_.js +1 -0
- package/dist/actions-C_B0fn1P.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-CCAZvLa5.d.cts → index-BSGP3dbi.d.ts} +250 -73
- package/dist/{siweAuthenticate-CVigMOxz.d.cts → index-Bh0TuKYS.d.ts} +122 -8
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → index-DW8xes2o.d.cts} +122 -8
- package/dist/{computeLegacyProductId-b5cUWdAm.d.ts → index-TRJNS6B5.d.cts} +250 -73
- 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-Bvhy_urG.d.cts} +395 -50
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-D2kTUv0-.d.ts} +394 -49
- package/dist/sdkConfigStore-B6CkorsU.cjs +1 -0
- package/dist/sdkConfigStore-Dx0oAVEO.js +1 -0
- package/dist/src-CLF8o8WB.cjs +13 -0
- package/dist/src-al3X6r-n.js +13 -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/processReferral.test.ts +73 -8
- package/src/actions/referral/processReferral.ts +15 -12
- 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 +233 -28
- package/src/clients/transports/iframeLifecycleManager.test.ts +14 -94
- package/src/clients/transports/iframeLifecycleManager.ts +35 -53
- package/src/index.ts +25 -5
- package/src/stubs/rrweb.ts +9 -0
- package/src/types/config.ts +19 -3
- package/src/types/context.ts +16 -4
- package/src/types/index.ts +15 -1
- package/src/types/lifecycle/client.ts +29 -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 +179 -1
- package/src/utils/FrakContext.ts +83 -8
- package/src/utils/analytics/events/component.ts +58 -0
- package/src/utils/analytics/events/index.ts +20 -0
- package/src/utils/analytics/events/lifecycle.ts +26 -0
- package/src/utils/analytics/events/referral.ts +11 -0
- package/src/utils/analytics/index.ts +8 -0
- package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
- package/src/utils/analytics/trackEvent.ts +34 -0
- 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 +11 -5
- 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/utils/trackEvent.ts +0 -41
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";
|
|
@@ -23,13 +28,16 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
23
28
|
if (!context) return;
|
|
24
29
|
try {
|
|
25
30
|
if (isV2Context(context)) {
|
|
26
|
-
// Runtime validation:
|
|
27
|
-
|
|
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;
|
|
28
35
|
return compressJsonToB64({
|
|
29
36
|
v: 2,
|
|
30
|
-
c: context.c,
|
|
31
37
|
m: context.m,
|
|
32
38
|
t: context.t,
|
|
39
|
+
...(context.c ? { c: context.c } : {}),
|
|
40
|
+
...(context.w ? { w: context.w } : {}),
|
|
33
41
|
});
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -56,10 +64,15 @@ function decompress(context?: string): FrakContext | undefined {
|
|
|
56
64
|
// Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
|
|
57
65
|
const json = decompressJsonFromB64<FrakContextV2>(context);
|
|
58
66
|
if (json && typeof json === "object" && json.v === 2) {
|
|
59
|
-
if (json.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
};
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
// Fall back to V1: raw 20-byte address
|
|
@@ -91,20 +104,81 @@ function parse({ url }: { url: string }): FrakContext | null | undefined {
|
|
|
91
104
|
return decompress(frakContext);
|
|
92
105
|
}
|
|
93
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Default UTM medium value when attribution is requested.
|
|
109
|
+
*/
|
|
110
|
+
const DEFAULT_UTM_MEDIUM = "referral";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Default utm_source / via value when attribution is requested.
|
|
114
|
+
*/
|
|
115
|
+
const DEFAULT_ATTRIBUTION_SOURCE = "frak";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Resolve attribution defaults from the provided context.
|
|
119
|
+
*
|
|
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.
|
|
124
|
+
*/
|
|
125
|
+
function resolveAttributionValues(
|
|
126
|
+
context: FrakContextV1 | FrakContextV2,
|
|
127
|
+
overrides: AttributionParams
|
|
128
|
+
): Record<string, string | undefined> {
|
|
129
|
+
const isV2 = isV2Context(context);
|
|
130
|
+
return {
|
|
131
|
+
utm_source: overrides.utmSource ?? DEFAULT_ATTRIBUTION_SOURCE,
|
|
132
|
+
utm_medium: overrides.utmMedium ?? DEFAULT_UTM_MEDIUM,
|
|
133
|
+
utm_campaign: overrides.utmCampaign ?? (isV2 ? context.m : undefined),
|
|
134
|
+
utm_content: overrides.utmContent,
|
|
135
|
+
utm_term: overrides.utmTerm,
|
|
136
|
+
via: overrides.via ?? DEFAULT_ATTRIBUTION_SOURCE,
|
|
137
|
+
ref: overrides.ref ?? (isV2 ? context.c : undefined),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Append attribution query params to a URL using gap-fill semantics.
|
|
143
|
+
*
|
|
144
|
+
* Existing params on the URL are preserved untouched (so merchant-provided
|
|
145
|
+
* UTMs take precedence). Only missing keys are populated.
|
|
146
|
+
*/
|
|
147
|
+
function applyAttributionParams(
|
|
148
|
+
urlObj: URL,
|
|
149
|
+
context: FrakContextV1 | FrakContextV2,
|
|
150
|
+
attribution?: AttributionParams
|
|
151
|
+
): void {
|
|
152
|
+
const values = resolveAttributionValues(context, attribution ?? {});
|
|
153
|
+
for (const [key, value] of Object.entries(values)) {
|
|
154
|
+
if (value === undefined || value === "") continue;
|
|
155
|
+
if (urlObj.searchParams.has(key)) continue;
|
|
156
|
+
urlObj.searchParams.set(key, value);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
94
160
|
/**
|
|
95
161
|
* Add or replace the `fCtx` query parameter in a URL with the given context.
|
|
96
162
|
*
|
|
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.
|
|
167
|
+
*
|
|
97
168
|
* @param args
|
|
98
169
|
* @param args.url - The URL to update
|
|
99
170
|
* @param args.context - The context to embed (V1 or V2)
|
|
171
|
+
* @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.
|
|
100
172
|
* @returns The updated URL string, or null on failure
|
|
101
173
|
*/
|
|
102
174
|
function update({
|
|
103
175
|
url,
|
|
104
176
|
context,
|
|
177
|
+
attribution,
|
|
105
178
|
}: {
|
|
106
179
|
url?: string;
|
|
107
180
|
context: FrakContextV1 | FrakContextV2;
|
|
181
|
+
attribution?: AttributionParams;
|
|
108
182
|
}): string | null {
|
|
109
183
|
if (!url) return null;
|
|
110
184
|
|
|
@@ -113,6 +187,7 @@ function update({
|
|
|
113
187
|
|
|
114
188
|
const urlObj = new URL(url);
|
|
115
189
|
urlObj.searchParams.set(contextKey, compressedContext);
|
|
190
|
+
applyAttributionParams(urlObj, context, attribution);
|
|
116
191
|
return urlObj.toString();
|
|
117
192
|
}
|
|
118
193
|
|
|
@@ -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
|
+
};
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
expect,
|
|
11
11
|
it,
|
|
12
12
|
vi,
|
|
13
|
-
} from "
|
|
14
|
-
import type { FrakClient } from "
|
|
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 = {
|
|
51
|
-
|
|
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
|
-
"
|
|
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, "
|
|
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, "
|
|
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
|
|
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
|
-
|
|
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);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FrakClient } from "../../types";
|
|
2
|
+
import type { SdkEventMap } from "./events";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Track an analytics event via the SDK's OpenPanel instance.
|
|
6
|
+
* Fire-and-forget — silently catches errors so analytics never break a
|
|
7
|
+
* partner integration.
|
|
8
|
+
*
|
|
9
|
+
* The client must be passed explicitly because the OpenPanel instance is
|
|
10
|
+
* scoped to each `FrakClient` (a partner site may hold multiple iframes).
|
|
11
|
+
*
|
|
12
|
+
* @param client - The Frak client instance (no-op if undefined)
|
|
13
|
+
* @param event - Typed event name from the SDK event map
|
|
14
|
+
* @param properties - Typed properties for the given event
|
|
15
|
+
*/
|
|
16
|
+
export function trackEvent<K extends keyof SdkEventMap>(
|
|
17
|
+
client: FrakClient | undefined,
|
|
18
|
+
event: K,
|
|
19
|
+
properties?: SdkEventMap[K]
|
|
20
|
+
): void {
|
|
21
|
+
if (!client) {
|
|
22
|
+
console.debug("[Frak] No client provided, skipping event tracking");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
client.openPanel?.track(
|
|
28
|
+
event as string,
|
|
29
|
+
properties as Record<string, unknown> | undefined
|
|
30
|
+
);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.debug("[Frak] Failed to track event:", event, e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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
|
+
}
|