@frak-labs/core-sdk 0.2.1-beta.b38eef2e → 0.2.1-beta.d2556d47
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-b5cUWdAm.d.ts → computeLegacyProductId-BP-ciVsp.d.cts} +30 -44
- package/dist/{computeLegacyProductId-CCAZvLa5.d.cts → computeLegacyProductId-DiJd7RNo.d.ts} +30 -44
- 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-B8v3Vtnh.d.ts} +118 -46
- package/dist/{openSso-CMzwvaCa.d.ts → openSso-n_B4LSuW.d.cts} +118 -46
- 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-CVigMOxz.d.cts → siweAuthenticate-CDCsp8EJ.d.ts} +8 -5
- package/dist/siweAuthenticate-CfQibjZR.cjs +1 -0
- package/dist/{siweAuthenticate-CnCZ7mok.d.ts → siweAuthenticate-yITE-iKh.d.cts} +8 -5
- 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/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 +5 -3
- package/src/types/config.ts +10 -3
- package/src/types/index.ts +6 -1
- 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 +4 -0
- package/src/types/rpc.ts +7 -5
- 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-CqTHGvVa.cjs +0 -13
- package/dist/setupClient-DTyvAPgh.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
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { Currency, Language } from "./config";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Response from the merchant resolve endpoint
|
|
5
|
+
* @category Config
|
|
6
|
+
*/
|
|
7
|
+
export type MerchantConfigResponse = {
|
|
8
|
+
merchantId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
domain: string;
|
|
11
|
+
allowedDomains: string[];
|
|
12
|
+
sdkConfig?: ResolvedSdkConfig;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolved placement config from backend
|
|
17
|
+
* Translations already flattened: default + lang-specific merged into one record
|
|
18
|
+
* @category Config
|
|
19
|
+
*/
|
|
20
|
+
export type ResolvedPlacement = {
|
|
21
|
+
/** Per-component configuration within this placement */
|
|
22
|
+
components?: {
|
|
23
|
+
buttonShare?: {
|
|
24
|
+
text?: string;
|
|
25
|
+
noRewardText?: string;
|
|
26
|
+
clickAction?: "embedded-wallet" | "share-modal";
|
|
27
|
+
useReward?: boolean;
|
|
28
|
+
css?: string;
|
|
29
|
+
};
|
|
30
|
+
buttonWallet?: {
|
|
31
|
+
position?: "bottom-right" | "bottom-left";
|
|
32
|
+
css?: string;
|
|
33
|
+
};
|
|
34
|
+
openInApp?: {
|
|
35
|
+
text?: string;
|
|
36
|
+
css?: string;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
targetInteraction?: string;
|
|
40
|
+
/** Already flattened: default + lang-specific merged into one record */
|
|
41
|
+
translations?: Record<string, string>;
|
|
42
|
+
/** Global placement CSS (applied to modals/listener) */
|
|
43
|
+
css?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Resolved SDK config from backend `/resolve` endpoint
|
|
48
|
+
* Language resolution and translation merging already applied
|
|
49
|
+
* @category Config
|
|
50
|
+
*/
|
|
51
|
+
export type ResolvedSdkConfig = {
|
|
52
|
+
name?: string;
|
|
53
|
+
logoUrl?: string;
|
|
54
|
+
homepageLink?: string;
|
|
55
|
+
currency?: Currency;
|
|
56
|
+
lang?: Language;
|
|
57
|
+
/** When true, all SDK components should be hidden */
|
|
58
|
+
hidden?: boolean;
|
|
59
|
+
css?: string;
|
|
60
|
+
translations?: Record<string, string>;
|
|
61
|
+
placements?: Record<string, ResolvedPlacement>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Internal SDK config store state
|
|
66
|
+
* Merged config: backend > SDK static > defaults
|
|
67
|
+
* Components subscribe to this reactively
|
|
68
|
+
* @category Config
|
|
69
|
+
*/
|
|
70
|
+
export type SdkResolvedConfig = {
|
|
71
|
+
/** Whether the backend config has been resolved */
|
|
72
|
+
isResolved: boolean;
|
|
73
|
+
|
|
74
|
+
/** Merchant ID from resolution */
|
|
75
|
+
merchantId: string;
|
|
76
|
+
|
|
77
|
+
/** Domain returned by the resolve endpoint */
|
|
78
|
+
domain?: string;
|
|
79
|
+
|
|
80
|
+
/** Domains allowed for this merchant (used by iframe trust check) */
|
|
81
|
+
allowedDomains?: string[];
|
|
82
|
+
|
|
83
|
+
/** Whether the resolve returned a backend sdkConfig object */
|
|
84
|
+
hasRawSdkConfig?: boolean;
|
|
85
|
+
|
|
86
|
+
/** Merged metadata fields */
|
|
87
|
+
name?: string;
|
|
88
|
+
logoUrl?: string;
|
|
89
|
+
homepageLink?: string;
|
|
90
|
+
lang?: Language;
|
|
91
|
+
currency?: Currency;
|
|
92
|
+
|
|
93
|
+
/** When true, all SDK components should be hidden */
|
|
94
|
+
hidden?: boolean;
|
|
95
|
+
|
|
96
|
+
/** Global CSS from backend config (passed to iframe) */
|
|
97
|
+
css?: string;
|
|
98
|
+
|
|
99
|
+
/** Global translations (for reference / component fallback) */
|
|
100
|
+
translations?: Record<string, string>;
|
|
101
|
+
|
|
102
|
+
/** Named placements (keyed by placement ID) */
|
|
103
|
+
placements?: Record<string, ResolvedPlacement>;
|
|
104
|
+
};
|
|
@@ -26,6 +26,10 @@ export type SendInteractionParamsType =
|
|
|
26
26
|
}
|
|
27
27
|
| {
|
|
28
28
|
type: "sharing";
|
|
29
|
+
/** Epoch seconds timestamp matching the V2 context `t` field embedded in the referral link URL, used for backend correlation */
|
|
30
|
+
sharingTimestamp?: number;
|
|
31
|
+
/** Merchant order ID linking this sharing event to a purchase (stays server-side, never in URL) */
|
|
32
|
+
purchaseId?: string;
|
|
29
33
|
}
|
|
30
34
|
| {
|
|
31
35
|
type: "custom";
|
package/src/types/rpc.ts
CHANGED
|
@@ -38,7 +38,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
|
|
|
38
38
|
* - Response Type: stream (emits updates when wallet status changes)
|
|
39
39
|
*
|
|
40
40
|
* #### frak_displayModal
|
|
41
|
-
* - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"]]
|
|
41
|
+
* - Params: [requests: {@link ModalRpcStepsInput}, metadata?: {@link ModalRpcMetadata}, configMetadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
|
|
42
42
|
* - Returns: {@link ModalRpcStepsResultType}
|
|
43
43
|
* - Response Type: promise (one-shot)
|
|
44
44
|
*
|
|
@@ -53,7 +53,7 @@ import type { WalletStatusReturnType } from "./rpc/walletStatus";
|
|
|
53
53
|
* - Response Type: promise (one-shot)
|
|
54
54
|
*
|
|
55
55
|
* #### frak_displayEmbeddedWallet
|
|
56
|
-
* - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"]]
|
|
56
|
+
* - Params: [request: {@link DisplayEmbeddedWalletParamsType}, metadata: {@link FrakWalletSdkConfig}["metadata"], placement?: string]
|
|
57
57
|
* - Returns: {@link DisplayEmbeddedWalletResultType}
|
|
58
58
|
* - Response Type: promise (one-shot)
|
|
59
59
|
*/
|
|
@@ -77,6 +77,7 @@ export type IFrameRpcSchema = [
|
|
|
77
77
|
requests: ModalRpcStepsInput,
|
|
78
78
|
metadata: ModalRpcMetadata | undefined,
|
|
79
79
|
configMetadata: FrakWalletSdkConfig["metadata"],
|
|
80
|
+
placement?: string,
|
|
80
81
|
];
|
|
81
82
|
ReturnType: ModalRpcStepsResultType;
|
|
82
83
|
},
|
|
@@ -89,7 +90,7 @@ export type IFrameRpcSchema = [
|
|
|
89
90
|
Method: "frak_prepareSso";
|
|
90
91
|
Parameters: [
|
|
91
92
|
params: PrepareSsoParamsType,
|
|
92
|
-
name
|
|
93
|
+
name?: string,
|
|
93
94
|
customCss?: string,
|
|
94
95
|
];
|
|
95
96
|
ReturnType: PrepareSsoReturnType;
|
|
@@ -104,7 +105,7 @@ export type IFrameRpcSchema = [
|
|
|
104
105
|
Method: "frak_openSso";
|
|
105
106
|
Parameters: [
|
|
106
107
|
params: OpenSsoParamsType,
|
|
107
|
-
name
|
|
108
|
+
name?: string,
|
|
108
109
|
customCss?: string,
|
|
109
110
|
];
|
|
110
111
|
ReturnType: OpenSsoReturnType;
|
|
@@ -130,6 +131,7 @@ export type IFrameRpcSchema = [
|
|
|
130
131
|
Parameters: [
|
|
131
132
|
request: DisplayEmbeddedWalletParamsType,
|
|
132
133
|
metadata: FrakWalletSdkConfig["metadata"],
|
|
134
|
+
placement?: string,
|
|
133
135
|
];
|
|
134
136
|
ReturnType: DisplayEmbeddedWalletResultType;
|
|
135
137
|
},
|
|
@@ -137,7 +139,7 @@ export type IFrameRpcSchema = [
|
|
|
137
139
|
* Method to send interactions (arrival, sharing, custom events)
|
|
138
140
|
* Fire-and-forget method - no return value expected
|
|
139
141
|
* merchantId is resolved from context
|
|
140
|
-
* clientId is passed via metadata as safeguard against
|
|
142
|
+
* clientId is passed via metadata as safeguard against race conditions
|
|
141
143
|
*/
|
|
142
144
|
{
|
|
143
145
|
Method: "frak_sendInteraction";
|
|
@@ -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
|
+
});
|