@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.1e44255d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/cdn/bundle.js +3 -8
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +3 -1400
- package/dist/actions.d.ts +3 -1400
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -13
- package/dist/bundle.d.cts +4 -1927
- package/dist/bundle.d.ts +4 -1927
- package/dist/bundle.js +1 -13
- package/dist/computeLegacyProductId-BkyJ4rEY.d.ts +538 -0
- package/dist/computeLegacyProductId-Raks6FXg.d.cts +538 -0
- package/dist/index.cjs +1 -13
- package/dist/index.d.cts +3 -1269
- package/dist/index.d.ts +3 -1269
- package/dist/index.js +1 -13
- package/dist/openSso-BCJGchIb.d.cts +1022 -0
- package/dist/openSso-DG-_9CED.d.ts +1022 -0
- package/dist/setupClient-Cfwpu08d.js +13 -0
- package/dist/setupClient-Dh8ljuhV.cjs +13 -0
- package/dist/siweAuthenticate-BH7Dn7nZ.d.cts +536 -0
- package/dist/siweAuthenticate-BJHbtty4.js +1 -0
- package/dist/siweAuthenticate-Btem4QHs.d.ts +536 -0
- 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 +25 -31
- package/src/actions/displayEmbeddedWallet.test.ts +194 -0
- package/src/actions/displayEmbeddedWallet.ts +21 -0
- package/src/actions/displayModal.test.ts +388 -0
- package/src/actions/displayModal.ts +120 -0
- package/src/actions/ensureIdentity.ts +68 -0
- package/src/actions/getMerchantInformation.test.ts +116 -0
- package/src/actions/getMerchantInformation.ts +16 -0
- package/src/actions/index.ts +30 -0
- package/src/actions/openSso.ts +118 -0
- package/src/actions/prepareSso.test.ts +223 -0
- package/src/actions/prepareSso.ts +48 -0
- package/src/actions/referral/processReferral.test.ts +248 -0
- package/src/actions/referral/processReferral.ts +232 -0
- package/src/actions/referral/referralInteraction.test.ts +147 -0
- package/src/actions/referral/referralInteraction.ts +52 -0
- package/src/actions/sendInteraction.ts +56 -0
- package/src/actions/trackPurchaseStatus.test.ts +500 -0
- package/src/actions/trackPurchaseStatus.ts +90 -0
- package/src/actions/watchWalletStatus.test.ts +372 -0
- package/src/actions/watchWalletStatus.ts +93 -0
- package/src/actions/wrapper/modalBuilder.test.ts +239 -0
- package/src/actions/wrapper/modalBuilder.ts +203 -0
- package/src/actions/wrapper/sendTransaction.test.ts +164 -0
- package/src/actions/wrapper/sendTransaction.ts +62 -0
- package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
- package/src/actions/wrapper/siweAuthenticate.ts +94 -0
- package/src/bundle.ts +2 -0
- package/src/clients/DebugInfo.test.ts +418 -0
- package/src/clients/DebugInfo.ts +182 -0
- package/src/clients/createIFrameFrakClient.ts +292 -0
- package/src/clients/index.ts +3 -0
- package/src/clients/setupClient.test.ts +343 -0
- package/src/clients/setupClient.ts +73 -0
- package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
- package/src/clients/transports/iframeLifecycleManager.ts +229 -0
- package/src/constants/interactionTypes.ts +15 -0
- package/src/constants/locales.ts +14 -0
- package/src/index.ts +109 -0
- package/src/types/client.ts +14 -0
- package/src/types/compression.ts +22 -0
- package/src/types/config.ts +117 -0
- package/src/types/context.ts +13 -0
- package/src/types/index.ts +74 -0
- package/src/types/lifecycle/client.ts +69 -0
- package/src/types/lifecycle/iframe.ts +41 -0
- package/src/types/lifecycle/index.ts +2 -0
- package/src/types/rpc/displayModal.ts +82 -0
- package/src/types/rpc/embedded/index.ts +68 -0
- package/src/types/rpc/embedded/loggedIn.ts +55 -0
- package/src/types/rpc/embedded/loggedOut.ts +28 -0
- package/src/types/rpc/interaction.ts +30 -0
- package/src/types/rpc/merchantInformation.ts +77 -0
- package/src/types/rpc/modal/final.ts +46 -0
- package/src/types/rpc/modal/generic.ts +46 -0
- package/src/types/rpc/modal/index.ts +16 -0
- package/src/types/rpc/modal/login.ts +36 -0
- package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
- package/src/types/rpc/modal/transaction.ts +33 -0
- package/src/types/rpc/sso.ts +80 -0
- package/src/types/rpc/walletStatus.ts +29 -0
- package/src/types/rpc.ts +150 -0
- package/src/types/tracking.ts +60 -0
- package/src/types/transport.ts +34 -0
- package/src/utils/FrakContext.test.ts +407 -0
- package/src/utils/FrakContext.ts +158 -0
- package/src/utils/backendUrl.test.ts +83 -0
- package/src/utils/backendUrl.ts +62 -0
- package/src/utils/clientId.test.ts +41 -0
- package/src/utils/clientId.ts +43 -0
- package/src/utils/compression/b64.test.ts +181 -0
- package/src/utils/compression/b64.ts +29 -0
- package/src/utils/compression/compress.test.ts +123 -0
- package/src/utils/compression/compress.ts +11 -0
- package/src/utils/compression/decompress.test.ts +149 -0
- package/src/utils/compression/decompress.ts +11 -0
- package/src/utils/compression/index.ts +3 -0
- package/src/utils/computeLegacyProductId.ts +11 -0
- package/src/utils/constants.test.ts +23 -0
- package/src/utils/constants.ts +9 -0
- package/src/utils/deepLinkWithFallback.test.ts +243 -0
- package/src/utils/deepLinkWithFallback.ts +103 -0
- package/src/utils/formatAmount.test.ts +113 -0
- package/src/utils/formatAmount.ts +24 -0
- package/src/utils/getCurrencyAmountKey.test.ts +44 -0
- package/src/utils/getCurrencyAmountKey.ts +15 -0
- package/src/utils/getSupportedCurrency.test.ts +51 -0
- package/src/utils/getSupportedCurrency.ts +14 -0
- package/src/utils/getSupportedLocale.test.ts +64 -0
- package/src/utils/getSupportedLocale.ts +16 -0
- package/src/utils/iframeHelper.test.ts +463 -0
- package/src/utils/iframeHelper.ts +150 -0
- package/src/utils/index.ts +36 -0
- package/src/utils/merchantId.test.ts +653 -0
- package/src/utils/merchantId.ts +143 -0
- package/src/utils/sso.ts +126 -0
- package/src/utils/ssoUrlListener.test.ts +252 -0
- package/src/utils/ssoUrlListener.ts +60 -0
- package/src/utils/trackEvent.test.ts +180 -0
- package/src/utils/trackEvent.ts +41 -0
- package/cdn/bundle.js.LICENSE.txt +0 -10
- package/dist/interactions.cjs +0 -1
- package/dist/interactions.d.cts +0 -182
- package/dist/interactions.d.ts +0 -182
- package/dist/interactions.js +0 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merchant ID utilities for auto-fetching from backend
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getBackendUrl } from "./backendUrl";
|
|
6
|
+
|
|
7
|
+
const MERCHANT_ID_STORAGE_KEY = "frak-merchant-id";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Response from the merchant lookup endpoint
|
|
11
|
+
*/
|
|
12
|
+
type MerchantLookupResponse = {
|
|
13
|
+
merchantId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
domain: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* In-memory cache for merchantId lookups
|
|
20
|
+
* Persists for the session to avoid repeated API calls
|
|
21
|
+
*/
|
|
22
|
+
let cachedMerchantId: string | undefined;
|
|
23
|
+
let cachePromise: Promise<string | undefined> | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch merchantId from backend by domain
|
|
27
|
+
*
|
|
28
|
+
* @param domain - The domain to lookup (defaults to current hostname)
|
|
29
|
+
* @param walletUrl - Optional wallet URL to derive backend URL
|
|
30
|
+
* @returns The merchantId if found, undefined otherwise
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const merchantId = await fetchMerchantId("shop.example.com");
|
|
35
|
+
* if (merchantId) {
|
|
36
|
+
* // Use merchantId for tracking
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function fetchMerchantId(
|
|
41
|
+
domain?: string,
|
|
42
|
+
walletUrl?: string
|
|
43
|
+
): Promise<string | undefined> {
|
|
44
|
+
// Use in-memory cache if available
|
|
45
|
+
if (cachedMerchantId) {
|
|
46
|
+
return cachedMerchantId;
|
|
47
|
+
}
|
|
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
|
+
|
|
58
|
+
// If a fetch is already in progress, wait for it
|
|
59
|
+
if (cachePromise) {
|
|
60
|
+
return cachePromise;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Start the fetch and cache the promise
|
|
64
|
+
cachePromise = fetchMerchantIdInternal(domain, walletUrl);
|
|
65
|
+
const result = await cachePromise;
|
|
66
|
+
cachePromise = undefined;
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Internal fetch logic
|
|
72
|
+
*/
|
|
73
|
+
async function fetchMerchantIdInternal(
|
|
74
|
+
domain?: string,
|
|
75
|
+
walletUrl?: string
|
|
76
|
+
): Promise<string | undefined> {
|
|
77
|
+
const targetDomain =
|
|
78
|
+
domain ??
|
|
79
|
+
(typeof window !== "undefined" ? window.location.hostname : "");
|
|
80
|
+
if (!targetDomain) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const backendUrl = getBackendUrl(walletUrl);
|
|
86
|
+
const response = await fetch(
|
|
87
|
+
`${backendUrl}/user/merchant/resolve?domain=${encodeURIComponent(targetDomain)}`
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
console.warn(
|
|
92
|
+
`[Frak SDK] Merchant lookup failed for domain ${targetDomain}: ${response.status}`
|
|
93
|
+
);
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const data = (await response.json()) as MerchantLookupResponse;
|
|
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
|
+
}
|
|
106
|
+
return cachedMerchantId;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.warn("[Frak SDK] Failed to fetch merchantId:", error);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Clear the cached merchantId
|
|
115
|
+
* Useful for testing or when switching domains
|
|
116
|
+
*/
|
|
117
|
+
export function clearMerchantIdCache(): void {
|
|
118
|
+
cachedMerchantId = undefined;
|
|
119
|
+
cachePromise = undefined;
|
|
120
|
+
if (typeof window !== "undefined") {
|
|
121
|
+
window.sessionStorage.removeItem(MERCHANT_ID_STORAGE_KEY);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get merchantId from config or auto-fetch from backend
|
|
127
|
+
*
|
|
128
|
+
* @param config - The SDK config that may contain merchantId
|
|
129
|
+
* @param walletUrl - Optional wallet URL to derive backend URL
|
|
130
|
+
* @returns The merchantId if available (from config or fetch), undefined otherwise
|
|
131
|
+
*/
|
|
132
|
+
export async function resolveMerchantId(
|
|
133
|
+
config: { metadata?: { merchantId?: string } },
|
|
134
|
+
walletUrl?: string
|
|
135
|
+
): Promise<string | undefined> {
|
|
136
|
+
// First, check config
|
|
137
|
+
if (config.metadata?.merchantId) {
|
|
138
|
+
return config.metadata.merchantId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Otherwise, try to fetch from backend
|
|
142
|
+
return fetchMerchantId(undefined, walletUrl);
|
|
143
|
+
}
|
package/src/utils/sso.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Hex } from "viem";
|
|
2
|
+
import type { PrepareSsoParamsType, SsoMetadata } from "../types";
|
|
3
|
+
import { compressJsonToB64 } from "./compression/compress";
|
|
4
|
+
|
|
5
|
+
export type AppSpecificSsoMetadata = SsoMetadata & {
|
|
6
|
+
name: string;
|
|
7
|
+
css?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The full SSO params that will be used for compression
|
|
12
|
+
*/
|
|
13
|
+
export type FullSsoParams = Omit<PrepareSsoParamsType, "metadata"> & {
|
|
14
|
+
metadata: AppSpecificSsoMetadata;
|
|
15
|
+
merchantId: string;
|
|
16
|
+
clientId: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate SSO URL with compressed parameters
|
|
21
|
+
* This mirrors the wallet's getOpenSsoLink() function
|
|
22
|
+
*
|
|
23
|
+
* @param walletUrl - Base wallet URL (e.g., "https://wallet.frak.id")
|
|
24
|
+
* @param params - SSO parameters
|
|
25
|
+
* @param merchantId - Merchant identifier
|
|
26
|
+
* @param name - Application name
|
|
27
|
+
* @param clientId - Client identifier for identity tracking
|
|
28
|
+
* @param css - Optional custom CSS
|
|
29
|
+
* @returns Complete SSO URL ready to open in popup or redirect
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const ssoUrl = generateSsoUrl(
|
|
34
|
+
* "https://wallet.frak.id",
|
|
35
|
+
* { metadata: { logoUrl: "..." }, directExit: true },
|
|
36
|
+
* "0x123...",
|
|
37
|
+
* "My App"
|
|
38
|
+
* );
|
|
39
|
+
* // Returns: https://wallet.frak.id/sso?p=<compressed_base64>
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function generateSsoUrl(
|
|
43
|
+
walletUrl: string,
|
|
44
|
+
params: PrepareSsoParamsType,
|
|
45
|
+
merchantId: string,
|
|
46
|
+
name: string,
|
|
47
|
+
clientId: string,
|
|
48
|
+
css?: string
|
|
49
|
+
): string {
|
|
50
|
+
// Build full params with app-specific metadata
|
|
51
|
+
const fullParams: FullSsoParams = {
|
|
52
|
+
redirectUrl: params.redirectUrl,
|
|
53
|
+
directExit: params.directExit,
|
|
54
|
+
lang: params.lang,
|
|
55
|
+
merchantId,
|
|
56
|
+
metadata: {
|
|
57
|
+
name,
|
|
58
|
+
css,
|
|
59
|
+
logoUrl: params.metadata?.logoUrl,
|
|
60
|
+
homepageLink: params.metadata?.homepageLink,
|
|
61
|
+
},
|
|
62
|
+
clientId,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Compress params to minimal format
|
|
66
|
+
const compressedParam = ssoParamsToCompressed(fullParams);
|
|
67
|
+
|
|
68
|
+
// Encode to base64url
|
|
69
|
+
const compressedString = compressJsonToB64(compressedParam);
|
|
70
|
+
|
|
71
|
+
// Build URL matching wallet's expected format: /sso?p=<compressed>
|
|
72
|
+
const ssoUrl = new URL(walletUrl);
|
|
73
|
+
ssoUrl.pathname = "/sso";
|
|
74
|
+
ssoUrl.searchParams.set("p", compressedString);
|
|
75
|
+
|
|
76
|
+
return ssoUrl.toString();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Map full sso params to compressed sso params
|
|
81
|
+
* @param params
|
|
82
|
+
*/
|
|
83
|
+
function ssoParamsToCompressed(params: FullSsoParams): CompressedSsoData {
|
|
84
|
+
return {
|
|
85
|
+
r: params.redirectUrl,
|
|
86
|
+
cId: params.clientId,
|
|
87
|
+
d: params.directExit,
|
|
88
|
+
l: params.lang,
|
|
89
|
+
m: params.merchantId,
|
|
90
|
+
md: {
|
|
91
|
+
n: params.metadata?.name,
|
|
92
|
+
css: params.metadata?.css,
|
|
93
|
+
l: params.metadata?.logoUrl,
|
|
94
|
+
h: params.metadata?.homepageLink,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Type of compressed the sso data
|
|
101
|
+
*/
|
|
102
|
+
export type CompressedSsoData = {
|
|
103
|
+
// Potential id from backend
|
|
104
|
+
id?: Hex;
|
|
105
|
+
// Client id
|
|
106
|
+
cId: string;
|
|
107
|
+
// redirect url
|
|
108
|
+
r?: string;
|
|
109
|
+
// direct exit
|
|
110
|
+
d?: boolean;
|
|
111
|
+
// language
|
|
112
|
+
l?: "en" | "fr";
|
|
113
|
+
// merchant id
|
|
114
|
+
m: string;
|
|
115
|
+
// metadata
|
|
116
|
+
md: {
|
|
117
|
+
// merchant name
|
|
118
|
+
n: string;
|
|
119
|
+
// custom css
|
|
120
|
+
css?: string;
|
|
121
|
+
// logo
|
|
122
|
+
l?: string;
|
|
123
|
+
// home page link
|
|
124
|
+
h?: string;
|
|
125
|
+
};
|
|
126
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { RpcClient } from "@frak-labs/frame-connector";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
} from "../../tests/vitest-fixtures";
|
|
10
|
+
import type { FrakLifecycleEvent } from "../types";
|
|
11
|
+
import type { IFrameRpcSchema } from "../types/rpc";
|
|
12
|
+
import { setupSsoUrlListener } from "./ssoUrlListener";
|
|
13
|
+
|
|
14
|
+
describe("setupSsoUrlListener", () => {
|
|
15
|
+
let mockRpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;
|
|
16
|
+
let mockWaitForConnection: Promise<boolean>;
|
|
17
|
+
let originalLocation: Location;
|
|
18
|
+
let originalHistory: History;
|
|
19
|
+
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
|
|
20
|
+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
// Save original values
|
|
24
|
+
originalLocation = window.location;
|
|
25
|
+
originalHistory = window.history;
|
|
26
|
+
|
|
27
|
+
// Mock RPC client
|
|
28
|
+
mockRpcClient = {
|
|
29
|
+
sendLifecycle: vi.fn(),
|
|
30
|
+
} as any;
|
|
31
|
+
|
|
32
|
+
// Mock console methods
|
|
33
|
+
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
34
|
+
consoleErrorSpy = vi
|
|
35
|
+
.spyOn(console, "error")
|
|
36
|
+
.mockImplementation(() => {});
|
|
37
|
+
|
|
38
|
+
// Mock history.replaceState
|
|
39
|
+
window.history.replaceState = vi.fn();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
consoleLogSpy.mockRestore();
|
|
45
|
+
consoleErrorSpy.mockRestore();
|
|
46
|
+
|
|
47
|
+
// Restore original values
|
|
48
|
+
Object.defineProperty(window, "location", {
|
|
49
|
+
value: originalLocation,
|
|
50
|
+
writable: true,
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(window, "history", {
|
|
53
|
+
value: originalHistory,
|
|
54
|
+
writable: true,
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should do nothing when window is undefined", () => {
|
|
59
|
+
const originalWindow = global.window;
|
|
60
|
+
// @ts-expect-error - intentionally removing window for test
|
|
61
|
+
delete global.window;
|
|
62
|
+
|
|
63
|
+
setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
|
|
64
|
+
|
|
65
|
+
expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
|
|
66
|
+
|
|
67
|
+
// Restore window
|
|
68
|
+
global.window = originalWindow;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should do nothing when no SSO parameter in URL", () => {
|
|
72
|
+
Object.defineProperty(window, "location", {
|
|
73
|
+
value: {
|
|
74
|
+
href: "https://example.com/test",
|
|
75
|
+
},
|
|
76
|
+
writable: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
|
|
80
|
+
|
|
81
|
+
expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
|
|
82
|
+
expect(window.history.replaceState).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should forward SSO data to iframe when connection is ready", async () => {
|
|
86
|
+
const compressedSso = "compressed-sso-data";
|
|
87
|
+
Object.defineProperty(window, "location", {
|
|
88
|
+
value: {
|
|
89
|
+
href: `https://example.com/test?sso=${compressedSso}`,
|
|
90
|
+
},
|
|
91
|
+
writable: true,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
mockWaitForConnection = Promise.resolve(true);
|
|
95
|
+
|
|
96
|
+
setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
|
|
97
|
+
|
|
98
|
+
await mockWaitForConnection;
|
|
99
|
+
|
|
100
|
+
// Wait for promise to resolve
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
102
|
+
|
|
103
|
+
expect(mockRpcClient.sendLifecycle).toHaveBeenCalledWith({
|
|
104
|
+
clientLifecycle: "sso-redirect-complete",
|
|
105
|
+
data: { compressed: compressedSso },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
109
|
+
"[SSO URL Listener] Forwarded compressed SSO data to iframe"
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("should clean URL immediately after detecting SSO parameter", async () => {
|
|
114
|
+
const compressedSso = "compressed-sso-data";
|
|
115
|
+
const originalUrl = `https://example.com/test?sso=${compressedSso}`;
|
|
116
|
+
Object.defineProperty(window, "location", {
|
|
117
|
+
value: {
|
|
118
|
+
href: originalUrl,
|
|
119
|
+
},
|
|
120
|
+
writable: true,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
mockWaitForConnection = Promise.resolve(true);
|
|
124
|
+
|
|
125
|
+
setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
|
|
126
|
+
|
|
127
|
+
// URL should be cleaned immediately, before connection resolves
|
|
128
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
129
|
+
{},
|
|
130
|
+
"",
|
|
131
|
+
"https://example.com/test"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
135
|
+
"[SSO URL Listener] SSO parameter detected and URL cleaned"
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should handle connection promise rejection", async () => {
|
|
140
|
+
const compressedSso = "compressed-sso-data";
|
|
141
|
+
Object.defineProperty(window, "location", {
|
|
142
|
+
value: {
|
|
143
|
+
href: `https://example.com/test?sso=${compressedSso}`,
|
|
144
|
+
},
|
|
145
|
+
writable: true,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const connectionError = new Error("Connection failed");
|
|
149
|
+
mockWaitForConnection = Promise.reject(connectionError);
|
|
150
|
+
|
|
151
|
+
setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
|
|
152
|
+
|
|
153
|
+
await mockWaitForConnection.catch(() => {});
|
|
154
|
+
|
|
155
|
+
// Wait for promise to reject
|
|
156
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
157
|
+
|
|
158
|
+
expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
|
|
159
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
160
|
+
"[SSO URL Listener] Failed to forward SSO data:",
|
|
161
|
+
connectionError
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("should handle URL with multiple parameters", async () => {
|
|
166
|
+
const compressedSso = "compressed-sso-data";
|
|
167
|
+
Object.defineProperty(window, "location", {
|
|
168
|
+
value: {
|
|
169
|
+
href: `https://example.com/test?param1=value1&sso=${compressedSso}¶m2=value2`,
|
|
170
|
+
},
|
|
171
|
+
writable: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
mockWaitForConnection = Promise.resolve(true);
|
|
175
|
+
|
|
176
|
+
setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
|
|
177
|
+
|
|
178
|
+
await mockWaitForConnection;
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
180
|
+
|
|
181
|
+
expect(mockRpcClient.sendLifecycle).toHaveBeenCalledWith({
|
|
182
|
+
clientLifecycle: "sso-redirect-complete",
|
|
183
|
+
data: { compressed: compressedSso },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// Should remove only the sso parameter
|
|
187
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
188
|
+
{},
|
|
189
|
+
"",
|
|
190
|
+
"https://example.com/test?param1=value1¶m2=value2"
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should not process empty SSO parameter", () => {
|
|
195
|
+
Object.defineProperty(window, "location", {
|
|
196
|
+
value: {
|
|
197
|
+
href: "https://example.com/test?sso=",
|
|
198
|
+
},
|
|
199
|
+
writable: true,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
|
|
203
|
+
|
|
204
|
+
// Empty string is falsy, so it should not process
|
|
205
|
+
expect(window.history.replaceState).not.toHaveBeenCalled();
|
|
206
|
+
expect(mockRpcClient.sendLifecycle).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("should clean URL even if connection fails", async () => {
|
|
210
|
+
const compressedSso = "compressed-sso-data";
|
|
211
|
+
Object.defineProperty(window, "location", {
|
|
212
|
+
value: {
|
|
213
|
+
href: `https://example.com/test?sso=${compressedSso}`,
|
|
214
|
+
},
|
|
215
|
+
writable: true,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const connectionError = new Error("Connection failed");
|
|
219
|
+
mockWaitForConnection = Promise.reject(connectionError);
|
|
220
|
+
|
|
221
|
+
setupSsoUrlListener(mockRpcClient, mockWaitForConnection);
|
|
222
|
+
|
|
223
|
+
// URL should still be cleaned even if connection fails
|
|
224
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
225
|
+
{},
|
|
226
|
+
"",
|
|
227
|
+
"https://example.com/test"
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
await mockWaitForConnection.catch(() => {});
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("should handle URL with hash", () => {
|
|
235
|
+
const compressedSso = "compressed-sso-data";
|
|
236
|
+
Object.defineProperty(window, "location", {
|
|
237
|
+
value: {
|
|
238
|
+
href: `https://example.com/test?sso=${compressedSso}#section`,
|
|
239
|
+
},
|
|
240
|
+
writable: true,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
setupSsoUrlListener(mockRpcClient, Promise.resolve(true));
|
|
244
|
+
|
|
245
|
+
// Should preserve hash when cleaning URL
|
|
246
|
+
expect(window.history.replaceState).toHaveBeenCalledWith(
|
|
247
|
+
{},
|
|
248
|
+
"",
|
|
249
|
+
"https://example.com/test#section"
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { RpcClient } from "@frak-labs/frame-connector";
|
|
2
|
+
import type { FrakLifecycleEvent } from "../types";
|
|
3
|
+
import type { IFrameRpcSchema } from "../types/rpc";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Listen for SSO redirect with compressed data in URL
|
|
7
|
+
* Forwards compressed data to iframe via lifecycle event
|
|
8
|
+
* Cleans URL immediately after detection
|
|
9
|
+
*
|
|
10
|
+
* Performance: One-shot URL check, no polling, no re-renders
|
|
11
|
+
*
|
|
12
|
+
* @param rpcClient - RPC client instance to send lifecycle events
|
|
13
|
+
* @param waitForConnection - Promise that resolves when iframe is connected
|
|
14
|
+
*/
|
|
15
|
+
export function setupSsoUrlListener(
|
|
16
|
+
rpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>,
|
|
17
|
+
waitForConnection: Promise<boolean>
|
|
18
|
+
): void {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// One-shot URL check - no need for MutationObserver or polling
|
|
24
|
+
const url = new URL(window.location.href);
|
|
25
|
+
const compressedSso = url.searchParams.get("sso");
|
|
26
|
+
|
|
27
|
+
// Early return if no SSO parameter
|
|
28
|
+
if (!compressedSso) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Forward compressed data directly to iframe (no decompression on SDK side)
|
|
33
|
+
// Iframe will decompress and process
|
|
34
|
+
waitForConnection
|
|
35
|
+
.then(() => {
|
|
36
|
+
// Send lifecycle event with compressed string
|
|
37
|
+
// This is a one-way notification, no response expected
|
|
38
|
+
rpcClient.sendLifecycle({
|
|
39
|
+
clientLifecycle: "sso-redirect-complete",
|
|
40
|
+
data: { compressed: compressedSso },
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(
|
|
44
|
+
"[SSO URL Listener] Forwarded compressed SSO data to iframe"
|
|
45
|
+
);
|
|
46
|
+
})
|
|
47
|
+
.catch((error) => {
|
|
48
|
+
console.error(
|
|
49
|
+
"[SSO URL Listener] Failed to forward SSO data:",
|
|
50
|
+
error
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Clean URL immediately to prevent exposure in browser history
|
|
55
|
+
// Use replaceState to avoid navigation/re-render
|
|
56
|
+
url.searchParams.delete("sso");
|
|
57
|
+
window.history.replaceState({}, "", url.toString());
|
|
58
|
+
|
|
59
|
+
console.log("[SSO URL Listener] SSO parameter detected and URL cleaned");
|
|
60
|
+
}
|