@fivenorth/loop-sdk 0.7.1 → 0.7.3

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 (2) hide show
  1. package/dist/index.js +281 -148
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2067,6 +2067,43 @@ var MessageType;
2067
2067
  MessageType2["REJECT_REQUEST"] = "reject_request";
2068
2068
  })(MessageType ||= {});
2069
2069
 
2070
+ // src/errors.ts
2071
+ class RequestTimeoutError extends Error {
2072
+ constructor(timeout) {
2073
+ super(`Request timed out after ${timeout}ms.`);
2074
+ }
2075
+ }
2076
+
2077
+ class RejectRequestError extends Error {
2078
+ constructor() {
2079
+ super("Request was rejected by the wallet.");
2080
+ }
2081
+ }
2082
+
2083
+ class UnauthorizedError extends Error {
2084
+ code;
2085
+ constructor(code) {
2086
+ super(code || "Unauthorized");
2087
+ this.code = code;
2088
+ }
2089
+ }
2090
+ var UNAUTH_CODES = new Set(["UNAUTHENTICATED", "UNAUTHORIZED", "SESSION_EXPIRED", "LOGGED_OUT"]);
2091
+ function extractErrorCode(message) {
2092
+ if (typeof message?.error?.code === "string" && message.error.code.length > 0) {
2093
+ return message.error.code;
2094
+ }
2095
+ if (message?.type === "unauthorized" && typeof message?.code === "string") {
2096
+ return message.code;
2097
+ }
2098
+ return null;
2099
+ }
2100
+ function isUnauthCode(code) {
2101
+ if (!code) {
2102
+ return false;
2103
+ }
2104
+ return UNAUTH_CODES.has(code);
2105
+ }
2106
+
2070
2107
  // src/connection.ts
