@frak-labs/core-sdk 0.1.0-beta.b0bd1f8a → 0.1.0-beta.b77c23c7

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.
@@ -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
+ });
@@ -4,7 +4,9 @@ export type FrakEvent =
4
4
  | "share_button_clicked"
5
5
  | "wallet_button_clicked"
6
6
  | "share_modal_error"
7
- | "user_referred";
7
+ | "user_referred_started"
8
+ | "user_referred_completed"
9
+ | "user_referred_error";
8
10
 
9
11
  type EventProps = Record<string, unknown>;
10
12
 
@@ -1 +0,0 @@
1
- import{n as e,t,u as n}from"./trackEvent-CHnYa85W.js";import{r}from"./interactions-DnfM3oe0.js";import{isAddressEqual as i}from"viem";import{Deferred as a,FrakRpcError as o,RpcErrorCodes as s}from"@frak-labs/frame-connector";import{generateSiweNonce as c}from"viem/siwe";async function l(e,t){return await e.request({method:`frak_displayEmbeddedWallet`,params:[t,e.config.metadata]})}async function u(e,{steps:t,metadata:n}){return await e.request({method:`frak_displayModal`,params:[t,n,e.config.metadata]})}async function d(e){return await e.request({method:`frak_getProductInformation`})}async function f(e,t){let{metadata:n,customizations:r}=e.config;return await e.request({method:`frak_prepareSso`,params:[t,n.name,r?.css]})}async function p(n,{walletStatus:i,frakContext:a,modalConfig:o,productId:s,options:c}){let l=!1;async function u(){if(!l)return l=!0,h(n,{modalConfig:{...o,loggedIn:{action:{key:`referred`}}},walletStatus:i})}async function d(e){let i=r.referred({referrer:e});await Promise.allSettled([v(n,{productId:s,interaction:i}),t(n,`user_referred`,{properties:{referrer:e}})])}try{let{status:t,currentWallet:n}=await m({initialWalletStatus:i,getFreshWalletStatus:u,pushReferralInteraction:d,frakContext:a});return e.replaceUrl({url:window.location?.href,context:c?.alwaysAppendUrl?{r:n}:null}),t}catch(t){return console.log(`Error processing referral`,{error:t}),e.replaceUrl({url:window.location?.href,context:c?.alwaysAppendUrl?{r:i?.wallet}:null}),g(t)}}async function m({initialWalletStatus:e,getFreshWalletStatus:t,pushReferralInteraction:n,frakContext:r}){let a=e?.wallet;return r?.r?(a||=await t(),a&&i(r.r,a)?{status:`self-referral`,currentWallet:a}:(e?.interactionSession||(a=await t()),await n(r.r),{status:`success`,currentWallet:a})):{status:`no-referrer`,currentWallet:a}}async function h(e,{modalConfig:t,walletStatus:n}){return n?.interactionSession?n.wallet??void 0:(await l(e,t??{}))?.wallet??void 0}function g(e){if(e instanceof o)switch(e.code){case s.walletNotConnected:return`no-wallet`;case s.serverErrorForInteractionDelegation:return`no-session`;default:return`error`}return`error`}async function _(t,{productId:n,modalConfig:r,options:i}={}){let a=e.parse({url:window.location.href}),o=await b(t);try{return await p(t,{walletStatus:o,frakContext:a,modalConfig:r,productId:n,options:i})}catch(e){console.warn(`Error processing referral`,{error:e})}}async function v(e,{productId:t,interaction:r,validation:i}){let a=t??n(e.config);return await e.request({method:`frak_sendInteraction`,params:[a,r,i]})}async function y(e){if(typeof window>`u`){console.warn(`[Frak] No window found, can't track purchase`);return}let t=window.sessionStorage.getItem(`frak-wallet-interaction-token`);if(!t){console.warn(`[Frak] No frak session found, skipping purchase check`);return}await fetch(`https://backend.frak.id/interactions/listenForPurchase`,{method:`POST`,headers:{Accept:`application/json`,"Content-Type":`application/json`,"x-wallet-sdk-auth":t},body:JSON.stringify(e)})}function b(e,t){if(!t)return e.request({method:`frak_listenToWalletStatus`}).then(t=>(x(e,t),t));let n=new a,r=!1;return e.listenerRequest({method:`frak_listenToWalletStatus`},i=>{x(e,i),t(i),r||=(n.resolve(i),!0)}),n.promise}function x(e,t){typeof window>`u`||(e.openPanel?.setGlobalProperties({wallet:t.wallet??null}),t.interactionToken?window.sessionStorage.setItem(`frak-wallet-interaction-token`,t.interactionToken):window.sessionStorage.removeItem(`frak-wallet-interaction-token`))}function S(e,{metadata:t,login:n,openSession:r}){return C(e,{steps:{login:n??{},openSession:r??{}},metadata:t})}function C(e,t){function n(n){return C(e,{...t,steps:{...t.steps,sendTransaction:n}})}function r(n){return C(e,{...t,steps:{...t.steps,final:{...n,action:{key:`reward`}}}})}function i(n,r){return C(e,{...t,steps:{...t.steps,final:{...r,action:{key:`sharing`,options:n}}}})}async function a(n){return n&&(t.metadata=n(t.metadata??{})),await u(e,t)}return{params:t,sendTx:n,reward:r,sharing:i,display:a}}async function w(e,{tx:t,metadata:n}){return(await u(e,{metadata:n,steps:{login:{},sendTransaction:{tx:t}}})).sendTransaction}async function T(e,{siwe:t,metadata:n}){let r=e.config?.domain??window.location.host,i=t?.statement??`I confirm that I want to use my Frak wallet on: ${e.config.metadata.name}`;return(await u(e,{metadata:n,steps:{login:{},siweAuthenticate:{siwe:{...t,statement:i,nonce:t?.nonce??c(),uri:t?.uri??`https://${r}`,version:t?.version??`1`,domain:r}}}})).siweAuthenticate}export{y as a,p as c,u as d,l as f,b as i,f as l,w as n,v as o,S as r,_ as s,T as t,d as u};
@@ -1 +0,0 @@
1
- const e=require(`./trackEvent-GuQm_1Nm.cjs`),t=require(`./interactions-EIXhNLf6.cjs`);let n=require(`viem`),r=require(`@frak-labs/frame-connector`),i=require(`viem/siwe`);async function a(e,t){return await e.request({method:`frak_displayEmbeddedWallet`,params:[t,e.config.metadata]})}async function o(e,{steps:t,metadata:n}){return await e.request({method:`frak_displayModal`,params:[t,n,e.config.metadata]})}async function s(e){return await e.request({method:`frak_getProductInformation`})}async function c(e,t){let{metadata:n,customizations:r}=e.config;return await e.request({method:`frak_prepareSso`,params:[t,n.name,r?.css]})}async function l(n,{walletStatus:r,frakContext:i,modalConfig:a,productId:o,options:s}){let c=!1;async function l(){if(!c)return c=!0,d(n,{modalConfig:{...a,loggedIn:{action:{key:`referred`}}},walletStatus:r})}async function p(r){let i=t.r.referred({referrer:r});await Promise.allSettled([m(n,{productId:o,interaction:i}),e.t(n,`user_referred`,{properties:{referrer:r}})])}try{let{status:t,currentWallet:n}=await u({initialWalletStatus:r,getFreshWalletStatus:l,pushReferralInteraction:p,frakContext:i});return e.n.replaceUrl({url:window.location?.href,context:s?.alwaysAppendUrl?{r:n}:null}),t}catch(t){return console.log(`Error processing referral`,{error:t}),e.n.replaceUrl({url:window.location?.href,context:s?.alwaysAppendUrl?{r:r?.wallet}:null}),f(t)}}async function u({initialWalletStatus:e,getFreshWalletStatus:t,pushReferralInteraction:r,frakContext:i}){let a=e?.wallet;return i?.r?(a||=await t(),a&&(0,n.isAddressEqual)(i.r,a)?{status:`self-referral`,currentWallet:a}:(e?.interactionSession||(a=await t()),await r(i.r),{status:`success`,currentWallet:a})):{status:`no-referrer`,currentWallet:a}}async function d(e,{modalConfig:t,walletStatus:n}){return n?.interactionSession?n.wallet??void 0:(await a(e,t??{}))?.wallet??void 0}function f(e){if(e instanceof r.FrakRpcError)switch(e.code){case r.RpcErrorCodes.walletNotConnected:return`no-wallet`;case r.RpcErrorCodes.serverErrorForInteractionDelegation:return`no-session`;default:return`error`}return`error`}async function p(t,{productId:n,modalConfig:r,options:i}={}){let a=e.n.parse({url:window.location.href}),o=await g(t);try{return await l(t,{walletStatus:o,frakContext:a,modalConfig:r,productId:n,options:i})}catch(e){console.warn(`Error processing referral`,{error:e})}}async function m(t,{productId:n,interaction:r,validation:i}){let a=n??e.u(t.config);return await t.request({method:`frak_sendInteraction`,params:[a,r,i]})}async function h(e){if(typeof window>`u`){console.warn(`[Frak] No window found, can't track purchase`);return}let t=window.sessionStorage.getItem(`frak-wallet-interaction-token`);if(!t){console.warn(`[Frak] No frak session found, skipping purchase check`);return}await fetch(`https://backend.frak.id/interactions/listenForPurchase`,{method:`POST`,headers:{Accept:`application/json`,"Content-Type":`application/json`,"x-wallet-sdk-auth":t},body:JSON.stringify(e)})}function g(e,t){if(!t)return e.request({method:`frak_listenToWalletStatus`}).then(t=>(_(e,t),t));let n=new r.Deferred,i=!1;return e.listenerRequest({method:`frak_listenToWalletStatus`},r=>{_(e,r),t(r),i||=(n.resolve(r),!0)}),n.promise}function _(e,t){typeof window>`u`||(e.openPanel?.setGlobalProperties({wallet:t.wallet??null}),t.interactionToken?window.sessionStorage.setItem(`frak-wallet-interaction-token`,t.interactionToken):window.sessionStorage.removeItem(`frak-wallet-interaction-token`))}function v(e,{metadata:t,login:n,openSession:r}){return y(e,{steps:{login:n??{},openSession:r??{}},metadata:t})}function y(e,t){function n(n){return y(e,{...t,steps:{...t.steps,sendTransaction:n}})}function r(n){return y(e,{...t,steps:{...t.steps,final:{...n,action:{key:`reward`}}}})}function i(n,r){return y(e,{...t,steps:{...t.steps,final:{...r,action:{key:`sharing`,options:n}}}})}async function a(n){return n&&(t.metadata=n(t.metadata??{})),await o(e,t)}return{params:t,sendTx:n,reward:r,sharing:i,display:a}}async function b(e,{tx:t,metadata:n}){return(await o(e,{metadata:n,steps:{login:{},sendTransaction:{tx:t}}})).sendTransaction}async function x(e,{siwe:t,metadata:n}){let r=e.config?.domain??window.location.host,a=t?.statement??`I confirm that I want to use my Frak wallet on: ${e.config.metadata.name}`;return(await o(e,{metadata:n,steps:{login:{},siweAuthenticate:{siwe:{...t,statement:a,nonce:t?.nonce??(0,i.generateSiweNonce)(),uri:t?.uri??`https://${r}`,version:t?.version??`1`,domain:r}}}})).siweAuthenticate}Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return o}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return g}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return b}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return v}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return p}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return x}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return s}});