@frak-labs/core-sdk 0.1.0 → 0.1.1-beta.2c88e0f4

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.
Files changed (130) hide show
  1. package/README.md +58 -0
  2. package/cdn/bundle.js +3 -8
  3. package/dist/actions.cjs +1 -1
  4. package/dist/actions.d.cts +3 -1400
  5. package/dist/actions.d.ts +3 -1400
  6. package/dist/actions.js +1 -1
  7. package/dist/bundle.cjs +1 -13
  8. package/dist/bundle.d.cts +4 -1927
  9. package/dist/bundle.d.ts +4 -1927
  10. package/dist/bundle.js +1 -13
  11. package/dist/computeLegacyProductId-B-2-L7VT.d.cts +538 -0
  12. package/dist/computeLegacyProductId-M9k7vnUf.d.ts +538 -0
  13. package/dist/index.cjs +1 -13
  14. package/dist/index.d.cts +3 -1269
  15. package/dist/index.d.ts +3 -1269
  16. package/dist/index.js +1 -13
  17. package/dist/openSso-DTFns_5E.d.cts +1022 -0
  18. package/dist/openSso-DXL8K2tc.d.ts +1022 -0
  19. package/dist/setupClient--zfx2pFM.cjs +13 -0
  20. package/dist/setupClient-CFQDaGKh.js +13 -0
  21. package/dist/siweAuthenticate-BxRXLOpn.d.ts +511 -0
  22. package/dist/siweAuthenticate-D_AfTjZY.d.cts +511 -0
  23. package/dist/siweAuthenticate-Dfu_kubw.js +1 -0
  24. package/dist/siweAuthenticate-o0RXILdG.cjs +1 -0
  25. package/dist/trackEvent-B0S5E6_h.js +1 -0
  26. package/dist/trackEvent-BbX2d3g_.cjs +1 -0
  27. package/package.json +25 -31
  28. package/src/actions/displayEmbeddedWallet.test.ts +194 -0
  29. package/src/actions/displayEmbeddedWallet.ts +21 -0
  30. package/src/actions/displayModal.test.ts +388 -0
  31. package/src/actions/displayModal.ts +120 -0
  32. package/src/actions/getMerchantInformation.test.ts +116 -0
  33. package/src/actions/getMerchantInformation.ts +16 -0
  34. package/src/actions/index.ts +29 -0
  35. package/src/actions/openSso.ts +116 -0
  36. package/src/actions/prepareSso.test.ts +223 -0
  37. package/src/actions/prepareSso.ts +48 -0
  38. package/src/actions/referral/processReferral.test.ts +248 -0
  39. package/src/actions/referral/processReferral.ts +232 -0
  40. package/src/actions/referral/referralInteraction.test.ts +147 -0
  41. package/src/actions/referral/referralInteraction.ts +52 -0
  42. package/src/actions/sendInteraction.ts +56 -0
  43. package/src/actions/trackPurchaseStatus.test.ts +500 -0
  44. package/src/actions/trackPurchaseStatus.ts +90 -0
  45. package/src/actions/watchWalletStatus.test.ts +372 -0
  46. package/src/actions/watchWalletStatus.ts +93 -0
  47. package/src/actions/wrapper/modalBuilder.test.ts +239 -0
  48. package/src/actions/wrapper/modalBuilder.ts +203 -0
  49. package/src/actions/wrapper/sendTransaction.test.ts +164 -0
  50. package/src/actions/wrapper/sendTransaction.ts +62 -0
  51. package/src/actions/wrapper/siweAuthenticate.test.ts +290 -0
  52. package/src/actions/wrapper/siweAuthenticate.ts +94 -0
  53. package/src/bundle.ts +2 -0
  54. package/src/clients/DebugInfo.test.ts +418 -0
  55. package/src/clients/DebugInfo.ts +182 -0
  56. package/src/clients/createIFrameFrakClient.ts +290 -0
  57. package/src/clients/index.ts +3 -0
  58. package/src/clients/setupClient.test.ts +343 -0
  59. package/src/clients/setupClient.ts +73 -0
  60. package/src/clients/transports/iframeLifecycleManager.test.ts +558 -0
  61. package/src/clients/transports/iframeLifecycleManager.ts +229 -0
  62. package/src/constants/interactionTypes.ts +15 -0
  63. package/src/constants/locales.ts +14 -0
  64. package/src/index.ts +109 -0
  65. package/src/types/client.ts +14 -0
  66. package/src/types/compression.ts +22 -0
  67. package/src/types/config.ts +117 -0
  68. package/src/types/context.ts +13 -0
  69. package/src/types/index.ts +74 -0
  70. package/src/types/lifecycle/client.ts +69 -0
  71. package/src/types/lifecycle/iframe.ts +41 -0
  72. package/src/types/lifecycle/index.ts +2 -0
  73. package/src/types/rpc/displayModal.ts +82 -0
  74. package/src/types/rpc/embedded/index.ts +68 -0
  75. package/src/types/rpc/embedded/loggedIn.ts +55 -0
  76. package/src/types/rpc/embedded/loggedOut.ts +28 -0
  77. package/src/types/rpc/interaction.ts +30 -0
  78. package/src/types/rpc/merchantInformation.ts +77 -0
  79. package/src/types/rpc/modal/final.ts +46 -0
  80. package/src/types/rpc/modal/generic.ts +46 -0
  81. package/src/types/rpc/modal/index.ts +16 -0
  82. package/src/types/rpc/modal/login.ts +36 -0
  83. package/src/types/rpc/modal/siweAuthenticate.ts +37 -0
  84. package/src/types/rpc/modal/transaction.ts +33 -0
  85. package/src/types/rpc/sso.ts +80 -0
  86. package/src/types/rpc/walletStatus.ts +29 -0
  87. package/src/types/rpc.ts +150 -0
  88. package/src/types/tracking.ts +60 -0
  89. package/src/types/transport.ts +34 -0
  90. package/src/utils/FrakContext.test.ts +407 -0
  91. package/src/utils/FrakContext.ts +158 -0
  92. package/src/utils/backendUrl.test.ts +83 -0
  93. package/src/utils/backendUrl.ts +62 -0
  94. package/src/utils/clientId.test.ts +41 -0
  95. package/src/utils/clientId.ts +40 -0
  96. package/src/utils/compression/b64.test.ts +181 -0
  97. package/src/utils/compression/b64.ts +29 -0
  98. package/src/utils/compression/compress.test.ts +123 -0
  99. package/src/utils/compression/compress.ts +11 -0
  100. package/src/utils/compression/decompress.test.ts +149 -0
  101. package/src/utils/compression/decompress.ts +11 -0
  102. package/src/utils/compression/index.ts +3 -0
  103. package/src/utils/computeLegacyProductId.ts +11 -0
  104. package/src/utils/constants.test.ts +23 -0
  105. package/src/utils/constants.ts +9 -0
  106. package/src/utils/deepLinkWithFallback.test.ts +243 -0
  107. package/src/utils/deepLinkWithFallback.ts +103 -0
  108. package/src/utils/formatAmount.test.ts +113 -0
  109. package/src/utils/formatAmount.ts +24 -0
  110. package/src/utils/getCurrencyAmountKey.test.ts +44 -0
  111. package/src/utils/getCurrencyAmountKey.ts +15 -0
  112. package/src/utils/getSupportedCurrency.test.ts +51 -0
  113. package/src/utils/getSupportedCurrency.ts +14 -0
  114. package/src/utils/getSupportedLocale.test.ts +64 -0
  115. package/src/utils/getSupportedLocale.ts +16 -0
  116. package/src/utils/iframeHelper.test.ts +450 -0
  117. package/src/utils/iframeHelper.ts +147 -0
  118. package/src/utils/index.ts +36 -0
  119. package/src/utils/merchantId.test.ts +653 -0
  120. package/src/utils/merchantId.ts +143 -0
  121. package/src/utils/sso.ts +127 -0
  122. package/src/utils/ssoUrlListener.test.ts +252 -0
  123. package/src/utils/ssoUrlListener.ts +60 -0
  124. package/src/utils/trackEvent.test.ts +180 -0
  125. package/src/utils/trackEvent.ts +41 -0
  126. package/cdn/bundle.js.LICENSE.txt +0 -10
  127. package/dist/interactions.cjs +0 -1
  128. package/dist/interactions.d.cts +0 -182
  129. package/dist/interactions.d.ts +0 -182
  130. 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
