@frak-labs/core-sdk 0.1.1-beta.f2691749 → 0.2.0-beta.514ef378
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/cdn/bundle.js +3 -3
- 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-CSSiQO4V.d.ts → computeLegacyProductId-BkyJ4rEY.d.ts} +3 -3
- package/dist/{computeLegacyProductId-fl_Sx8v5.d.cts → computeLegacyProductId-Raks6FXg.d.cts} +3 -3
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{openSso-CC1-loUk.d.cts → openSso-BCJGchIb.d.cts} +10 -7
- package/dist/{openSso-tkqaDQLV.d.ts → openSso-DG-_9CED.d.ts} +10 -7
- package/dist/setupClient-BEiAE56h.js +13 -0
- package/dist/setupClient-Ls3vKSlH.cjs +13 -0
- package/dist/{siweAuthenticate-CRMIfmna.d.ts → siweAuthenticate-BH7Dn7nZ.d.cts} +34 -4
- package/dist/siweAuthenticate-BJHbtty4.js +1 -0
- package/dist/{siweAuthenticate-BPqJTadR.d.cts → siweAuthenticate-Btem4QHs.d.ts} +34 -4
- package/dist/siweAuthenticate-Cwj3HP0m.cjs +1 -0
- package/dist/trackEvent-M2RLTQ2p.js +1 -0
- package/dist/trackEvent-T_R9ER2S.cjs +1 -0
- package/package.json +3 -3
- package/src/actions/ensureIdentity.ts +68 -0
- package/src/actions/index.ts +1 -0
- package/src/actions/openSso.ts +2 -0
- package/src/actions/sendInteraction.ts +2 -1
- package/src/actions/trackPurchaseStatus.test.ts +354 -141
- package/src/actions/trackPurchaseStatus.ts +45 -11
- package/src/actions/watchWalletStatus.ts +2 -2
- package/src/clients/createIFrameFrakClient.ts +3 -1
- package/src/clients/transports/iframeLifecycleManager.test.ts +2 -2
- package/src/clients/transports/iframeLifecycleManager.ts +50 -3
- package/src/types/lifecycle/client.ts +5 -5
- package/src/types/rpc.ts +6 -2
- package/src/utils/clientId.ts +3 -0
- package/src/utils/iframeHelper.test.ts +18 -5
- package/src/utils/iframeHelper.ts +4 -1
- package/src/utils/merchantId.test.ts +90 -1
- package/src/utils/merchantId.ts +22 -1
- package/src/utils/sso.ts +4 -5
- package/dist/setupClient-B-LPXmJS.js +0 -13
- package/dist/setupClient-CksNcHQl.cjs +0 -13
- package/dist/siweAuthenticate-B_Z2OZmj.cjs +0 -1
- package/dist/siweAuthenticate-CQ4OfPuA.js +0 -1
- package/dist/trackEvent-CGIryq5h.cjs +0 -1
- package/dist/trackEvent-YfUh4jrx.js +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { getBackendUrl } from "../utils/backendUrl";
|
|
2
|
+
import { getClientId } from "../utils/clientId";
|
|
3
|
+
import { fetchMerchantId } from "../utils/merchantId";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Function used to track the status of a purchase
|
|
@@ -7,6 +9,7 @@ import { getBackendUrl } from "../utils/backendUrl";
|
|
|
7
9
|
* @param args.customerId - The customer id that made the purchase (on your side)
|
|
8
10
|
* @param args.orderId - The order id of the purchase (on your side)
|
|
9
11
|
* @param args.token - The token of the purchase
|
|
12
|
+
* @param args.merchantId - Optional explicit merchant id to use for the tracking request
|
|
10
13
|
*
|
|
11
14
|
* @description This function will send a request to the backend to listen for the purchase status.
|
|
12
15
|
*
|
|
@@ -16,41 +19,72 @@ import { getBackendUrl } from "../utils/backendUrl";
|
|
|
16
19
|
* customerId: checkout.order.customer.id,
|
|
17
20
|
* orderId: checkout.order.id,
|
|
18
21
|
* token: checkout.token,
|
|
22
|
+
* merchantId: "your-merchant-id",
|
|
19
23
|
* };
|
|
20
24
|
*
|
|
21
25
|
* await trackPurchaseStatus(payload);
|
|
22
26
|
* }
|
|
23
27
|
*
|
|
24
28
|
* @remarks
|
|
25
|
-
* -
|
|
26
|
-
* - This function
|
|
29
|
+
* - Merchant id is resolved in this order: explicit `args.merchantId`, `frak-merchant-id` from session storage, then `fetchMerchantId()`.
|
|
30
|
+
* - This function supports anonymous users and will use the `x-frak-client-id` header when available.
|
|
31
|
+
* - At least one identity source must exist (`frak-wallet-interaction-token` or `x-frak-client-id`), otherwise the tracking request is skipped.
|
|
32
|
+
* - This function will print a warning if used in a non-browser environment or if no identity / merchant id can be resolved.
|
|
27
33
|
*/
|
|
28
34
|
export async function trackPurchaseStatus(args: {
|
|
29
35
|
customerId: string | number;
|
|
30
36
|
orderId: string | number;
|
|
31
37
|
token: string;
|
|
38
|
+
merchantId?: string;
|
|
32
39
|
}) {
|
|
33
40
|
if (typeof window === "undefined") {
|
|
34
41
|
console.warn("[Frak] No window found, can't track purchase");
|
|
35
42
|
return;
|
|
36
43
|
}
|
|
44
|
+
|
|
37
45
|
const interactionToken = window.sessionStorage.getItem(
|
|
38
46
|
"frak-wallet-interaction-token"
|
|
39
47
|
);
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
|
|
49
|
+
const clientId = getClientId();
|
|
50
|
+
if (!interactionToken && !clientId) {
|
|
51
|
+
console.warn("[Frak] No identity found, skipping purchase check");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const merchantIdFromStorage =
|
|
56
|
+
window.sessionStorage.getItem("frak-merchant-id");
|
|
57
|
+
const merchantId =
|
|
58
|
+
args.merchantId ?? merchantIdFromStorage ?? (await fetchMerchantId());
|
|
59
|
+
|
|
60
|
+
if (!merchantId) {
|
|
61
|
+
console.warn("[Frak] No merchant id found, skipping purchase check");
|
|
42
62
|
return;
|
|
43
63
|
}
|
|
44
64
|
|
|
65
|
+
const headers: Record<string, string> = {
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
"Content-Type": "application/json",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (interactionToken) {
|
|
71
|
+
headers["x-wallet-sdk-auth"] = interactionToken;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (clientId) {
|
|
75
|
+
headers["x-frak-client-id"] = clientId;
|
|
76
|
+
}
|
|
77
|
+
|
|
45
78
|
// Submit the listening request
|
|
46
79
|
const backendUrl = getBackendUrl();
|
|
47
|
-
await fetch(`${backendUrl}/
|
|
80
|
+
await fetch(`${backendUrl}/user/track/purchase`, {
|
|
48
81
|
method: "POST",
|
|
49
|
-
headers
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
82
|
+
headers,
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
customerId: args.customerId,
|
|
85
|
+
orderId: args.orderId,
|
|
86
|
+
token: args.token,
|
|
87
|
+
merchantId,
|
|
88
|
+
}),
|
|
55
89
|
});
|
|
56
90
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Deferred } from "@frak-labs/frame-connector";
|
|
2
2
|
import type { FrakClient } from "../types/client";
|
|
3
3
|
import type { WalletStatusReturnType } from "../types/rpc/walletStatus";
|
|
4
|
+
import { ensureIdentity } from "./ensureIdentity";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Function used to watch the current frak wallet status
|
|
@@ -81,13 +82,12 @@ function walletStatusSideEffect(
|
|
|
81
82
|
});
|
|
82
83
|
|
|
83
84
|
if (status.interactionToken) {
|
|
84
|
-
// If we got an interaction token, save it
|
|
85
85
|
window.sessionStorage.setItem(
|
|
86
86
|
"frak-wallet-interaction-token",
|
|
87
87
|
status.interactionToken
|
|
88
88
|
);
|
|
89
|
+
ensureIdentity(status.interactionToken);
|
|
89
90
|
} else {
|
|
90
|
-
// Otherwise, remove it
|
|
91
91
|
window.sessionStorage.removeItem("frak-wallet-interaction-token");
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -9,9 +9,9 @@ import type { FrakLifecycleEvent } from "../types";
|
|
|
9
9
|
import type { FrakClient } from "../types/client";
|
|
10
10
|
import type { FrakWalletSdkConfig } from "../types/config";
|
|
11
11
|
import type { IFrameRpcSchema } from "../types/rpc";
|
|
12
|
+
import { getClientId } from "../utils";
|
|
12
13
|
import { BACKUP_KEY } from "../utils/constants";
|
|
13
14
|
import { setupSsoUrlListener } from "../utils/ssoUrlListener";
|
|
14
|
-
|
|
15
15
|
import { DebugInfoGatherer } from "./DebugInfo";
|
|
16
16
|
import {
|
|
17
17
|
createIFrameLifecycleManager,
|
|
@@ -142,6 +142,7 @@ export function createIFrameFrakClient({
|
|
|
142
142
|
payload.properties = {
|
|
143
143
|
...payload.properties,
|
|
144
144
|
sdkVersion: process.env.SDK_VERSION,
|
|
145
|
+
userAnonymousClientId: getClientId(),
|
|
145
146
|
};
|
|
146
147
|
}
|
|
147
148
|
|
|
@@ -150,6 +151,7 @@ export function createIFrameFrakClient({
|
|
|
150
151
|
});
|
|
151
152
|
openPanel.setGlobalProperties({
|
|
152
153
|
sdkVersion: process.env.SDK_VERSION,
|
|
154
|
+
userAnonymousClientId: getClientId(),
|
|
153
155
|
});
|
|
154
156
|
openPanel.init();
|
|
155
157
|
}
|
|
@@ -25,7 +25,7 @@ vi.mock("../../utils/iframeHelper", () => ({
|
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
27
|
vi.mock("../../utils/clientId", () => ({
|
|
28
|
-
getClientId: vi.fn(() => "mock-client-id
|
|
28
|
+
getClientId: vi.fn(() => "mock-client-id"),
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
31
|
vi.mock("../../utils/deepLinkWithFallback", () => ({
|
|
@@ -265,7 +265,7 @@ describe("createIFrameLifecycleManager", () => {
|
|
|
265
265
|
data: {
|
|
266
266
|
token: "handshake-token-123",
|
|
267
267
|
currentUrl: "https://test.com",
|
|
268
|
-
clientId: "mock-client-id
|
|
268
|
+
clientId: "mock-client-id",
|
|
269
269
|
},
|
|
270
270
|
},
|
|
271
271
|
"https://wallet.frak.id"
|
|
@@ -8,6 +8,28 @@ import {
|
|
|
8
8
|
} from "../../utils/deepLinkWithFallback";
|
|
9
9
|
import { changeIframeVisibility } from "../../utils/iframeHelper";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Detect iOS in-app browsers (Instagram, Facebook) where server-side
|
|
13
|
+
* 302 redirects to custom URL schemes (x-safari-https://) are silently
|
|
14
|
+
* swallowed by WKWebView. Direct window.location.href assignment works.
|
|
15
|
+
*/
|
|
16
|
+
const isIOSInAppBrowser = (() => {
|
|
17
|
+
if (typeof navigator === "undefined") return false;
|
|
18
|
+
const ua = navigator.userAgent;
|
|
19
|
+
// Standard iOS or iPadOS 13+ (reports as Macintosh with touch)
|
|
20
|
+
const isIOS =
|
|
21
|
+
/iPhone|iPad|iPod/i.test(ua) ||
|
|
22
|
+
(/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1);
|
|
23
|
+
if (!isIOS) return false;
|
|
24
|
+
const lower = ua.toLowerCase();
|
|
25
|
+
return (
|
|
26
|
+
lower.includes("instagram") ||
|
|
27
|
+
lower.includes("fban") ||
|
|
28
|
+
lower.includes("fbav") ||
|
|
29
|
+
lower.includes("facebook")
|
|
30
|
+
);
|
|
31
|
+
})();
|
|
32
|
+
|
|
11
33
|
/** @ignore */
|
|
12
34
|
export type IframeLifecycleManager = {
|
|
13
35
|
isConnected: Promise<boolean>;
|
|
@@ -47,9 +69,9 @@ function handleHandshake(
|
|
|
47
69
|
data: {
|
|
48
70
|
token,
|
|
49
71
|
currentUrl: window.location.href,
|
|
50
|
-
clientId: getClientId(),
|
|
51
72
|
pendingMergeToken,
|
|
52
73
|
configDomain,
|
|
74
|
+
clientId: getClientId(),
|
|
53
75
|
},
|
|
54
76
|
},
|
|
55
77
|
targetOrigin
|
|
@@ -87,6 +109,27 @@ function computeRedirectUrl(
|
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Redirect current page to Safari via x-safari-https:// scheme.
|
|
114
|
+
* Used on iOS in-app browsers where backend 302 → custom scheme fails.
|
|
115
|
+
*/
|
|
116
|
+
function redirectToSafari(mergeToken?: string) {
|
|
117
|
+
const url = new URL(window.location.href);
|
|
118
|
+
if (mergeToken) {
|
|
119
|
+
url.searchParams.set("fmt", mergeToken);
|
|
120
|
+
}
|
|
121
|
+
const scheme =
|
|
122
|
+
url.protocol === "http:" ? "x-safari-http" : "x-safari-https";
|
|
123
|
+
window.location.href = `${scheme}://${url.host}${url.pathname}${url.search}${url.hash}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Check if this is a social/in-app-browser escape redirect (contains /common/social)
|
|
128
|
+
*/
|
|
129
|
+
function isSocialRedirect(url: string): boolean {
|
|
130
|
+
return url.includes("/common/social");
|
|
131
|
+
}
|
|
132
|
+
|
|
90
133
|
/**
|
|
91
134
|
* Handle redirect with deep link fallback
|
|
92
135
|
*/
|
|
@@ -96,9 +139,8 @@ function handleRedirect(
|
|
|
96
139
|
targetOrigin: string,
|
|
97
140
|
mergeToken?: string
|
|
98
141
|
): void {
|
|
99
|
-
const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);
|
|
100
|
-
|
|
101
142
|
if (isFrakDeepLink(baseRedirectUrl)) {
|
|
143
|
+
const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);
|
|
102
144
|
triggerDeepLinkWithFallback(finalUrl, {
|
|
103
145
|
onFallback: () => {
|
|
104
146
|
iframe.contentWindow?.postMessage(
|
|
@@ -110,7 +152,12 @@ function handleRedirect(
|
|
|
110
152
|
);
|
|
111
153
|
},
|
|
112
154
|
});
|
|
155
|
+
} else if (isIOSInAppBrowser && isSocialRedirect(baseRedirectUrl)) {
|
|
156
|
+
// iOS WKWebView silently swallows 302 redirects to custom URL
|
|
157
|
+
// schemes — bypass the server redirect entirely
|
|
158
|
+
redirectToSafari(mergeToken);
|
|
113
159
|
} else {
|
|
160
|
+
const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);
|
|
114
161
|
window.location.href = finalUrl;
|
|
115
162
|
}
|
|
116
163
|
}
|
|
@@ -38,17 +38,17 @@ type HandshakeResponse = {
|
|
|
38
38
|
data: {
|
|
39
39
|
token: string;
|
|
40
40
|
currentUrl: string;
|
|
41
|
-
/**
|
|
42
|
-
* The anonymous client ID for identity tracking
|
|
43
|
-
* Generated by SDK, stored in partner site's localStorage
|
|
44
|
-
*/
|
|
45
|
-
clientId: string;
|
|
46
41
|
/**
|
|
47
42
|
* Pending merge token extracted from URL (?fmt= parameter)
|
|
48
43
|
* When present, listener should execute identity merge in background
|
|
49
44
|
* URL is cleaned after handshake response is sent
|
|
50
45
|
*/
|
|
51
46
|
pendingMergeToken?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Client ID for identity tracking (belt & suspenders fallback)
|
|
49
|
+
* Primary delivery is via iframe URL query param; handshake is backup for SSR
|
|
50
|
+
*/
|
|
51
|
+
clientId?: string;
|
|
52
52
|
/**
|
|
53
53
|
* Explicit domain from SDK config (FrakWalletSdkConfig.domain)
|
|
54
54
|
* When present, listener should prefer this over URL-derived domain
|
package/src/types/rpc.ts
CHANGED
|
@@ -136,11 +136,15 @@ export type IFrameRpcSchema = [
|
|
|
136
136
|
/**
|
|
137
137
|
* Method to send interactions (arrival, sharing, custom events)
|
|
138
138
|
* Fire-and-forget method - no return value expected
|
|
139
|
-
* merchantId
|
|
139
|
+
* merchantId is resolved from context
|
|
140
|
+
* clientId is passed via metadata as safeguard against handshake race condition
|
|
140
141
|
*/
|
|
141
142
|
{
|
|
142
143
|
Method: "frak_sendInteraction";
|
|
143
|
-
Parameters: [
|
|
144
|
+
Parameters: [
|
|
145
|
+
interaction: SendInteractionParamsType,
|
|
146
|
+
metadata?: { clientId?: string },
|
|
147
|
+
];
|
|
144
148
|
ReturnType: undefined;
|
|
145
149
|
},
|
|
146
150
|
];
|
package/src/utils/clientId.ts
CHANGED
|
@@ -28,6 +28,9 @@ function generateUUID(): string {
|
|
|
28
28
|
export function getClientId(): string {
|
|
29
29
|
if (typeof window === "undefined" || !window.localStorage) {
|
|
30
30
|
// SSR or no localStorage - generate ephemeral ID
|
|
31
|
+
console.warn(
|
|
32
|
+
"[Frak SDK] No Window / localStorage available to save the clientId"
|
|
33
|
+
);
|
|
31
34
|
return generateUUID();
|
|
32
35
|
}
|
|
33
36
|
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("./clientId", () => ({
|
|
4
|
+
getClientId: vi.fn(() => "mock-client-id-for-test"),
|
|
5
|
+
}));
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Tests for iframe helper utilities
|
|
3
9
|
* Tests iframe creation, visibility management, and finder functions
|
|
@@ -9,7 +15,6 @@ import {
|
|
|
9
15
|
describe,
|
|
10
16
|
expect,
|
|
11
17
|
it,
|
|
12
|
-
vi,
|
|
13
18
|
} from "../../tests/vitest-fixtures";
|
|
14
19
|
import type { FrakWalletSdkConfig } from "../types";
|
|
15
20
|
import {
|
|
@@ -106,7 +111,9 @@ describe("iframeHelper", () => {
|
|
|
106
111
|
it("should set iframe src to default wallet URL", async () => {
|
|
107
112
|
await createIframe({});
|
|
108
113
|
|
|
109
|
-
expect(mockIframe.src).toBe(
|
|
114
|
+
expect(mockIframe.src).toBe(
|
|
115
|
+
"https://wallet.frak.id/listener?clientId=mock-client-id-for-test"
|
|
116
|
+
);
|
|
110
117
|
});
|
|
111
118
|
|
|
112
119
|
it("should use config walletUrl when provided", async () => {
|
|
@@ -117,13 +124,17 @@ describe("iframeHelper", () => {
|
|
|
117
124
|
|
|
118
125
|
await createIframe({ config });
|
|
119
126
|
|
|
120
|
-
expect(mockIframe.src).toBe(
|
|
127
|
+
expect(mockIframe.src).toBe(
|
|
128
|
+
"https://custom-wallet.com/listener?clientId=mock-client-id-for-test"
|
|
129
|
+
);
|
|
121
130
|
});
|
|
122
131
|
|
|
123
132
|
it("should use deprecated walletBaseUrl when provided", async () => {
|
|
124
133
|
await createIframe({ walletBaseUrl: "https://legacy-wallet.com" });
|
|
125
134
|
|
|
126
|
-
expect(mockIframe.src).toBe(
|
|
135
|
+
expect(mockIframe.src).toBe(
|
|
136
|
+
"https://legacy-wallet.com/listener?clientId=mock-client-id-for-test"
|
|
137
|
+
);
|
|
127
138
|
});
|
|
128
139
|
|
|
129
140
|
it("should prefer config.walletUrl over walletBaseUrl", async () => {
|
|
@@ -137,7 +148,9 @@ describe("iframeHelper", () => {
|
|
|
137
148
|
config,
|
|
138
149
|
});
|
|
139
150
|
|
|
140
|
-
expect(mockIframe.src).toBe(
|
|
151
|
+
expect(mockIframe.src).toBe(
|
|
152
|
+
"https://new-wallet.com/listener?clientId=mock-client-id-for-test"
|
|
153
|
+
);
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
it("should remove existing iframe before creating new one", async () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FrakWalletSdkConfig } from "../types";
|
|
2
|
+
import { getClientId } from "./clientId";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Base props for the iframe
|
|
@@ -55,13 +56,15 @@ export function createIframe({
|
|
|
55
56
|
// Set src BEFORE appending to DOM to avoid about:blank load event
|
|
56
57
|
const walletUrl =
|
|
57
58
|
config?.walletUrl ?? walletBaseUrl ?? "https://wallet.frak.id";
|
|
58
|
-
|
|
59
|
+
const clientId = getClientId();
|
|
60
|
+
iframe.src = `${walletUrl}/listener?clientId=${encodeURIComponent(clientId)}`;
|
|
59
61
|
|
|
60
62
|
return new Promise((resolve) => {
|
|
61
63
|
iframe.addEventListener("load", () => resolve(iframe));
|
|
62
64
|
document.body.appendChild(iframe);
|
|
63
65
|
});
|
|
64
66
|
}
|
|
67
|
+
|
|
65
68
|
/**
|
|
66
69
|
* Change the visibility of the given iframe
|
|
67
70
|
* @ignore
|
|
@@ -17,8 +17,9 @@ vi.mock("./backendUrl", () => ({
|
|
|
17
17
|
|
|
18
18
|
describe("merchantId", () => {
|
|
19
19
|
beforeEach(() => {
|
|
20
|
-
// Clear cache before each test
|
|
20
|
+
// Clear cache before each test (also clears sessionStorage)
|
|
21
21
|
clearMerchantIdCache();
|
|
22
|
+
window.sessionStorage.clear();
|
|
22
23
|
// Clear all mocks
|
|
23
24
|
vi.clearAllMocks();
|
|
24
25
|
// Mock console methods to avoid noise in test output
|
|
@@ -263,6 +264,70 @@ describe("merchantId", () => {
|
|
|
263
264
|
expect(result2).toBe("merchant-cached");
|
|
264
265
|
expect(global.fetch).not.toHaveBeenCalled();
|
|
265
266
|
});
|
|
267
|
+
|
|
268
|
+
it("should persist merchantId to sessionStorage after fetch", async () => {
|
|
269
|
+
const mockResponse = {
|
|
270
|
+
merchantId: "merchant-persisted",
|
|
271
|
+
name: "Test Merchant",
|
|
272
|
+
domain: "shop.example.com",
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
276
|
+
ok: true,
|
|
277
|
+
json: async () => mockResponse,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
await fetchMerchantId("shop.example.com");
|
|
281
|
+
|
|
282
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
283
|
+
"merchant-persisted"
|
|
284
|
+
);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should restore merchantId from sessionStorage when in-memory cache is cleared", async () => {
|
|
288
|
+
const mockResponse = {
|
|
289
|
+
merchantId: "merchant-storage",
|
|
290
|
+
name: "Test Merchant",
|
|
291
|
+
domain: "shop.example.com",
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
295
|
+
ok: true,
|
|
296
|
+
json: async () => mockResponse,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// First call populates both caches
|
|
300
|
+
const result1 = await fetchMerchantId("shop.example.com");
|
|
301
|
+
expect(result1).toBe("merchant-storage");
|
|
302
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
303
|
+
|
|
304
|
+
// Clear only in-memory cache (not sessionStorage)
|
|
305
|
+
vi.clearAllMocks();
|
|
306
|
+
global.fetch = vi.fn();
|
|
307
|
+
|
|
308
|
+
// Manually clear in-memory but keep sessionStorage
|
|
309
|
+
// We do this by calling the internal clear then restoring sessionStorage
|
|
310
|
+
const stored = window.sessionStorage.getItem("frak-merchant-id");
|
|
311
|
+
clearMerchantIdCache();
|
|
312
|
+
window.sessionStorage.setItem("frak-merchant-id", stored!);
|
|
313
|
+
|
|
314
|
+
// Second call should restore from sessionStorage without fetch
|
|
315
|
+
const result2 = await fetchMerchantId("shop.example.com");
|
|
316
|
+
expect(result2).toBe("merchant-storage");
|
|
317
|
+
expect(global.fetch).not.toHaveBeenCalled();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("should not write to sessionStorage when fetch fails", async () => {
|
|
321
|
+
global.fetch = vi
|
|
322
|
+
.fn()
|
|
323
|
+
.mockRejectedValueOnce(new Error("Network error"));
|
|
324
|
+
|
|
325
|
+
await fetchMerchantId("shop.example.com");
|
|
326
|
+
|
|
327
|
+
expect(
|
|
328
|
+
window.sessionStorage.getItem("frak-merchant-id")
|
|
329
|
+
).toBeNull();
|
|
330
|
+
});
|
|
266
331
|
});
|
|
267
332
|
|
|
268
333
|
describe("clearMerchantIdCache", () => {
|
|
@@ -324,6 +389,30 @@ describe("merchantId", () => {
|
|
|
324
389
|
const result2 = await fetchMerchantId("shop2.example.com");
|
|
325
390
|
expect(result2).toBe("merchant-second");
|
|
326
391
|
});
|
|
392
|
+
|
|
393
|
+
it("should clear sessionStorage when clearing cache", async () => {
|
|
394
|
+
const mockResponse = {
|
|
395
|
+
merchantId: "merchant-session-clear",
|
|
396
|
+
name: "Test Merchant",
|
|
397
|
+
domain: "shop.example.com",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
global.fetch = vi.fn().mockResolvedValueOnce({
|
|
401
|
+
ok: true,
|
|
402
|
+
json: async () => mockResponse,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await fetchMerchantId("shop.example.com");
|
|
406
|
+
expect(window.sessionStorage.getItem("frak-merchant-id")).toBe(
|
|
407
|
+
"merchant-session-clear"
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
clearMerchantIdCache();
|
|
411
|
+
|
|
412
|
+
expect(
|
|
413
|
+
window.sessionStorage.getItem("frak-merchant-id")
|
|
414
|
+
).toBeNull();
|
|
415
|
+
});
|
|
327
416
|
});
|
|
328
417
|
|
|
329
418
|
describe("resolveMerchantId", () => {
|
package/src/utils/merchantId.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { getBackendUrl } from "./backendUrl";
|
|
6
6
|
|
|
7
|
+
const MERCHANT_ID_STORAGE_KEY = "frak-merchant-id";
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Response from the merchant lookup endpoint
|
|
9
11
|
*/
|
|
@@ -39,11 +41,20 @@ export async function fetchMerchantId(
|
|
|
39
41
|
domain?: string,
|
|
40
42
|
walletUrl?: string
|
|
41
43
|
): Promise<string | undefined> {
|
|
42
|
-
// Use
|
|
44
|
+
// Use in-memory cache if available
|
|
43
45
|
if (cachedMerchantId) {
|
|
44
46
|
return cachedMerchantId;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// Check sessionStorage (survives page navigations)
|
|
50
|
+
if (typeof window !== "undefined") {
|
|
51
|
+
const stored = window.sessionStorage.getItem(MERCHANT_ID_STORAGE_KEY);
|
|
52
|
+
if (stored) {
|
|
53
|
+
cachedMerchantId = stored;
|
|
54
|
+
return stored;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
47
58
|
// If a fetch is already in progress, wait for it
|
|
48
59
|
if (cachePromise) {
|
|
49
60
|
return cachePromise;
|
|
@@ -85,6 +96,13 @@ async function fetchMerchantIdInternal(
|
|
|
85
96
|
|
|
86
97
|
const data = (await response.json()) as MerchantLookupResponse;
|
|
87
98
|
cachedMerchantId = data.merchantId;
|
|
99
|
+
// Persist to sessionStorage so it survives page navigations
|
|
100
|
+
if (typeof window !== "undefined") {
|
|
101
|
+
window.sessionStorage.setItem(
|
|
102
|
+
MERCHANT_ID_STORAGE_KEY,
|
|
103
|
+
data.merchantId
|
|
104
|
+
);
|
|
105
|
+
}
|
|
88
106
|
return cachedMerchantId;
|
|
89
107
|
} catch (error) {
|
|
90
108
|
console.warn("[Frak SDK] Failed to fetch merchantId:", error);
|
|
@@ -99,6 +117,9 @@ async function fetchMerchantIdInternal(
|
|
|
99
117
|
export function clearMerchantIdCache(): void {
|
|
100
118
|
cachedMerchantId = undefined;
|
|
101
119
|
cachePromise = undefined;
|
|
120
|
+
if (typeof window !== "undefined") {
|
|
121
|
+
window.sessionStorage.removeItem(MERCHANT_ID_STORAGE_KEY);
|
|
122
|
+
}
|
|
102
123
|
}
|
|
103
124
|
|
|
104
125
|
/**
|
package/src/utils/sso.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Hex } from "viem";
|
|
2
2
|
import type { PrepareSsoParamsType, SsoMetadata } from "../types";
|
|
3
|
-
import { getClientId } from "./clientId";
|
|
4
3
|
import { compressJsonToB64 } from "./compression/compress";
|
|
5
4
|
|
|
6
5
|
export type AppSpecificSsoMetadata = SsoMetadata & {
|
|
@@ -25,8 +24,8 @@ export type FullSsoParams = Omit<PrepareSsoParamsType, "metadata"> & {
|
|
|
25
24
|
* @param params - SSO parameters
|
|
26
25
|
* @param merchantId - Merchant identifier
|
|
27
26
|
* @param name - Application name
|
|
27
|
+
* @param clientId - Client identifier for identity tracking
|
|
28
28
|
* @param css - Optional custom CSS
|
|
29
|
-
* @param clientId - Optional client identifier (auto-generated if omitted)
|
|
30
29
|
* @returns Complete SSO URL ready to open in popup or redirect
|
|
31
30
|
*
|
|
32
31
|
* @example
|
|
@@ -45,8 +44,8 @@ export function generateSsoUrl(
|
|
|
45
44
|
params: PrepareSsoParamsType,
|
|
46
45
|
merchantId: string,
|
|
47
46
|
name: string,
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
clientId: string,
|
|
48
|
+
css?: string
|
|
50
49
|
): string {
|
|
51
50
|
// Build full params with app-specific metadata
|
|
52
51
|
const fullParams: FullSsoParams = {
|
|
@@ -60,7 +59,7 @@ export function generateSsoUrl(
|
|
|
60
59
|
logoUrl: params.metadata?.logoUrl,
|
|
61
60
|
homepageLink: params.metadata?.homepageLink,
|
|
62
61
|
},
|
|
63
|
-
clientId
|
|
62
|
+
clientId,
|
|
64
63
|
};
|
|
65
64
|
|
|
66
65
|
// Compress params to minimal format
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import{d as e,l as t,r as n}from"./trackEvent-YfUh4jrx.js";import{Deferred as r,FrakRpcError as i,RpcErrorCodes as a,createRpcClient as o,jsonDecode as s}from"@frak-labs/frame-connector";import{OpenPanel as c}from"@openpanel/web";const l=`nexus-wallet-backup`,u=`frakwallet://`;function d(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}var f=class e{config;iframe;isSetupDone=!1;lastResponse=null;lastRequest=null;constructor(e,t){this.config=e,this.iframe=t,this.lastRequest=null,this.lastResponse=null}setLastResponse(e,t){this.lastResponse={message:e,response:t,timestamp:Date.now()}}setLastRequest(e){this.lastRequest={event:e,timestamp:Date.now()}}updateSetupStatus(e){this.isSetupDone=e}base64Encode(e){try{return btoa(JSON.stringify(e))}catch(e){return console.warn(`Failed to encode debug data`,e),btoa(`Failed to encode data`)}}getIframeStatus(){return this.iframe?{loading:this.iframe.hasAttribute(`loading`),url:this.iframe.src,readyState:this.iframe.contentDocument?.readyState?this.iframe.contentDocument.readyState===`complete`?1:0:-1,contentWindow:!!this.iframe.contentWindow,isConnected:this.iframe.isConnected}:null}getNavigatorInfo(){return navigator?{userAgent:navigator.userAgent,language:navigator.language,onLine:navigator.onLine,screenWidth:window.screen.width,screenHeight:window.screen.height,pixelRatio:window.devicePixelRatio}:null}gatherDebugInfo(e){let t=this.getIframeStatus(),n=this.getNavigatorInfo(),r=`Unknown`;return e instanceof i?r=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?r=e.message:typeof e==`string`&&(r=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:n?this.base64Encode(n):`no-navigator`,iframeStatus:t?this.base64Encode(t):`not-iframe`,lastRequest:this.lastRequest?this.base64Encode(this.lastRequest):`No Frak request logged`,lastResponse:this.lastResponse?this.base64Encode(this.lastResponse):`No Frak response logged`,clientStatus:this.isSetupDone?`setup`:`not-setup`,error:r}}static empty(){return new e}formatDebugInfo(e){let t=this.gatherDebugInfo(e);return`
|
|
2
|
-
Debug Information:
|
|
3
|
-
-----------------
|
|
4
|
-
Timestamp: ${t.timestamp}
|
|
5
|
-
URL: ${t.encodedUrl}
|
|
6
|
-
Config: ${t.encodedConfig}
|
|
7
|
-
Navigator Info: ${t.navigatorInfo}
|
|
8
|
-
IFrame Status: ${t.iframeStatus}
|
|
9
|
-
Last Request: ${t.lastRequest}
|
|
10
|
-
Last Response: ${t.lastResponse}
|
|
11
|
-
Client Status: ${t.clientStatus}
|
|
12
|
-
Error: ${t.error}
|
|
13
|
-
`.trim()}};function p(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function m(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function h(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=p()&&g(e)?m(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function g(e){return e.startsWith(u)}const _={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function v({walletBaseUrl:e,config:t}){let n=document.querySelector(`#frak-wallet`);n&&n.remove();let r=document.createElement(`iframe`);return r.id=_.id,r.name=_.name,r.allow=_.allow,r.style.zIndex=_.style.zIndex.toString(),y({iframe:r,isVisible:!1}),r.src=`${t?.walletUrl??e??`https://wallet.frak.id`}/listener`,new Promise(e=>{r.addEventListener(`load`,()=>e(r)),document.body.appendChild(r)})}function y({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function b(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function x(e){e?localStorage.setItem(l,e):localStorage.removeItem(l)}function S(t,n,r,i){let a=new URL(window.location.href),o=a.searchParams.get(`fmt`)??void 0;t.contentWindow?.postMessage({clientLifecycle:`handshake-response`,data:{token:n,currentUrl:window.location.href,clientId:e(),pendingMergeToken:o,configDomain:i}},r),o&&(a.searchParams.delete(`fmt`),window.history.replaceState({},``,a.toString()))}function C(e,t){try{let n=new URL(e);return n.searchParams.has(`u`)?(n.searchParams.delete(`u`),n.searchParams.append(`u`,window.location.href),t&&n.searchParams.append(`fmt`,t),n.toString()):e}catch{return e}}function w(e,t,n,r){let i=C(t,r);g(t)?h(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}}):window.location.href=i}function T({iframe:e,targetOrigin:t,configDomain:n}){let i=new r;return{handleEvent:async r=>{if(!(`iframeLifecycle`in r))return;let{iframeLifecycle:a,data:o}=r;switch(a){case`connected`:i.resolve(!0);break;case`do-backup`:x(o.backup);break;case`remove-backup`:localStorage.removeItem(l);break;case`show`:case`hide`:y({iframe:e,isVisible:a===`show`});break;case`handshake`:S(e,o.token,t,n);break;case`redirect`:w(e,o.baseRedirectUrl,t,o.mergeToken);break}},isConnected:i.promise}}function E({config:e,iframe:t}){let n=e?.walletUrl??`https://wallet.frak.id`,r=T({iframe:t,targetOrigin:n,configDomain:e.domain}),s=new f(e,t);if(!t.contentWindow)throw new i(a.configError,`The iframe does not have a content window`);let l=o({emittingTransport:t.contentWindow,listeningTransport:window,targetOrigin:n,middleware:[{async onRequest(e,t){if(!await r.isConnected)throw new i(a.clientNotConnected,`The iframe provider isn't connected yet`);return t}},{onRequest(e,t){return s.setLastRequest(e),t},onResponse(e,t){return s.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:async(e,t)=>{await r.handleEvent(e)}}}),u=D(l,r),d=async()=>{u(),l.cleanup(),t.remove()},p;console.log(`[Frak SDK] Initializing OpenPanel`),p=new c({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:e,payload:t})=>(e!==`track`||!t?.properties||`sdkVersion`in t.properties||(t.properties={...t.properties,sdkVersion:`0.1.1`}),!0)}),p.setGlobalProperties({sdkVersion:`0.1.1`}),p.init();let m=O({config:e,rpcClient:l,lifecycleManager:r}).then(()=>s.updateSetupStatus(!0));return{config:e,debugInfo:s,waitForConnection:r.isConnected,waitForSetup:m,request:l.request,listenerRequest:l.listen,destroy:d,openPanel:p}}function D(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,1e3),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function O({config:e,rpcClient:t,lifecycleManager:n}){await n.isConnected,d(t,n.isConnected);async function r(){let n=e.customizations?.css;if(!n)return;let r={clientLifecycle:`modal-css`,data:{cssLink:n}};t.sendLifecycle(r)}async function i(){let n=e.customizations?.i18n;if(!n)return;let r={clientLifecycle:`modal-i18n`,data:{i18n:n}};t.sendLifecycle(r)}async function a(){if(typeof window>`u`)return;let e=window.localStorage.getItem(l);if(!e)return;let n={clientLifecycle:`restore-backup`,data:{backup:e}};t.sendLifecycle(n)}await Promise.allSettled([r(),i(),a()])}function k(e){return s(t(e))}const A={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function j(e){return e&&e in A?e:`eur`}function M(e){return e?A[e]??A.eur:A.eur}function N(e,t){let n=M(t),r=j(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function P(e){return e?`${e}Amount`:`eurAmount`}let F,I;async function L(e,t){if(F)return F;if(I)return I;I=R(e,t);let n=await I;return I=void 0,n}async function R(e,t){let r=e??(typeof window<`u`?window.location.hostname:``);if(r)try{let e=n(t),i=await fetch(`${e}/user/merchant/resolve?domain=${encodeURIComponent(r)}`);if(!i.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${r}: ${i.status}`);return}return F=(await i.json()).merchantId,F}catch(e){console.warn(`[Frak SDK] Failed to fetch merchantId:`,e);return}}function z(){F=void 0,I=void 0}async function B(e,t){return e.metadata?.merchantId?e.metadata.merchantId:L(void 0,t)}async function V({config:e}){let t=H(e),n=await v({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=E({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function H(e){let t=j(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}export{m as _,P as a,u as b,j as c,E as d,_ as f,g,p as h,B as i,A as l,b as m,z as n,N as o,v as p,L as r,M as s,V as t,k as u,h as v,f as y};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
const e=require(`./trackEvent-CGIryq5h.cjs`);let t=require(`@frak-labs/frame-connector`),n=require(`@openpanel/web`);const r=`nexus-wallet-backup`,i=`frakwallet://`;function a(e,t){if(typeof window>`u`)return;let n=new URL(window.location.href),r=n.searchParams.get(`sso`);r&&(t.then(()=>{e.sendLifecycle({clientLifecycle:`sso-redirect-complete`,data:{compressed:r}}),console.log(`[SSO URL Listener] Forwarded compressed SSO data to iframe`)}).catch(e=>{console.error(`[SSO URL Listener] Failed to forward SSO data:`,e)}),n.searchParams.delete(`sso`),window.history.replaceState({},``,n.toString()),console.log(`[SSO URL Listener] SSO parameter detected and URL cleaned`))}var o=class e{config;iframe;isSetupDone=!1;lastResponse=null;lastRequest=null;constructor(e,t){this.config=e,this.iframe=t,this.lastRequest=null,this.lastResponse=null}setLastResponse(e,t){this.lastResponse={message:e,response:t,timestamp:Date.now()}}setLastRequest(e){this.lastRequest={event:e,timestamp:Date.now()}}updateSetupStatus(e){this.isSetupDone=e}base64Encode(e){try{return btoa(JSON.stringify(e))}catch(e){return console.warn(`Failed to encode debug data`,e),btoa(`Failed to encode data`)}}getIframeStatus(){return this.iframe?{loading:this.iframe.hasAttribute(`loading`),url:this.iframe.src,readyState:this.iframe.contentDocument?.readyState?this.iframe.contentDocument.readyState===`complete`?1:0:-1,contentWindow:!!this.iframe.contentWindow,isConnected:this.iframe.isConnected}:null}getNavigatorInfo(){return navigator?{userAgent:navigator.userAgent,language:navigator.language,onLine:navigator.onLine,screenWidth:window.screen.width,screenHeight:window.screen.height,pixelRatio:window.devicePixelRatio}:null}gatherDebugInfo(e){let n=this.getIframeStatus(),r=this.getNavigatorInfo(),i=`Unknown`;return e instanceof t.FrakRpcError?i=`FrakRpcError: ${e.code} '${e.message}'`:e instanceof Error?i=e.message:typeof e==`string`&&(i=e),{timestamp:new Date().toISOString(),encodedUrl:btoa(window.location.href),encodedConfig:this.config?this.base64Encode(this.config):`no-config`,navigatorInfo:r?this.base64Encode(r):`no-navigator`,iframeStatus:n?this.base64Encode(n):`not-iframe`,lastRequest:this.lastRequest?this.base64Encode(this.lastRequest):`No Frak request logged`,lastResponse:this.lastResponse?this.base64Encode(this.lastResponse):`No Frak response logged`,clientStatus:this.isSetupDone?`setup`:`not-setup`,error:i}}static empty(){return new e}formatDebugInfo(e){let t=this.gatherDebugInfo(e);return`
|
|
2
|
-
Debug Information:
|
|
3
|
-
-----------------
|
|
4
|
-
Timestamp: ${t.timestamp}
|
|
5
|
-
URL: ${t.encodedUrl}
|
|
6
|
-
Config: ${t.encodedConfig}
|
|
7
|
-
Navigator Info: ${t.navigatorInfo}
|
|
8
|
-
IFrame Status: ${t.iframeStatus}
|
|
9
|
-
Last Request: ${t.lastRequest}
|
|
10
|
-
Last Response: ${t.lastResponse}
|
|
11
|
-
Client Status: ${t.clientStatus}
|
|
12
|
-
Error: ${t.error}
|
|
13
|
-
`.trim()}};function s(){let e=navigator.userAgent;return/Android/i.test(e)&&/Chrome\/\d+/i.test(e)}function c(e){return`intent://${e.slice(13)}#Intent;scheme=frakwallet;end`}function l(e,t){let n=t?.timeout??2500,r=!1,i=()=>{document.hidden&&(r=!0)};document.addEventListener(`visibilitychange`,i);let a=s()&&u(e)?c(e):e;window.location.href=a,setTimeout(()=>{document.removeEventListener(`visibilitychange`,i),r||t?.onFallback?.()},n)}function u(e){return e.startsWith(i)}const d={id:`frak-wallet`,name:`frak-wallet`,title:`Frak Wallet`,allow:`publickey-credentials-get *; clipboard-write; web-share *`,style:{width:`0`,height:`0`,border:`0`,position:`absolute`,zIndex:2000001,top:`-1000px`,left:`-1000px`,colorScheme:`auto`}};function f({walletBaseUrl:e,config:t}){let n=document.querySelector(`#frak-wallet`);n&&n.remove();let r=document.createElement(`iframe`);return r.id=d.id,r.name=d.name,r.allow=d.allow,r.style.zIndex=d.style.zIndex.toString(),p({iframe:r,isVisible:!1}),r.src=`${t?.walletUrl??e??`https://wallet.frak.id`}/listener`,new Promise(e=>{r.addEventListener(`load`,()=>e(r)),document.body.appendChild(r)})}function p({iframe:e,isVisible:t}){if(!t){e.style.width=`0`,e.style.height=`0`,e.style.border=`0`,e.style.position=`fixed`,e.style.top=`-1000px`,e.style.left=`-1000px`;return}e.style.position=`fixed`,e.style.top=`0`,e.style.left=`0`,e.style.width=`100%`,e.style.height=`100%`,e.style.pointerEvents=`auto`}function m(e=`/listener`){if(!window.opener)return null;let t=t=>{try{return t.location.origin===window.location.origin&&t.location.pathname===e}catch{return!1}};if(t(window.opener))return window.opener;try{let e=window.opener.frames;for(let n=0;n<e.length;n++)if(t(e[n]))return e[n];return null}catch(t){return console.error(`[findIframeInOpener] Error finding iframe with pathname ${e}:`,t),null}}function h(e){e?localStorage.setItem(r,e):localStorage.removeItem(r)}function g(t,n,r,i){let a=new URL(window.location.href),o=a.searchParams.get(`fmt`)??void 0;t.contentWindow?.postMessage({clientLifecycle:`handshake-response`,data:{token:n,currentUrl:window.location.href,clientId:e.d(),pendingMergeToken:o,configDomain:i}},r),o&&(a.searchParams.delete(`fmt`),window.history.replaceState({},``,a.toString()))}function _(e,t){try{let n=new URL(e);return n.searchParams.has(`u`)?(n.searchParams.delete(`u`),n.searchParams.append(`u`,window.location.href),t&&n.searchParams.append(`fmt`,t),n.toString()):e}catch{return e}}function v(e,t,n,r){let i=_(t,r);u(t)?l(i,{onFallback:()=>{e.contentWindow?.postMessage({clientLifecycle:`deep-link-failed`,data:{originalUrl:i}},n)}}):window.location.href=i}function y({iframe:e,targetOrigin:n,configDomain:i}){let a=new t.Deferred;return{handleEvent:async t=>{if(!(`iframeLifecycle`in t))return;let{iframeLifecycle:o,data:s}=t;switch(o){case`connected`:a.resolve(!0);break;case`do-backup`:h(s.backup);break;case`remove-backup`:localStorage.removeItem(r);break;case`show`:case`hide`:p({iframe:e,isVisible:o===`show`});break;case`handshake`:g(e,s.token,n,i);break;case`redirect`:v(e,s.baseRedirectUrl,n,s.mergeToken);break}},isConnected:a.promise}}function b({config:e,iframe:r}){let i=e?.walletUrl??`https://wallet.frak.id`,a=y({iframe:r,targetOrigin:i,configDomain:e.domain}),s=new o(e,r);if(!r.contentWindow)throw new t.FrakRpcError(t.RpcErrorCodes.configError,`The iframe does not have a content window`);let c=(0,t.createRpcClient)({emittingTransport:r.contentWindow,listeningTransport:window,targetOrigin:i,middleware:[{async onRequest(e,n){if(!await a.isConnected)throw new t.FrakRpcError(t.RpcErrorCodes.clientNotConnected,`The iframe provider isn't connected yet`);return n}},{onRequest(e,t){return s.setLastRequest(e),t},onResponse(e,t){return s.setLastResponse(e,t),t}}],lifecycleHandlers:{iframeLifecycle:async(e,t)=>{await a.handleEvent(e)}}}),l=x(c,a),u=async()=>{l(),c.cleanup(),r.remove()},d;console.log(`[Frak SDK] Initializing OpenPanel`),d=new n.OpenPanel({apiUrl:`https://op-api.gcp.frak.id`,clientId:`6eacc8d7-49ac-4936-95e9-81ef29449570`,trackScreenViews:!0,trackOutgoingLinks:!0,trackAttributes:!1,filter:({type:e,payload:t})=>(e!==`track`||!t?.properties||`sdkVersion`in t.properties||(t.properties={...t.properties,sdkVersion:`0.1.1`}),!0)}),d.setGlobalProperties({sdkVersion:`0.1.1`}),d.init();let f=S({config:e,rpcClient:c,lifecycleManager:a}).then(()=>s.updateSetupStatus(!0));return{config:e,debugInfo:s,waitForConnection:a.isConnected,waitForSetup:f,request:c.request,listenerRequest:c.listen,destroy:u,openPanel:d}}function x(e,t){let n,r,i=()=>e.sendLifecycle({clientLifecycle:`heartbeat`});async function a(){i(),n=setInterval(i,1e3),r=setTimeout(()=>{o(),console.log(`Heartbeat timeout: connection failed`)},3e4),await t.isConnected,o()}function o(){n&&clearInterval(n),r&&clearTimeout(r)}return a(),o}async function S({config:e,rpcClient:t,lifecycleManager:n}){await n.isConnected,a(t,n.isConnected);async function i(){let n=e.customizations?.css;if(!n)return;let r={clientLifecycle:`modal-css`,data:{cssLink:n}};t.sendLifecycle(r)}async function o(){let n=e.customizations?.i18n;if(!n)return;let r={clientLifecycle:`modal-i18n`,data:{i18n:n}};t.sendLifecycle(r)}async function s(){if(typeof window>`u`)return;let e=window.localStorage.getItem(r);if(!e)return;let n={clientLifecycle:`restore-backup`,data:{backup:e}};t.sendLifecycle(n)}await Promise.allSettled([i(),o(),s()])}function C(n){return(0,t.jsonDecode)(e.l(n))}const w={eur:`fr-FR`,usd:`en-US`,gbp:`en-GB`};function T(e){return e&&e in w?e:`eur`}function E(e){return e?w[e]??w.eur:w.eur}function D(e,t){let n=E(t),r=T(t);return e.toLocaleString(n,{style:`currency`,currency:r,minimumFractionDigits:0,maximumFractionDigits:2})}function O(e){return e?`${e}Amount`:`eurAmount`}let k,A;async function j(e,t){if(k)return k;if(A)return A;A=M(e,t);let n=await A;return A=void 0,n}async function M(t,n){let r=t??(typeof window<`u`?window.location.hostname:``);if(r)try{let t=e.r(n),i=await fetch(`${t}/user/merchant/resolve?domain=${encodeURIComponent(r)}`);if(!i.ok){console.warn(`[Frak SDK] Merchant lookup failed for domain ${r}: ${i.status}`);return}return k=(await i.json()).merchantId,k}catch(e){console.warn(`[Frak SDK] Failed to fetch merchantId:`,e);return}}function N(){k=void 0,A=void 0}async function P(e,t){return e.metadata?.merchantId?e.metadata.merchantId:j(void 0,t)}async function F({config:e}){let t=I(e),n=await f({config:t});if(!n){console.error(`Failed to create iframe`);return}let r=b({config:t,iframe:n});if(await r.waitForSetup,!await r.waitForConnection){console.error(`Failed to connect to client`);return}return r}function I(e){let t=T(e.metadata?.currency);return{...e,metadata:{...e.metadata,currency:t}}}Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return O}}),Object.defineProperty(exports,`b`,{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return T}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return P}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return w}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return N}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return D}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return f}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return j}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return E}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return C}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return o}});
|