@frak-labs/core-sdk 0.2.0 → 0.2.1-beta.06c52c98
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 +55 -3
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- 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-Raks6FXg.d.cts → computeLegacyProductId-BP-ciVsp.d.cts} +73 -88
- package/dist/{computeLegacyProductId-BkyJ4rEY.d.ts → computeLegacyProductId-DiJd7RNo.d.ts} +73 -88
- 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-BCJGchIb.d.cts → openSso-B8v3Vtnh.d.ts} +157 -52
- package/dist/{openSso-DG-_9CED.d.ts → openSso-n_B4LSuW.d.cts} +157 -52
- package/dist/setupClient-Dr_UYfTD.cjs +13 -0
- package/dist/setupClient-TuhDjVJx.js +13 -0
- package/dist/siweAuthenticate-0UPcUqI1.js +1 -0
- package/dist/{siweAuthenticate-Btem4QHs.d.ts → siweAuthenticate-CDCsp8EJ.d.ts} +35 -36
- package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
- package/dist/{siweAuthenticate-BH7Dn7nZ.d.cts → siweAuthenticate-yITE-iKh.d.cts} +35 -36
- package/dist/trackEvent-5j5kkOCj.js +1 -0
- package/dist/trackEvent-B2uom25e.cjs +1 -0
- package/package.json +8 -8
- package/src/actions/displayEmbeddedWallet.ts +6 -2
- package/src/actions/displayModal.ts +6 -2
- package/src/actions/ensureIdentity.ts +2 -2
- package/src/actions/referral/processReferral.test.ts +109 -125
- package/src/actions/referral/processReferral.ts +134 -180
- package/src/actions/referral/referralInteraction.test.ts +3 -5
- package/src/actions/referral/referralInteraction.ts +2 -7
- 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 +146 -25
- package/src/clients/transports/iframeLifecycleManager.test.ts +0 -80
- package/src/clients/transports/iframeLifecycleManager.ts +0 -44
- package/src/index.ts +8 -3
- package/src/types/config.ts +10 -3
- package/src/types/context.ts +48 -6
- package/src/types/index.ts +8 -2
- package/src/types/lifecycle/client.ts +22 -27
- package/src/types/lifecycle/iframe.ts +0 -8
- package/src/types/resolvedConfig.ts +104 -0
- package/src/types/rpc/interaction.ts +9 -0
- package/src/types/rpc.ts +7 -5
- package/src/types/tracking.ts +5 -34
- package/src/utils/FrakContext.test.ts +270 -186
- package/src/utils/FrakContext.ts +78 -56
- package/src/utils/backendUrl.test.ts +2 -2
- package/src/utils/backendUrl.ts +1 -1
- package/src/utils/index.ts +1 -5
- package/src/utils/sdkConfigStore.test.ts +405 -0
- package/src/utils/sdkConfigStore.ts +277 -0
- package/src/utils/sso.ts +3 -7
- package/dist/setupClient-CQrMDGyZ.js +0 -13
- package/dist/setupClient-Ccv3XxwL.cjs +0 -13
- package/dist/siweAuthenticate-BJHbtty4.js +0 -1
- package/dist/siweAuthenticate-Cwj3HP0m.cjs +0 -1
- package/dist/trackEvent-M2RLTQ2p.js +0 -1
- package/dist/trackEvent-T_R9ER2S.cjs +0 -1
- package/src/utils/merchantId.test.ts +0 -653
- package/src/utils/merchantId.ts +0 -143
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,
|
|
@@ -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 (
|
package/src/utils/index.ts
CHANGED
|
@@ -22,11 +22,7 @@ export {
|
|
|
22
22
|
createIframe,
|
|
23
23
|
findIframeInOpener,
|
|
24
24
|
} from "./iframeHelper";
|
|
25
|
-
export {
|
|
26
|
-
clearMerchantIdCache,
|
|
27
|
-
fetchMerchantId,
|
|
28
|
-
resolveMerchantId,
|
|
29
|
-
} from "./merchantId";
|
|
25
|
+
export { sdkConfigStore } from "./sdkConfigStore";
|
|
30
26
|
export {
|
|
31
27
|
type AppSpecificSsoMetadata,
|
|
32
28
|
type CompressedSsoData,
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { sdkConfigStore } from "./sdkConfigStore";
|
|
3
|
+
|
|
4
|
+
vi.mock("./backendUrl", () => ({
|
|
5
|
+
getBackendUrl: vi.fn((walletUrl?: string) => {
|
|
6
|
+
if (walletUrl?.includes("localhost")) {
|
|
7
|
+
return "http://localhost:3030";
|
|
8
|
+
}
|
|
9
|
+
return "https://backend.frak.id";
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("sdkConfigStore", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
sdkConfigStore.clearCache();
|
|
16
|
+
window.sessionStorage.clear();
|
|
17
|
+
window.localStorage.clear();
|
|
18
|
+
window.__frakSdkConfig = undefined;
|
|
19
|
+
vi.clearAllMocks();
|
|
20
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
vi.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("sdkConfigStore.resolve", () => {
|
|
28
|
+
it("should fetch from backend when not cached", async () => {
|
|
29
|
+
const mockResponse = {
|
|
30
|
+
merchantId: "merchant-123",
|
|
31
|
+
name: "Test",
|
|
32
|
+
domain: "shop.example.com",
|
|
33
|
+
allowedDomains: [],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
37
|
+
ok: true,
|
|
38
|
+
json: async () => mockResponse,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const result = await sdkConfigStore.resolve("shop.example.com");
|
|
42
|
+
|
|
43
|
+
expect(result).toEqual(mockResponse);
|
|
44
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
45
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com"
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should return cached response on subsequent calls", async () => {
|
|
50
|
+
const mockResponse = {
|
|
51
|
+
merchantId: "merchant-456",
|
|
52
|
+
name: "Test",
|
|
53
|
+
domain: "shop.example.com",
|
|
54
|
+
allowedDomains: [],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
58
|
+
ok: true,
|
|
59
|
+
json: async () => mockResponse,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const result1 = await sdkConfigStore.resolve("shop.example.com");
|
|
63
|
+
expect(result1).toEqual(mockResponse);
|
|
64
|
+
|
|
65
|
+
const result2 = await sdkConfigStore.resolve("shop.example.com");
|
|
66
|
+
expect(result2).toEqual(mockResponse);
|
|
67
|
+
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should deduplicate concurrent requests", async () => {
|
|
72
|
+
const mockResponse = {
|
|
73
|
+
merchantId: "merchant-789",
|
|
74
|
+
name: "Test",
|
|
75
|
+
domain: "shop.example.com",
|
|
76
|
+
allowedDomains: [],
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
80
|
+
ok: true,
|
|
81
|
+
json: async () => mockResponse,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const [result1, result2, result3] = await Promise.all([
|
|
85
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
86
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
87
|
+
sdkConfigStore.resolve("shop.example.com"),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
expect(result1).toEqual(mockResponse);
|
|
91
|
+
expect(result2).toEqual(mockResponse);
|
|
92
|
+
expect(result3).toEqual(mockResponse);
|
|
93
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should use window.location.hostname when domain not provided", async () => {
|
|
97
|
+
const mockResponse = {
|
|
98
|
+
merchantId: "merchant-default",
|
|
99
|
+
name: "Test",
|
|
100
|
+
domain: "example.com",
|
|
101
|
+
allowedDomains: [],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => mockResponse,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
Object.defineProperty(window, "location", {
|
|
110
|
+
value: { hostname: "example.com" },
|
|
111
|
+
writable: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const result = await sdkConfigStore.resolve();
|
|
115
|
+
|
|
116
|
+
expect(result).toEqual(mockResponse);
|
|
117
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
118
|
+
"https://backend.frak.id/user/merchant/resolve?domain=example.com"
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("should return undefined when domain is empty", async () => {
|
|
123
|
+
global.fetch = vi.fn();
|
|
124
|
+
|
|
125
|
+
const result = await sdkConfigStore.resolve("");
|
|
126
|
+
|
|
127
|
+
expect(result).toBeUndefined();
|
|
128
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should handle fetch errors gracefully", async () => {
|
|
132
|
+
global.fetch = vi
|
|
133
|
+
.fn()
|
|
134
|
+
.mockRejectedValueOnce(new Error("Network error"));
|
|
135
|
+
|
|
136
|
+
const result = await sdkConfigStore.resolve("shop.example.com");
|
|
137
|
+
|
|
138
|
+
expect(result).toBeUndefined();
|
|
139
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
140
|
+
"[Frak SDK] Failed to fetch merchant config:",
|
|
141
|
+
expect.any(Error)
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should handle non-ok response (404, 500)", async () => {
|
|
146
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
147
|
+
ok: false,
|
|
148
|
+
status: 404,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const result = await sdkConfigStore.resolve("nonexistent.com");
|
|
152
|
+
|
|
153
|
+
expect(result).toBeUndefined();
|
|
154
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
155
|
+
"[Frak SDK] Merchant lookup failed for domain nonexistent.com: 404"
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("should use custom walletUrl to derive backend URL", async () => {
|
|
160
|
+
const mockResponse = {
|
|
161
|
+
merchantId: "merchant-local",
|
|
162
|
+
name: "Test",
|
|
163
|
+
domain: "shop.example.com",
|
|
164
|
+
allowedDomains: [],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
168
|
+
ok: true,
|
|
169
|
+
json: async () => mockResponse,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const result = await sdkConfigStore.resolve(
|
|
173
|
+
"shop.example.com",
|
|
174
|
+
"http://localhost:3000"
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual(mockResponse);
|
|
178
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
179
|
+
"http://localhost:3030/user/merchant/resolve?domain=shop.example.com"
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should encode domain in URL query parameter", async () => {
|
|
184
|
+
const mockResponse = {
|
|
185
|
+
merchantId: "merchant-encoded",
|
|
186
|
+
name: "Test",
|
|
187
|
+
domain: "shop.example.com",
|
|
188
|
+
allowedDomains: [],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
192
|
+
ok: true,
|
|
193
|
+
json: async () => mockResponse,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const domainWithSpecialChars = "shop.example.com?test=1&foo=bar";
|
|
197
|
+
await sdkConfigStore.resolve(domainWithSpecialChars);
|
|
198
|
+
|
|
199
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
200
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com%3Ftest%3D1%26foo%3Dbar"
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should write merchantId to sessionStorage as 'frak-merchant-id'", async () => {
|
|
205
|
+
const mockResponse = {
|
|
206
|
+
merchantId: "merchant-persisted",
|
|
207
|
+
name: "Test",
|
|
208
|
+
domain: "shop.example.com",
|
|
209
|
+
allowedDomains: [],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
213
|
+
ok: true,
|
|
214
|
+
json: async () => mockResponse,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await sdkConfigStore.resolve("shop.example.com");
|
|
218
|
+
|
|
219
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
220
|
+
"merchant-persisted"
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should pass lang parameter to backend when provided", async () => {
|
|
225
|
+
const mockResponse = {
|
|
226
|
+
merchantId: "merchant-lang",
|
|
227
|
+
name: "Test",
|
|
228
|
+
domain: "shop.example.com",
|
|
229
|
+
allowedDomains: [],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
233
|
+
ok: true,
|
|
234
|
+
json: async () => mockResponse,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await sdkConfigStore.resolve("shop.example.com", undefined, "fr");
|
|
238
|
+
|
|
239
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
240
|
+
"https://backend.frak.id/user/merchant/resolve?domain=shop.example.com&lang=fr"
|
|
241
|
+
);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("sdkConfigStore.getMerchantId", () => {
|
|
246
|
+
it("should return merchantId from resolved config", () => {
|
|
247
|
+
window.__frakSdkConfig = {
|
|
248
|
+
isResolved: true,
|
|
249
|
+
merchantId: "config-merchant-123",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = sdkConfigStore.getMerchantId();
|
|
253
|
+
|
|
254
|
+
expect(result).toBe("config-merchant-123");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("should fall back to sessionStorage", () => {
|
|
258
|
+
window.__frakSdkConfig = {
|
|
259
|
+
isResolved: false,
|
|
260
|
+
merchantId: "",
|
|
261
|
+
};
|
|
262
|
+
window.sessionStorage.setItem(
|
|
263
|
+
"frak-merchant-id",
|
|
264
|
+
"session-merchant-456"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const result = sdkConfigStore.getMerchantId();
|
|
268
|
+
|
|
269
|
+
expect(result).toBe("session-merchant-456");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should return undefined when nothing cached", () => {
|
|
273
|
+
window.__frakSdkConfig = {
|
|
274
|
+
isResolved: false,
|
|
275
|
+
merchantId: "",
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const result = sdkConfigStore.getMerchantId();
|
|
279
|
+
|
|
280
|
+
expect(result).toBeUndefined();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("sdkConfigStore.resolveMerchantId", () => {
|
|
285
|
+
it("should return merchantId from store without fetch", async () => {
|
|
286
|
+
window.__frakSdkConfig = {
|
|
287
|
+
isResolved: true,
|
|
288
|
+
merchantId: "store-merchant-123",
|
|
289
|
+
};
|
|
290
|
+
global.fetch = vi.fn();
|
|
291
|
+
|
|
292
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
293
|
+
|
|
294
|
+
expect(result).toBe("store-merchant-123");
|
|
295
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("should return merchantId from sessionStorage without fetch", async () => {
|
|
299
|
+
window.__frakSdkConfig = {
|
|
300
|
+
isResolved: false,
|
|
301
|
+
merchantId: "",
|
|
302
|
+
};
|
|
303
|
+
window.sessionStorage.setItem(
|
|
304
|
+
"frak-merchant-id",
|
|
305
|
+
"session-merchant-789"
|
|
306
|
+
);
|
|
307
|
+
global.fetch = vi.fn();
|
|
308
|
+
|
|
309
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
310
|
+
|
|
311
|
+
expect(result).toBe("session-merchant-789");
|
|
312
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("should fetch from backend as last resort", async () => {
|
|
316
|
+
const mockResponse = {
|
|
317
|
+
merchantId: "fetched-merchant-456",
|
|
318
|
+
name: "Test",
|
|
319
|
+
domain: "shop.example.com",
|
|
320
|
+
allowedDomains: [],
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
324
|
+
ok: true,
|
|
325
|
+
json: async () => mockResponse,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
Object.defineProperty(window, "location", {
|
|
329
|
+
value: { hostname: "shop.example.com" },
|
|
330
|
+
writable: true,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
334
|
+
|
|
335
|
+
expect(result).toBe("fetched-merchant-456");
|
|
336
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should return undefined when fetch fails", async () => {
|
|
340
|
+
global.fetch = vi
|
|
341
|
+
.fn()
|
|
342
|
+
.mockRejectedValueOnce(new Error("Network error"));
|
|
343
|
+
|
|
344
|
+
Object.defineProperty(window, "location", {
|
|
345
|
+
value: { hostname: "shop.example.com" },
|
|
346
|
+
writable: true,
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const result = await sdkConfigStore.resolveMerchantId();
|
|
350
|
+
|
|
351
|
+
expect(result).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("sdkConfigStore.clearCache", () => {
|
|
356
|
+
it("should clear all caches and allow re-fetching", async () => {
|
|
357
|
+
const mockResponse = {
|
|
358
|
+
merchantId: "merchant-clear-test",
|
|
359
|
+
name: "Test",
|
|
360
|
+
domain: "shop.example.com",
|
|
361
|
+
allowedDomains: [],
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
365
|
+
ok: true,
|
|
366
|
+
json: async () => mockResponse,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const result1 = await sdkConfigStore.resolve("shop.example.com");
|
|
370
|
+
expect(result1).toEqual(mockResponse);
|
|
371
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
372
|
+
|
|
373
|
+
sdkConfigStore.clearCache();
|
|
374
|
+
|
|
375
|
+
const result2 = await sdkConfigStore.resolve("shop.example.com");
|
|
376
|
+
expect(result2).toEqual(mockResponse);
|
|
377
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should clear sessionStorage frak-merchant-id", async () => {
|
|
381
|
+
const mockResponse = {
|
|
382
|
+
merchantId: "merchant-session-clear",
|
|
383
|
+
name: "Test",
|
|
384
|
+
domain: "shop.example.com",
|
|
385
|
+
allowedDomains: [],
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
389
|
+
ok: true,
|
|
390
|
+
json: async () => mockResponse,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
await sdkConfigStore.resolve("shop.example.com");
|
|
394
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
395
|
+
"merchant-session-clear"
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
sdkConfigStore.clearCache();
|
|
399
|
+
|
|
400
|
+
expect(
|
|
401
|
+
window.sessionStorage.getItem("frak-merchant-id")
|
|
402
|
+
).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
});
|