+ }
@@ -0,0 +1,127 @@
1
+ import type { Hex } from "viem";
2
+ import type { PrepareSsoParamsType, SsoMetadata } from "../types";
3
+ import { getClientId } from "./clientId";
4
+ import { compressJsonToB64 } from "./compression/compress";
5
+
6
+ export type AppSpecificSsoMetadata = SsoMetadata & {
7
+ name: string;
8
+ css?: string;
9
+ };
10
+
11
+ /**
12
+ * The full SSO params that will be used for compression
13
+ */
14
+ export type FullSsoParams = Omit<PrepareSsoParamsType, "metadata"> & {
15
+ metadata: AppSpecificSsoMetadata;
16
+ merchantId: string;
17
+ clientId: string;
18
+ };
19
+
20
+ /**
21
+ * Generate SSO URL with compressed parameters
22
+ * This mirrors the wallet's getOpenSsoLink() function
23
+ *
24
+ * @param walletUrl - Base wallet URL (e.g., "https://wallet.frak.id")
25
+ * @param params - SSO parameters
26
+ * @param merchantId - Merchant identifier
27
+ * @param name - Application name
28
+ * @param css - Optional custom CSS
29
+ * @param clientId - Optional client identifier (auto-generated if omitted)
30
+ * @returns Complete SSO URL ready to open in popup or redirect
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * const ssoUrl = generateSsoUrl(
35
+ * "https://wallet.frak.id",
36
+ * { metadata: { logoUrl: "..." }, directExit: true },
37
+ * "0x123...",
38
+ * "My App"
39
+ * );
40
+ * // Returns: https://wallet.frak.id/sso?p=<compressed_base64>
41
+ * ```
42
+ */
43
+ export function generateSsoUrl(
44
+ walletUrl: string,
45
+ params: PrepareSsoParamsType,
46
+ merchantId: string,
47
+ name: string,
48
+ css?: string,
49
+ clientId?: string
50
+ ): string {
51
+ // Build full params with app-specific metadata
52
+ const fullParams: FullSsoParams = {
53
+ redirectUrl: params.redirectUrl,
54
+ directExit: params.directExit,
55
+ lang: params.lang,
56
+ merchantId,
57
+ metadata: {
58
+ name,
59
+ css,
60
+ logoUrl: params.metadata?.logoUrl,
61
+ homepageLink: params.metadata?.homepageLink,
62
+ },
63
+ clientId: clientId ?? getClientId(),
64
+ };
65
+
66
+ // Compress params to minimal format
67
+ const compressedParam = ssoParamsToCompressed(fullParams);
68
+
69
+ // Encode to base64url
70
+ const compressedString = compressJsonToB64(compressedParam);
71
+
72
+ // Build URL matching wallet's expected format: /sso?p=<compressed>
73
+ const ssoUrl = new URL(walletUrl);
74
+ ssoUrl.pathname = "/sso";
75
+ ssoUrl.searchParams.set("p", compressedString);
76
+
77
+ return ssoUrl.toString();
78
+ }
79
+
80
+ /**
81
+ * Map full sso params to compressed sso params
82
+ * @param params
83
+ */
84
+ function ssoParamsToCompressed(params: FullSsoParams): CompressedSsoData {
85
+ return {
86
+ r: params.redirectUrl,
87
+ cId: params.clientId,
88
+ d: params.directExit,
89
+ l: params.lang,
90
+ m: params.merchantId,
91
+ md: {
92
+ n: params.metadata?.name,
93
+ css: params.metadata?.css,
94
+ l: params.metadata?.logoUrl,
95
+ h: params.metadata?.homepageLink,
96
+ },
97
+ };
98
+ }
99
+
100
+ /**
101
+ * Type of compressed the sso data
102
+ */
103
+ export type CompressedSsoData = {
104
+ // Potential id from backend
105
+ id?: Hex;
106
+ // Client id
107
+ cId: string;
108
+ // redirect url
109
+ r?: string;
110
+ // direct exit
111
+ d?: boolean;
112
+ // language
113
+ l?: "en" | "fr";
114
+ // merchant id
115
+ m: string;
116
+ // metadata
117
+ md: {
118
+ // merchant name
119
+ n: string;
120
+ // custom css
121
+ css?: string;
122
+ // logo
123
+ l?: string;
124
+ // home page link
125
+ h?: string;
126
+ };
127
+ };
@@ -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}&param2=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&param2=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
+ }