2071
2108
  class Connection {
2072
2109
  walletUrl = "https://cantonloop.com";
@@ -2076,6 +2113,7 @@ class Connection {
2076
2113
  ticketId = null;
2077
2114
  onMessageHandler = null;
2078
2115
  reconnectPromise = null;
2116
+ status = "disconnected";
2079
2117
  constructor({ network, walletUrl, apiUrl }) {
2080
2118
  this.network = network || "main";
2081
2119
  switch (this.network) {
@@ -2106,6 +2144,9 @@ class Connection {
2106
2144
  this.apiUrl = apiUrl;
2107
2145
  }
2108
2146
  }
2147
+ connectInProgress() {
2148
+ return this.status === "connecting" || this.status === "connected";
2149
+ }
2109
2150
  async getTicket(appName, sessionId, version) {
2110
2151
  const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/tickets`, {
2111
2152
  method: "POST",
@@ -2198,7 +2239,10 @@ class Connection {
2198
2239
  }
2199
2240
  });
2200
2241
  if (!response.ok) {
2201
- throw new Error("Session verification failed.");
2242
+ if (response.status === 401 || response.status === 403) {
2243
+ throw new UnauthorizedError;
2244
+ }
2245
+ throw new Error(`Session verification failed with status ${response.status}.`);
2202
2246
  }
2203
2247
  const data = await response.json();
2204
2248
  const email = data?.email;
@@ -2209,49 +2253,31 @@ class Connection {
2209
2253
  party_id: data?.party_id,
2210
2254
  auth_token: authToken,
2211
2255
  public_key: data?.public_key,
2212
- email
2256
+ email,
2257
+ has_preapproval: data?.has_preapproval,
2258
+ has_merge_delegation: data?.has_merge_delegation,
2259
+ usdc_bridge_access: data?.usdc_bridge_access
2213
2260
  };
2214
2261
  return account;
2215
2262
  }
2216
- websocketUrl(ticketId) {
2217
- return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
2218
- }
2219
- attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
2220
- const wsUrl = this.websocketUrl(ticketId);
2221
- const ws = new WebSocket(wsUrl);
2222
- ws.onmessage = onMessage;
2223
- ws.onopen = () => {
2224
- console.log("Connected to ticket server.");
2225
- onOpen?.();
2226
- };
2227
- ws.onclose = (event) => {
2228
- if (this.ws === ws) {
2229
- this.ws = null;
2230
- }
2231
- console.log("Disconnected from ticket server.");
2232
- onClose?.(event);
2233
- };
2234
- ws.onerror = (event) => {
2235
- if (this.ws === ws) {
2236
- this.ws = null;
2237
- }
2238
- onError?.(event);
2239
- };
2240
- this.ws = ws;
2241
- }
2242
2263
  connectWebSocket(ticketId, onMessage) {
2243
- this.ticketId = ticketId;
2264
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) && this.ticketId !== ticketId) {
2265
+ this.ws.close();
2266
+ this.ws = null;
2267
+ }
2268
+ if (this.status === "connecting" || this.status === "connected") {
2269
+ return;
2270
+ }
2244
2271
  this.onMessageHandler = onMessage;
2272
+ this.ticketId = ticketId;
2273
+ this.status = "connecting";
2245
2274
  this.attachWebSocket(ticketId, onMessage);
2246
2275
  }
2247
2276
  reconnect() {
2248
2277
  if (!this.ticketId || !this.onMessageHandler) {
2249
2278
  return Promise.reject(new Error("Cannot reconnect without a known ticket."));
2250
2279
  }
2251
- if (this.reconnectPromise) {
2252
- return this.reconnectPromise;
2253
- }
2254
- this.reconnectPromise = new Promise((resolve, reject) => {
2280
+ return new Promise((resolve, reject) => {
2255
2281
  let opened = false;
2256
2282
  this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
2257
2283
  opened = true;
@@ -2267,29 +2293,37 @@ class Connection {
2267
2293
  }
2268
2294
  reject(new Error("Failed to reconnect to ticket server."));
2269
2295
  });
2270
- }).finally(() => {
2271
- this.reconnectPromise = null;
2272
2296
  });
2273
- return this.reconnectPromise;
2274
2297
  }
2275
- async reconnectWebSocket() {
2276
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
2277
- return;
2278
- }
2279
- return this.reconnect();
2280
- }
2281
- }
2282
-
2283
- // src/errors.ts
2284
- class RequestTimeoutError extends Error {
2285
- constructor(timeout) {
2286
- super(`Request timed out after ${timeout}ms.`);
2298
+ websocketUrl(ticketId) {
2299
+ return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${encodeURIComponent(ticketId)}`;
2287
2300
  }
2288
- }
2289
-
2290
- class RejectRequestError extends Error {
2291
- constructor() {
2292
- super("Request was rejected by the wallet.");
2301
+ attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
2302
+ const wsUrl = this.websocketUrl(ticketId);
2303
+ const ws = new WebSocket(wsUrl);
2304
+ ws.onmessage = onMessage;
2305
+ ws.onopen = () => {
2306
+ this.status = "connected";
2307
+ console.log("[LoopSDK] Connected to ticket server.");
2308
+ onOpen?.();
2309
+ };
2310
+ ws.onclose = (event) => {
2311
+ this.status = "disconnected";
2312
+ if (this.ws === ws) {
2313
+ this.ws = null;
2314
+ }
2315
+ console.log("[LoopSDK] Disconnected from ticket server.");
2316
+ onClose?.(event);
2317
+ };
2318
+ ws.onerror = (event) => {
2319
+ this.status = "disconnected";
2320
+ ws.close();
2321
+ if (this.ws === ws) {
2322
+ this.ws = null;
2323
+ }
2324
+ onError?.(event);
2325
+ };
2326
+ this.ws = ws;
2293
2327
  }
2294
2328
  }
2295
2329
 
@@ -2345,10 +2379,13 @@ class Provider {
2345
2379
  this.requests.set(message.request_id, message);
2346
2380
  }
2347
2381
  }
2348
- async getHolding() {
2382
+ getHolding() {
2349
2383
  return this.connection.getHolding(this.auth_token);
2350
2384
  }
2351
- async getActiveContracts(params) {
2385
+ getAccount() {
2386
+ return this.connection.verifySession(this.auth_token);
2387
+ }
2388
+ getActiveContracts(params) {
2352
2389
  return this.connection.getActiveContracts(this.auth_token, params);
2353
2390
  }
2354
2391
  async submitTransaction(payload, options) {
@@ -2397,24 +2434,24 @@ class Provider {
2397
2434
  }
2398
2435
  async ensureConnected() {
2399
2436
  if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2400
- return;
2437
+ return Promise.resolve();
2401
2438
  }
2402
- if (typeof this.connection.reconnectWebSocket === "function") {
2403
- await this.connection.reconnectWebSocket();
2404
- if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2405
- return;
2406
- }
2439
+ await this.connection.reconnect();
2440
+ if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2441
+ return;
2407
2442
  }
2408
2443
  throw new Error("Not connected.");
2409
2444
  }
2410
2445
  sendRequest(messageType, params = {}, options) {
2411
2446
  return new Promise((resolve, reject) => {
2412
2447
  const requestId = generateRequestId();
2413
- const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2448
+ let requestContext;
2414
2449
  const ensure = async () => {
2415
2450
  try {
2416
2451
  await this.ensureConnected();
2452
+ requestContext = await this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2417
2453
  } catch (error) {
2454
+ console.error("[LoopSDK] error when checking connection status", error);
2418
2455
  this.hooks?.onRequestFinish?.({
2419
2456
  status: "error",
2420
2457
  messageType,
@@ -2438,7 +2475,13 @@ class Provider {
2438
2475
  };
2439
2476
  }
2440
2477
  }
2441
- this.connection.ws.send(JSON.stringify(requestBody));
2478
+ try {
2479
+ this.connection.ws.send(JSON.stringify(requestBody));
2480
+ } catch (error) {
2481
+ console.error("[LoopSDK] error when sending request", error);
2482
+ reject(error);
2483
+ return;
2484
+ }
2442
2485
  const intervalTime = 300;
2443
2486
  let elapsedTime = 0;
2444
2487
  const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
@@ -2447,6 +2490,18 @@ class Provider {
2447
2490
  if (response) {
2448
2491
  clearInterval(intervalId);
2449
2492
  this.requests.delete(requestId);
2493
+ const code = extractErrorCode(response);
2494
+ if (isUnauthCode(code)) {
2495
+ this.hooks?.onRequestFinish?.({
2496
+ status: "error",
2497
+ messageType,
2498
+ requestLabel: options?.requestLabel,
2499
+ requestContext,
2500
+ errorCode: code
2501
+ });
2502
+ reject(new UnauthorizedError(code));
2503
+ return;
2504
+ }
2450
2505
  if (response.type === "reject_request" /* REJECT_REQUEST */) {
2451
2506
  this.hooks?.onRequestFinish?.({
2452
2507
  status: "rejected",
@@ -2562,11 +2617,87 @@ class LoopWallet {
2562
2617
  }
2563
2618
  }
2564
2619
 
2620
+ // src/session.ts
2621
+ var STORAGE_KEY_LOOP_CONNECT = "loop_connect";
2622
+
2623
+ class SessionInfo {
2624
+ sessionId;
2625
+ ticketId;
2626
+ authToken;
2627
+ partyId;
2628
+ publicKey;
2629
+ email;
2630
+ _isAuthorized = false;
2631
+ constructor({ sessionId, ticketId, authToken, partyId, publicKey, email }) {
2632
+ this.sessionId = sessionId;
2633
+ this.ticketId = ticketId;
2634
+ this.authToken = authToken;
2635
+ this.partyId = partyId;
2636
+ this.publicKey = publicKey;
2637
+ this.email = email;
2638
+ }
2639
+ setTicketId(ticketId) {
2640
+ this.ticketId = ticketId;
2641
+ this.save();
2642
+ }
2643
+ authorized() {
2644
+ if (this.ticketId === undefined || this.sessionId === undefined || this.authToken === undefined || this.partyId === undefined || this.publicKey === undefined) {
2645
+ throw new Error("Session cannot be authorized without all required fields.");
2646
+ }
2647
+ this._isAuthorized = true;
2648
+ }
2649
+ isPreAuthorized() {
2650
+ return !this._isAuthorized && this.ticketId !== undefined && this.sessionId !== undefined && this.authToken !== undefined && this.partyId !== undefined && this.publicKey !== undefined;
2651
+ }
2652
+ isAuthorized() {
2653
+ return this._isAuthorized;
2654
+ }
2655
+ save() {
2656
+ localStorage.setItem("loop_connect", this.toJson());
2657
+ }
2658
+ reset() {
2659
+ localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
2660
+ this.sessionId = generateRequestId();
2661
+ this._isAuthorized = false;
2662
+ this.ticketId = undefined;
2663
+ this.authToken = undefined;
2664
+ this.partyId = undefined;
2665
+ this.publicKey = undefined;
2666
+ this.email = undefined;
2667
+ }
2668
+ static fromStorage() {
2669
+ const existingConnectionRaw = localStorage.getItem(STORAGE_KEY_LOOP_CONNECT);
2670
+ if (!existingConnectionRaw) {
2671
+ return new SessionInfo({ sessionId: generateRequestId() });
2672
+ }
2673
+ let session = null;
2674
+ try {
2675
+ session = new SessionInfo(JSON.parse(existingConnectionRaw));
2676
+ } catch (error) {
2677
+ console.error("Failed to parse existing connection info, local storage is corrupted.", error);
2678
+ localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
2679
+ session = new SessionInfo({ sessionId: generateRequestId() });
2680
+ }
2681
+ return session;
2682
+ }
2683
+ toJson() {
2684
+ return JSON.stringify({
2685
+ sessionId: this.sessionId,
2686
+ ticketId: this.ticketId,
2687
+ authToken: this.authToken,
2688
+ partyId: this.partyId,
2689
+ publicKey: this.publicKey,
2690
+ email: this.email
2691
+ });
2692
+ }
2693
+ }
2694
+
2565
2695
  // src/index.ts
2566
2696
  class LoopSDK {
2567
- version = "0.0.1";
2697
+ version = "0.7.3";
2568
2698
  appName = "Unknown";
2569
2699
  connection = null;
2700
+ session = null;
2570
2701
  provider = null;
2571
2702
  openMode = "popup";
2572
2703
  requestSigningMode = "popup";
@@ -2575,7 +2706,6 @@ class LoopSDK {
2575
2706
  onAccept = null;
2576
2707
  onReject = null;
2577
2708
  overlay = null;
2578
- ticketId = null;
2579
2709
  wallet;
2580
2710
  constructor() {
2581
2711
  this.wallet = new LoopWallet(() => this.provider);
@@ -2589,6 +2719,9 @@ class LoopSDK {
2589
2719
  onReject,
2590
2720
  options
2591
2721
  }) {
2722
+ if (typeof window === "undefined" || typeof document === "undefined" || typeof localStorage === "undefined") {
2723
+ throw new Error("LoopSDK can only be initialized in a browser environment with localStorage support.");
2724
+ }
2592
2725
  this.appName = appName;
2593
2726
  this.onAccept = onAccept || null;
2594
2727
  this.onReject = onReject || null;
@@ -2603,70 +2736,68 @@ class LoopSDK {
2603
2736
  this.redirectUrl = resolvedOptions.redirectUrl;
2604
2737
  this.connection = new Connection({ network, walletUrl, apiUrl });
2605
2738
  }
2606
- async connect() {
2607
- if (typeof window === "undefined") {
2608
- console.warn("LoopSDK.connect() can only be called in a browser environment.");
2739
+ async loadSessionInfo() {
2740
+ if (this.session) {
2741
+ return;
2742
+ }
2743
+ this.session = SessionInfo.fromStorage();
2744
+ if (!this.session.isPreAuthorized()) {
2609
2745
  return;
2610
2746
  }
2747
+ try {
2748
+ const verifiedAccount = await this.connection?.verifySession(this.session.authToken);
2749
+ if (!verifiedAccount || verifiedAccount?.party_id !== this.session.partyId) {
2750
+ console.warn("[LoopSDK] Stored partyId does not match verified account. Clearing cached session.");
2751
+ this.logout();
2752
+ return;
2753
+ }
2754
+ this.session.authorized();
2755
+ } catch (err) {
2756
+ if (err instanceof UnauthorizedError) {
2757
+ console.error("Unauthorized error when verifying session.", err);
2758
+ this.session.reset();
2759
+ return;
2760
+ }
2761
+ console.error("[LoopSDK] Failed to verify session.", err);
2762
+ throw err;
2763
+ }
2764
+ }
2765
+ async autoConnect() {
2611
2766
  if (!this.connection) {
2612
2767
  throw new Error("SDK not initialized. Call init() first.");
2613
2768
  }
2614
- const existingConnectionRaw = localStorage.getItem("loop_connect");
2615
- if (existingConnectionRaw) {
2616
- try {
2617
- let canReuseTicket = true;
2618
- const { ticketId, authToken, partyId, publicKey, email } = JSON.parse(existingConnectionRaw);
2619
- if (authToken && partyId && publicKey) {
2620
- try {
2621
- const verifiedAccount = await this.connection.verifySession(authToken);
2622
- if (verifiedAccount.party_id === partyId) {
2623
- this.provider = new Provider({
2624
- connection: this.connection,
2625
- party_id: partyId,
2626
- auth_token: authToken,
2627
- public_key: publicKey,
2628
- email,
2629
- hooks: this.createProviderHooks()
2630
- });
2631
- this.ticketId = ticketId || null;
2632
- this.onAccept?.(this.provider);
2633
- if (ticketId) {
2634
- this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2635
- }
2636
- return;
2637
- } else {
2638
- console.warn("[LoopSDK] Sttored partyId does not march verified account. Clearing cached session.");
2639
- canReuseTicket = false;
2640
- localStorage.removeItem("loop_connect");
2641
- }
2642
- } catch (err) {
2643
- console.error("Auto-login failed, token is invalid. Starting new connection.", err);
2644
- canReuseTicket = false;
2645
- localStorage.removeItem("loop_connect");
2646
- }
2647
- }
2648
- if (ticketId && canReuseTicket) {
2649
- this.ticketId = ticketId;
2650
- const url = new URL("/.connect/", this.connection.walletUrl);
2651
- url.searchParams.set("ticketId", ticketId);
2652
- if (this.redirectUrl) {
2653
- url.searchParams.set("redirectUrl", this.redirectUrl);
2654
- }
2655
- const connectUrl = url.toString();
2656
- this.showQrCode(connectUrl);
2657
- this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2658
- return;
2659
- }
2660
- } catch (error) {
2661
- console.error("Failed to parse existing connection info, creating a new one.", error);
2662
- }
2663
- localStorage.removeItem("loop_connect");
2769
+ await this.loadSessionInfo();
2770
+ if (!this.session) {
2771
+ throw new Error("No valid session found. The network connection maynot available or the backend is not reachable.");
2772
+ }
2773
+ if (this.session.isAuthorized()) {
2774
+ this.provider = new Provider({
2775
+ connection: this.connection,
2776
+ party_id: this.session.partyId,
2777
+ auth_token: this.session.authToken,
2778
+ public_key: this.session.publicKey,
2779
+ email: this.session.email,
2780
+ hooks: this.createProviderHooks()
2781
+ });
2782
+ this.onAccept?.(this.provider);
2783
+ this.connection.connectWebSocket(this.session.ticketId, this.handleWebSocketMessage.bind(this));
2784
+ return Promise.resolve();
2785
+ }
2786
+ }
2787
+ async connect() {
2788
+ if (!this.connection) {
2789
+ throw new Error("SDK not initialized. Call init() first.");
2790
+ }
2791
+ await this.autoConnect();
2792
+ if (this.connection?.connectInProgress() === true) {
2793
+ return;
2794
+ }
2795
+ if (this.session && this.session.isAuthorized()) {
2796
+ return;
2664
2797
  }
2665
- const sessionId = generateRequestId();
2666
2798
  try {
2667
- const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
2668
- this.ticketId = ticketId;
2669
- localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
2799
+ const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, this.session.sessionId, this.version);
2800
+ this.session.setTicketId(ticketId);
2670
2801
  const connectUrl = this.buildConnectUrl(ticketId);
2671
2802
  this.showQrCode(connectUrl);
2672
2803
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
@@ -2677,6 +2808,12 @@ class LoopSDK {
2677
2808
  }
2678
2809
  handleWebSocketMessage(event) {
2679
2810
  const message = JSON.parse(event.data);
2811
+ const errCode = extractErrorCode(message);
2812
+ if (isUnauthCode(errCode)) {
2813
+ console.warn("[LoopSDK] Detected session invalidation:", errCode, { message });
2814
+ this.logout();
2815
+ return;
2816
+ }
2680
2817
  console.log("[LoopSDK] WS message received:", message);
2681
2818
  if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
2682
2819
  console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
@@ -2690,32 +2827,27 @@ class LoopSDK {
2690
2827
  email,
2691
2828
  hooks: this.createProviderHooks()
2692
2829
  });
2693
- const connectionInfoRaw = localStorage.getItem("loop_connect");
2694
- if (connectionInfoRaw) {
2695
- try {
2696
- const connectionInfo = JSON.parse(connectionInfoRaw);
2697
- this.ticketId = connectionInfo.ticketId || this.ticketId;
2698
- connectionInfo.authToken = authToken;
2699
- connectionInfo.partyId = partyId;
2700
- connectionInfo.publicKey = publicKey;
2701
- connectionInfo.email = email;
2702
- localStorage.setItem("loop_connect", JSON.stringify(connectionInfo));
2703
- this.onAccept?.(this.provider);
2704
- this.hideQrCode();
2705
- this.connection?.connectWebSocket(connectionInfo.ticketId, this.handleWebSocketMessage.bind(this));
2706
- console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
2707
- this.popupWindow = null;
2708
- } catch (error) {
2709
- console.error("Failed to update local storage with auth token.", error);
2710
- }
2830
+ try {
2831
+ this.session.authToken = authToken;
2832
+ this.session.partyId = partyId;
2833
+ this.session.publicKey = publicKey;
2834
+ this.session.email = email;
2835
+ this.session.authorized();
2836
+ this.session.save();
2837
+ this.onAccept?.(this.provider);
2838
+ this.hideQrCode();
2839
+ console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
2840
+ this.popupWindow = null;
2841
+ } catch (error) {
2842
+ console.error("Failed to update local storage with auth token.", error);
2711
2843
  }
2712
2844
  }
2713
2845
  } else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
2714
2846
  console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
2715
- localStorage.removeItem("loop_connect");
2716
2847
  this.connection?.ws?.close();
2717
2848
  this.onReject?.();
2718
2849
  this.hideQrCode();
2850
+ this.session?.reset();
2719
2851
  console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
2720
2852
  this.popupWindow = null;
2721
2853
  } else if (this.provider) {
@@ -2740,7 +2872,7 @@ class LoopSDK {
2740
2872
  if (typeof window === "undefined") {
2741
2873
  return null;
2742
2874
  }
2743
- if (!this.ticketId) {
2875
+ if (!this.session?.ticketId) {
2744
2876
  console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
2745
2877
  return null;
2746
2878
  }
@@ -2785,9 +2917,6 @@ class LoopSDK {
2785
2917
  return window.open(url, "_blank", "noopener,noreferrer");
2786
2918
  }
2787
2919
  showQrCode(url) {
2788
- if (typeof window === "undefined" || typeof document === "undefined") {
2789
- return;
2790
- }
2791
2920
  import_qrcode.default.toDataURL(url, (err, dataUrl) => {
2792
2921
  if (err) {
2793
2922
  console.error("Failed to generate QR code", err);
@@ -2841,6 +2970,12 @@ class LoopSDK {
2841
2970
  this.overlay = null;
2842
2971
  }
2843
2972
  }
2973
+ logout() {
2974
+ this.session?.reset();
2975
+ this.provider = null;
2976
+ this.connection?.ws?.close();
2977
+ this.hideQrCode();
2978
+ }
2844
2979
  requireProvider() {
2845
2980
  if (!this.provider) {
2846
2981
  throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
@@ -2849,9 +2984,7 @@ class LoopSDK {
2849
2984
  }
2850
2985
  createProviderHooks() {
2851
2986
  return {
2852
- onRequestStart: () => {
2853
- return this.openRequestUi();
2854
- },
2987
+ onRequestStart: () => this.openRequestUi(),
2855
2988
  onRequestFinish: ({ requestContext }) => {
2856
2989
  const win = requestContext;
2857
2990
  if (win) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivenorth/loop-sdk",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "author": "hello@fivenorth.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",