@frak-labs/core-sdk 0.1.1 → 0.2.0-beta.7898df5b
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 +58 -0
- package/cdn/bundle.js +14 -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 -6
- package/dist/bundle.d.ts +4 -6
- package/dist/bundle.js +1 -1
- package/dist/computeLegacyProductId-CCAZvLa5.d.cts +537 -0
- package/dist/computeLegacyProductId-b5cUWdAm.d.ts +537 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -4
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -1
- package/dist/{openSso-D--Airj6.d.cts → openSso-B0g7-807.d.cts} +173 -136
- package/dist/{openSso-DsKJ4y0j.d.ts → openSso-CMzwvaCa.d.ts} +173 -136
- package/dist/setupClient-BICl5fdX.js +13 -0
- package/dist/setupClient-nl8Dhh4V.cjs +13 -0
- package/dist/siweAuthenticate-BWmI2_TN.cjs +1 -0
- package/dist/{index-d8xS4ryI.d.ts → siweAuthenticate-CVigMOxz.d.cts} +113 -92
- package/dist/{index-C6FxkWPC.d.cts → siweAuthenticate-CnCZ7mok.d.ts} +113 -92
- package/dist/siweAuthenticate-zczqxm0a.js +1 -0
- package/dist/trackEvent-CeLFVzZn.js +1 -0
- package/dist/trackEvent-Ew5r5zfI.cjs +1 -0
- package/package.json +11 -22
- package/src/actions/displayEmbeddedWallet.ts +1 -0
- package/src/actions/displayModal.test.ts +12 -11
- package/src/actions/displayModal.ts +7 -18
- package/src/actions/ensureIdentity.ts +68 -0
- package/src/actions/{getProductInformation.test.ts → getMerchantInformation.test.ts} +33 -50
- package/src/actions/getMerchantInformation.ts +16 -0
- package/src/actions/index.ts +3 -2
- package/src/actions/openSso.ts +4 -2
- package/src/actions/referral/processReferral.test.ts +117 -242
- package/src/actions/referral/processReferral.ts +134 -204
- package/src/actions/referral/referralInteraction.test.ts +4 -12
- package/src/actions/referral/referralInteraction.ts +3 -13
- package/src/actions/sendInteraction.ts +46 -22
- package/src/actions/trackPurchaseStatus.test.ts +354 -141
- package/src/actions/trackPurchaseStatus.ts +48 -11
- package/src/actions/watchWalletStatus.ts +2 -3
- package/src/actions/wrapper/modalBuilder.test.ts +0 -14
- package/src/actions/wrapper/modalBuilder.ts +3 -12
- package/src/bundle.ts +0 -1
- package/src/clients/createIFrameFrakClient.ts +10 -5
- package/src/clients/transports/iframeLifecycleManager.test.ts +163 -4
- package/src/clients/transports/iframeLifecycleManager.ts +172 -33
- package/src/constants/interactionTypes.ts +12 -41
- package/src/index.ts +27 -16
- package/src/types/config.ts +6 -0
- package/src/types/context.ts +48 -6
- package/src/types/index.ts +15 -11
- package/src/types/lifecycle/client.ts +24 -1
- package/src/types/lifecycle/iframe.ts +6 -0
- package/src/types/rpc/displayModal.ts +2 -4
- package/src/types/rpc/embedded/index.ts +2 -2
- package/src/types/rpc/interaction.ts +31 -39
- package/src/types/rpc/merchantInformation.ts +77 -0
- package/src/types/rpc/modal/index.ts +0 -4
- package/src/types/rpc/modal/login.ts +5 -1
- package/src/types/rpc/walletStatus.ts +1 -7
- package/src/types/rpc.ts +22 -30
- package/src/types/tracking.ts +31 -0
- package/src/utils/FrakContext.test.ts +270 -186
- package/src/utils/FrakContext.ts +78 -56
- package/src/utils/backendUrl.test.ts +83 -0
- package/src/utils/backendUrl.ts +62 -0
- package/src/utils/clientId.test.ts +41 -0
- package/src/utils/clientId.ts +43 -0
- package/src/utils/compression/compress.test.ts +1 -1
- package/src/utils/compression/compress.ts +2 -2
- package/src/utils/compression/decompress.test.ts +8 -4
- package/src/utils/compression/decompress.ts +2 -2
- package/src/utils/{computeProductId.ts → computeLegacyProductId.ts} +2 -2
- package/src/utils/constants.ts +5 -0
- package/src/utils/deepLinkWithFallback.test.ts +243 -0
- package/src/utils/deepLinkWithFallback.ts +103 -0
- package/src/utils/formatAmount.ts +6 -0
- package/src/utils/iframeHelper.test.ts +18 -5
- package/src/utils/iframeHelper.ts +10 -3
- package/src/utils/index.ts +16 -1
- package/src/utils/merchantId.test.ts +653 -0
- package/src/utils/merchantId.ts +143 -0
- package/src/utils/sso.ts +18 -11
- package/src/utils/trackEvent.test.ts +23 -5
- package/src/utils/trackEvent.ts +13 -0
- package/cdn/bundle.iife.js +0 -14
- package/dist/actions-B5j-i1p0.cjs +0 -1
- package/dist/actions-q090Z0oR.js +0 -1
- package/dist/index-7OZ39x1U.d.ts +0 -195
- package/dist/index-CRsQWnTs.d.cts +0 -351
- package/dist/index-Ck1hudEi.d.ts +0 -351
- package/dist/index-zDq-VlKx.d.cts +0 -195
- package/dist/interaction-DMJ3ZfaF.d.cts +0 -45
- package/dist/interaction-KX1h9a7V.d.ts +0 -45
- package/dist/interactions-DnfM3oe0.js +0 -1
- package/dist/interactions-EIXhNLf6.cjs +0 -1
- package/dist/interactions.cjs +0 -1
- package/dist/interactions.d.cts +0 -2
- package/dist/interactions.d.ts +0 -2
- package/dist/interactions.js +0 -1
- package/dist/productTypes-BUkXJKZ7.cjs +0 -1
- package/dist/productTypes-CGb1MmBF.js +0 -1
- package/dist/src-1LQ4eLq5.js +0 -13
- package/dist/src-hW71KjPN.cjs +0 -13
- package/dist/trackEvent-CHnYa85W.js +0 -1
- package/dist/trackEvent-GuQm_1Nm.cjs +0 -1
- package/src/actions/getProductInformation.ts +0 -14
- package/src/actions/openSso.test.ts +0 -407
- package/src/actions/sendInteraction.test.ts +0 -219
- package/src/constants/interactionTypes.test.ts +0 -128
- package/src/constants/productTypes.test.ts +0 -130
- package/src/constants/productTypes.ts +0 -33
- package/src/interactions/index.ts +0 -5
- package/src/interactions/pressEncoder.test.ts +0 -215
- package/src/interactions/pressEncoder.ts +0 -53
- package/src/interactions/purchaseEncoder.test.ts +0 -291
- package/src/interactions/purchaseEncoder.ts +0 -99
- package/src/interactions/referralEncoder.test.ts +0 -170
- package/src/interactions/referralEncoder.ts +0 -47
- package/src/interactions/retailEncoder.test.ts +0 -107
- package/src/interactions/retailEncoder.ts +0 -37
- package/src/interactions/webshopEncoder.test.ts +0 -56
- package/src/interactions/webshopEncoder.ts +0 -30
- package/src/types/rpc/modal/openSession.ts +0 -25
- package/src/types/rpc/productInformation.ts +0 -59
- package/src/utils/computeProductId.test.ts +0 -80
- package/src/utils/sso.test.ts +0 -361
package/src/utils/FrakContext.ts
CHANGED
|
@@ -1,20 +1,39 @@
|
|
|
1
|
-
import { type Address, bytesToHex, hexToBytes } from "viem";
|
|
2
|
-
import type { FrakContext } from "../types";
|
|
1
|
+
import { type Address, bytesToHex, hexToBytes, isAddress } from "viem";
|
|
2
|
+
import type { FrakContext, FrakContextV1, FrakContextV2 } from "../types";
|
|
3
|
+
import { isV2Context } from "../types";
|
|
3
4
|
import { base64urlDecode, base64urlEncode } from "./compression/b64";
|
|
5
|
+
import { compressJsonToB64 } from "./compression/compress";
|
|
6
|
+
import { decompressJsonFromB64 } from "./compression/decompress";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
|
-
*
|
|
9
|
+
* URL parameter key for the Frak referral context
|
|
7
10
|
*/
|
|
8
11
|
const contextKey = "fCtx";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
|
-
* Compress
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* Compress a Frak context into a URL-safe string.
|
|
15
|
+
*
|
|
16
|
+
* - V2 contexts are serialized as compressed JSON (base64url).
|
|
17
|
+
* - V1 contexts encode the wallet address as raw bytes (base64url).
|
|
18
|
+
*
|
|
19
|
+
* @param context - The context to compress (V1 or V2)
|
|
20
|
+
* @returns A compressed base64url string, or undefined on failure
|
|
14
21
|
*/
|
|
15
|
-
function compress(context?:
|
|
16
|
-
if (!context
|
|
22
|
+
function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
23
|
+
if (!context) return;
|
|
17
24
|
try {
|
|
25
|
+
if (isV2Context(context)) {
|
|
26
|
+
// Runtime validation: all V2 fields must be present and truthy
|
|
27
|
+
if (!context.c || !context.m || !context.t) return undefined;
|
|
28
|
+
return compressJsonToB64({
|
|
29
|
+
v: 2,
|
|
30
|
+
c: context.c,
|
|
31
|
+
m: context.m,
|
|
32
|
+
t: context.t,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// V1 legacy: compress wallet address as raw bytes
|
|
18
37
|
const bytes = hexToBytes(context.r);
|
|
19
38
|
return base64urlEncode(bytes);
|
|
20
39
|
} catch (e) {
|
|
@@ -24,15 +43,31 @@ function compress(context?: Partial<FrakContext>): string | undefined {
|
|
|
24
43
|
}
|
|
25
44
|
|
|
26
45
|
/**
|
|
27
|
-
* Decompress
|
|
28
|
-
*
|
|
29
|
-
*
|
|
46
|
+
* Decompress a base64url string back into a Frak context.
|
|
47
|
+
*
|
|
48
|
+
* Attempts V2 JSON decompression first, then falls back to V1 raw bytes.
|
|
49
|
+
*
|
|
50
|
+
* @param context - The compressed context string
|
|
51
|
+
* @returns The decompressed FrakContext, or undefined on failure
|
|
30
52
|
*/
|
|
31
53
|
function decompress(context?: string): FrakContext | undefined {
|
|
32
54
|
if (!context || context.length === 0) return;
|
|
33
55
|
try {
|
|
56
|
+
// Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
|
|
57
|
+
const json = decompressJsonFromB64<FrakContextV2>(context);
|
|
58
|
+
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;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fall back to V1: raw 20-byte address
|
|
34
66
|
const bytes = base64urlDecode(context);
|
|
35
|
-
|
|
67
|
+
const hex = bytesToHex(bytes, { size: 20 }) as Address;
|
|
68
|
+
if (isAddress(hex)) {
|
|
69
|
+
return { r: hex };
|
|
70
|
+
}
|
|
36
71
|
} catch (e) {
|
|
37
72
|
console.error("Error decompressing Frak context", { e, context });
|
|
38
73
|
}
|
|
@@ -40,113 +75,100 @@ function decompress(context?: string): FrakContext | undefined {
|
|
|
40
75
|
}
|
|
41
76
|
|
|
42
77
|
/**
|
|
43
|
-
* Parse
|
|
78
|
+
* Parse a URL to extract the Frak referral context from the `fCtx` query parameter.
|
|
79
|
+
*
|
|
44
80
|
* @param args
|
|
45
|
-
* @param args.url - The
|
|
46
|
-
* @returns The parsed
|
|
81
|
+
* @param args.url - The URL to parse
|
|
82
|
+
* @returns The parsed FrakContext, or null if absent
|
|
47
83
|
*/
|
|
48
|
-
function parse({ url }: { url: string }) {
|
|
84
|
+
function parse({ url }: { url: string }): FrakContext | null | undefined {
|
|
49
85
|
if (!url) return null;
|
|
50
86
|
|
|
51
|
-
// Check if the url contain the frak context key
|
|
52
87
|
const urlObj = new URL(url);
|
|
53
88
|
const frakContext = urlObj.searchParams.get(contextKey);
|
|
54
89
|
if (!frakContext) return null;
|
|
55
90
|
|
|
56
|
-
// Decompress and return it
|
|
57
91
|
return decompress(frakContext);
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
/**
|
|
61
|
-
*
|
|
95
|
+
* Add or replace the `fCtx` query parameter in a URL with the given context.
|
|
96
|
+
*
|
|
62
97
|
* @param args
|
|
63
|
-
* @param args.url - The
|
|
64
|
-
* @param args.context - The context to
|
|
65
|
-
* @returns The
|
|
98
|
+
* @param args.url - The URL to update
|
|
99
|
+
* @param args.context - The context to embed (V1 or V2)
|
|
100
|
+
* @returns The updated URL string, or null on failure
|
|
66
101
|
*/
|
|
67
102
|
function update({
|
|
68
103
|
url,
|
|
69
104
|
context,
|
|
70
105
|
}: {
|
|
71
106
|
url?: string;
|
|
72
|
-
context:
|
|
73
|
-
}) {
|
|
107
|
+
context: FrakContextV1 | FrakContextV2;
|
|
108
|
+
}): string | null {
|
|
74
109
|
if (!url) return null;
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
const currentContext = parse({ url });
|
|
78
|
-
|
|
79
|
-
// Merge the current context with the new context
|
|
80
|
-
const mergedContext = currentContext
|
|
81
|
-
? { ...currentContext, ...context }
|
|
82
|
-
: context;
|
|
83
|
-
|
|
84
|
-
// If we don't have a referrer, early exit
|
|
85
|
-
if (!mergedContext.r) return null;
|
|
86
|
-
|
|
87
|
-
// Compress it
|
|
88
|
-
const compressedContext = compress(mergedContext);
|
|
111
|
+
const compressedContext = compress(context);
|
|
89
112
|
if (!compressedContext) return null;
|
|
90
113
|
|
|
91
|
-
// Build the new url and return it
|
|
92
114
|
const urlObj = new URL(url);
|
|
93
115
|
urlObj.searchParams.set(contextKey, compressedContext);
|
|
94
116
|
return urlObj.toString();
|
|
95
117
|
}
|
|
96
118
|
|
|
97
119
|
/**
|
|
98
|
-
* Remove
|
|
99
|
-
*
|
|
100
|
-
* @
|
|
120
|
+
* Remove the `fCtx` query parameter from a URL.
|
|
121
|
+
*
|
|
122
|
+
* @param url - The URL to strip the context from
|
|
123
|
+
* @returns The cleaned URL string
|
|
101
124
|
*/
|
|
102
|
-
function remove(url: string) {
|
|
125
|
+
function remove(url: string): string {
|
|
103
126
|
const urlObj = new URL(url);
|
|
104
127
|
urlObj.searchParams.delete(contextKey);
|
|
105
128
|
return urlObj.toString();
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
/**
|
|
109
|
-
* Replace the current
|
|
132
|
+
* Replace the current browser URL with an updated Frak context.
|
|
133
|
+
*
|
|
134
|
+
* - If `context` is non-null, embeds it via {@link update}.
|
|
135
|
+
* - If `context` is null, strips the context via {@link remove}.
|
|
136
|
+
*
|
|
110
137
|
* @param args
|
|
111
|
-
* @param args.url -
|
|
112
|
-
* @param args.context -
|
|
138
|
+
* @param args.url - Base URL (defaults to `window.location.href`)
|
|
139
|
+
* @param args.context - Context to set, or null to remove
|
|
113
140
|
*/
|
|
114
141
|
function replaceUrl({
|
|
115
142
|
url: baseUrl,
|
|
116
143
|
context,
|
|
117
144
|
}: {
|
|
118
145
|
url?: string;
|
|
119
|
-
context:
|
|
146
|
+
context: FrakContextV1 | FrakContextV2 | null;
|
|
120
147
|
}) {
|
|
121
|
-
// If no window here early exit
|
|
122
148
|
if (!window.location?.href || typeof window === "undefined") {
|
|
123
149
|
console.error("No window found, can't update context");
|
|
124
150
|
return;
|
|
125
151
|
}
|
|
126
152
|
|
|
127
|
-
// If no url, try to use the current one
|
|
128
153
|
const url = baseUrl ?? window.location.href;
|
|
129
154
|
|
|
130
|
-
// Get our new url with the frak context
|
|
131
155
|
let newUrl: string | null;
|
|
132
156
|
if (context !== null) {
|
|
133
|
-
newUrl = update({
|
|
134
|
-
url,
|
|
135
|
-
context,
|
|
136
|
-
});
|
|
157
|
+
newUrl = update({ url, context });
|
|
137
158
|
} else {
|
|
138
159
|
newUrl = remove(url);
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
// If no new url, early exit
|
|
142
162
|
if (!newUrl) return;
|
|
143
163
|
|
|
144
|
-
// Update the url
|
|
145
164
|
window.history.replaceState(null, "", newUrl.toString());
|
|
146
165
|
}
|
|
147
166
|
|
|
148
167
|
/**
|
|
149
|
-
*
|
|
168
|
+
* Manager for Frak referral context in URLs.
|
|
169
|
+
*
|
|
170
|
+
* Handles compression, decompression, URL parsing, and browser history updates
|
|
171
|
+
* for both V1 (wallet address) and V2 (anonymous clientId) referral contexts.
|
|
150
172
|
*/
|
|
151
173
|
export const FrakContextManager = {
|
|
152
174
|
compress,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { getBackendUrl } from "./backendUrl";
|
|
3
|
+
|
|
4
|
+
describe("getBackendUrl", () => {
|
|
5
|
+
const originalWindow = globalThis.window;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.stubGlobal("window", { ...originalWindow });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.unstubAllGlobals();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("with explicit walletUrl", () => {
|
|
16
|
+
test("should return localhost backend for localhost:3000", () => {
|
|
17
|
+
expect(getBackendUrl("https://localhost:3000")).toBe(
|
|
18
|
+
"http://localhost:3030"
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should return localhost backend for localhost:3010", () => {
|
|
23
|
+
expect(getBackendUrl("https://localhost:3010")).toBe(
|
|
24
|
+
"http://localhost:3030"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("should return dev backend for wallet-dev.frak.id", () => {
|
|
29
|
+
expect(getBackendUrl("https://wallet-dev.frak.id")).toBe(
|
|
30
|
+
"https://backend.gcp-dev.frak.id"
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("should return dev backend for wallet.gcp-dev.frak.id", () => {
|
|
35
|
+
expect(getBackendUrl("https://wallet.gcp-dev.frak.id")).toBe(
|
|
36
|
+
"https://backend.gcp-dev.frak.id"
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("should return production backend for wallet.frak.id", () => {
|
|
41
|
+
expect(getBackendUrl("https://wallet.frak.id")).toBe(
|
|
42
|
+
"https://backend.frak.id"
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("should return production backend for unknown URLs", () => {
|
|
47
|
+
expect(getBackendUrl("https://some-other-url.com")).toBe(
|
|
48
|
+
"https://backend.frak.id"
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("with FrakSetup global config", () => {
|
|
54
|
+
test("should derive from window.FrakSetup.client.config.walletUrl", () => {
|
|
55
|
+
vi.stubGlobal("window", {
|
|
56
|
+
FrakSetup: {
|
|
57
|
+
client: {
|
|
58
|
+
config: {
|
|
59
|
+
walletUrl: "https://wallet-dev.frak.id",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(getBackendUrl()).toBe("https://backend.gcp-dev.frak.id");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("should fall back to production when FrakSetup has no walletUrl", () => {
|
|
69
|
+
vi.stubGlobal("window", {
|
|
70
|
+
FrakSetup: { client: { config: {} } },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(getBackendUrl()).toBe("https://backend.frak.id");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("fallback", () => {
|
|
78
|
+
test("should return production URL when no walletUrl and no FrakSetup", () => {
|
|
79
|
+
vi.stubGlobal("window", {});
|
|
80
|
+
expect(getBackendUrl()).toBe("https://backend.frak.id");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default production backend URL
|
|
3
|
+
*/
|
|
4
|
+
const DEFAULT_BACKEND_URL = "https://backend.frak.id";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if wallet URL is local development (port 3000 or 3010)
|
|
8
|
+
*/
|
|
9
|
+
function isLocalDevelopment(walletUrl: string): boolean {
|
|
10
|
+
return (
|
|
11
|
+
walletUrl.includes("localhost:3000") ||
|
|
12
|
+
walletUrl.includes("localhost:3010")
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derive backend URL from wallet URL
|
|
18
|
+
* Maps wallet URLs to their corresponding backend URLs
|
|
19
|
+
*/
|
|
20
|
+
function deriveBackendUrl(walletUrl: string): string {
|
|
21
|
+
if (isLocalDevelopment(walletUrl)) {
|
|
22
|
+
return "http://localhost:3030";
|
|
23
|
+
}
|
|
24
|
+
// Dev environment
|
|
25
|
+
if (
|
|
26
|
+
walletUrl.includes("wallet-dev.frak.id") ||
|
|
27
|
+
walletUrl.includes("wallet.gcp-dev.frak.id")
|
|
28
|
+
) {
|
|
29
|
+
return "https://backend.gcp-dev.frak.id";
|
|
30
|
+
}
|
|
31
|
+
// Production
|
|
32
|
+
return DEFAULT_BACKEND_URL;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the backend URL for API calls
|
|
37
|
+
* Tries to derive from SDK config, falls back to production
|
|
38
|
+
*
|
|
39
|
+
* @param walletUrl - Optional wallet URL to derive from (overrides global config)
|
|
40
|
+
*/
|
|
41
|
+
export function getBackendUrl(walletUrl?: string): string {
|
|
42
|
+
// If explicit walletUrl provided, derive from it
|
|
43
|
+
if (walletUrl) {
|
|
44
|
+
return deriveBackendUrl(walletUrl);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Try to get from global FrakSetup config
|
|
48
|
+
if (typeof window !== "undefined") {
|
|
49
|
+
const configWalletUrl = (
|
|
50
|
+
window as {
|
|
51
|
+
FrakSetup?: { client?: { config?: { walletUrl?: string } } };
|
|
52
|
+
}
|
|
53
|
+
).FrakSetup?.client?.config?.walletUrl;
|
|
54
|
+
|
|
55
|
+
if (configWalletUrl) {
|
|
56
|
+
return deriveBackendUrl(configWalletUrl);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Fallback to production
|
|
61
|
+
return DEFAULT_BACKEND_URL;
|
|
62
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { getClientId } from "./clientId";
|
|
3
|
+
|
|
4
|
+
describe("clientId", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Clear localStorage before each test
|
|
7
|
+
localStorage.clear();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("getClientId", () => {
|
|
15
|
+
it("should generate and store a new client ID when none exists", () => {
|
|
16
|
+
const clientId = getClientId();
|
|
17
|
+
|
|
18
|
+
expect(clientId).toBeDefined();
|
|
19
|
+
expect(clientId).toMatch(
|
|
20
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
|
21
|
+
);
|
|
22
|
+
expect(localStorage.getItem("frak-client-id")).toBe(clientId);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should return existing client ID from localStorage", () => {
|
|
26
|
+
const existingId = "existing-uuid-1234";
|
|
27
|
+
localStorage.setItem("frak-client-id", existingId);
|
|
28
|
+
|
|
29
|
+
const clientId = getClientId();
|
|
30
|
+
|
|
31
|
+
expect(clientId).toBe(existingId);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should generate consistent UUIDs", () => {
|
|
35
|
+
const id1 = getClientId();
|
|
36
|
+
const id2 = getClientId();
|
|
37
|
+
|
|
38
|
+
expect(id1).toBe(id2);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client ID utilities for anonymous tracking
|
|
3
|
+
* Generates and persists a UUID fingerprint for referral attribution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const CLIENT_ID_KEY = "frak-client-id";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a UUID v4
|
|
10
|
+
* Uses crypto.randomUUID if available, otherwise falls back to a manual implementation
|
|
11
|
+
*/
|
|
12
|
+
function generateUUID(): string {
|
|
13
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
14
|
+
return crypto.randomUUID();
|
|
15
|
+
}
|
|
16
|
+
// Fallback for older browsers
|
|
17
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
18
|
+
const r = (Math.random() * 16) | 0;
|
|
19
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
20
|
+
return v.toString(16);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the client ID from localStorage, creating one if it doesn't exist
|
|
26
|
+
* @returns The client ID (UUID format)
|
|
27
|
+
*/
|
|
28
|
+
export function getClientId(): string {
|
|
29
|
+
if (typeof window === "undefined" || !window.localStorage) {
|
|
30
|
+
// SSR or no localStorage - generate ephemeral ID
|
|
31
|
+
console.warn(
|
|
32
|
+
"[Frak SDK] No Window / localStorage available to save the clientId"
|
|
33
|
+
);
|
|
34
|
+
return generateUUID();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let clientId = localStorage.getItem(CLIENT_ID_KEY);
|
|
38
|
+
if (!clientId) {
|
|
39
|
+
clientId = generateUUID();
|
|
40
|
+
localStorage.setItem(CLIENT_ID_KEY, clientId);
|
|
41
|
+
}
|
|
42
|
+
return clientId;
|
|
43
|
+
}
|
|
@@ -7,7 +7,7 @@ import { vi } from "vitest";
|
|
|
7
7
|
|
|
8
8
|
// Mock the frame-connector module - must be before imports
|
|
9
9
|
vi.mock("@frak-labs/frame-connector", () => ({
|
|
10
|
-
|
|
10
|
+
jsonEncode: vi.fn((data: unknown) => {
|
|
11
11
|
// Simple mock: convert JSON to Uint8Array
|
|
12
12
|
const jsonString = JSON.stringify(data);
|
|
13
13
|
return new TextEncoder().encode(jsonString);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsonEncode } from "@frak-labs/frame-connector";
|
|
2
2
|
import { base64urlEncode } from "./b64";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,5 +7,5 @@ import { base64urlEncode } from "./b64";
|
|
|
7
7
|
* @ignore
|
|
8
8
|
*/
|
|
9
9
|
export function compressJsonToB64(data: unknown): string {
|
|
10
|
-
return base64urlEncode(
|
|
10
|
+
return base64urlEncode(jsonEncode(data));
|
|
11
11
|
}
|
|
@@ -7,15 +7,19 @@ import { vi } from "vitest";
|
|
|
7
7
|
|
|
8
8
|
// Mock the frame-connector module - must be before imports
|
|
9
9
|
vi.mock("@frak-labs/frame-connector", () => ({
|
|
10
|
-
|
|
10
|
+
jsonEncode: vi.fn((data: unknown) => {
|
|
11
11
|
// Simple mock: convert JSON to Uint8Array
|
|
12
12
|
const jsonString = JSON.stringify(data);
|
|
13
13
|
return new TextEncoder().encode(jsonString);
|
|
14
14
|
}),
|
|
15
|
-
|
|
15
|
+
jsonDecode: vi.fn((data: Uint8Array) => {
|
|
16
16
|
// Simple mock: convert Uint8Array back to JSON
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
try {
|
|
18
|
+
const jsonString = new TextDecoder().decode(data);
|
|
19
|
+
return JSON.parse(jsonString);
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
19
23
|
}),
|
|
20
24
|
}));
|
|
21
25
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsonDecode } from "@frak-labs/frame-connector";
|
|
2
2
|
import { base64urlDecode } from "./b64";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -7,5 +7,5 @@ import { base64urlDecode } from "./b64";
|
|
|
7
7
|
* @ignore
|
|
8
8
|
*/
|
|
9
9
|
export function decompressJsonFromB64<T>(data: string): T | null {
|
|
10
|
-
return
|
|
10
|
+
return jsonDecode<T>(base64urlDecode(data));
|
|
11
11
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { keccak256, toHex } from "viem";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Compute the product id from a domain
|
|
4
|
+
* Compute the legacy product id from a domain
|
|
5
5
|
* @ignore
|
|
6
6
|
*/
|
|
7
|
-
export function
|
|
7
|
+
export function computeLegacyProductId({ domain }: { domain?: string } = {}) {
|
|
8
8
|
const effectiveDomain = domain ?? window.location.host;
|
|
9
9
|
const normalizedDomain = effectiveDomain.replace("www.", "");
|
|
10
10
|
return keccak256(toHex(normalizedDomain));
|