@fivenorth/loop-sdk 0.7.2 → 0.7.4

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 +428 -186
  2. package/package.json +29 -29
package/dist/index.js CHANGED
@@ -2055,17 +2055,42 @@ var require_browser = __commonJS((exports) => {
2055
2055
  // src/index.ts
2056
2056
  var import_qrcode = __toESM(require_browser(), 1);
2057
2057
 
2058
- // src/types.ts
2059
- var MessageType;
2060
- ((MessageType2) => {
2061
- MessageType2["HANDSHAKE_ACCEPT"] = "handshake_accept";
2062
- MessageType2["HANDSHAKE_REJECT"] = "handshake_reject";
2063
- MessageType2["RUN_TRANSACTION"] = "run_transaction";
2064
- MessageType2["RUN_TRANSACTION_RESPONSE"] = "run_transaction_response";
2065
- MessageType2["SIGN_RAW_MESSAGE"] = "sign_raw_message";
2066
- MessageType2["SIGN_RAW_MESSAGE_RESPONSE"] = "sign_raw_message_response";
2067
- MessageType2["REJECT_REQUEST"] = "reject_request";
2068
- })(MessageType ||= {});
2058
+ // src/errors.ts
2059
+ class RequestTimeoutError extends Error {
2060
+ constructor(timeout) {
2061
+ super(`Request timed out after ${timeout}ms.`);
2062
+ }
2063
+ }
2064
+
2065
+ class RejectRequestError extends Error {
2066
+ constructor() {
2067
+ super("Request was rejected by the wallet.");
2068
+ }
2069
+ }
2070
+
2071
+ class UnauthorizedError extends Error {
2072
+ code;
2073
+ constructor(code) {
2074
+ super(code || "Unauthorized");
2075
+ this.code = code;
2076
+ }
2077
+ }
2078
+ var UNAUTH_CODES = new Set(["UNAUTHENTICATED", "UNAUTHORIZED", "SESSION_EXPIRED", "LOGGED_OUT"]);
2079
+ function extractErrorCode(message) {
2080
+ if (typeof message?.error?.code === "string" && message.error.code.length > 0) {
2081
+ return message.error.code;
2082
+ }
2083
+ if (message?.type === "unauthorized" && typeof message?.code === "string") {
2084
+ return message.code;
2085
+ }
2086
+ return null;
2087
+ }
2088
+ function isUnauthCode(code) {
2089
+ if (!code) {
2090
+ return false;
2091
+ }
2092
+ return UNAUTH_CODES.has(code);
2093
+ }
2069
2094
 
2070
2095
  // src/connection.ts
2071
2096
  class Connection {
@@ -2076,6 +2101,7 @@ class Connection {
2076
2101
  ticketId = null;
2077
2102
  onMessageHandler = null;
2078
2103
  reconnectPromise = null;
2104
+ status = "disconnected";
2079
2105
  constructor({ network, walletUrl, apiUrl }) {
2080
2106
  this.network = network || "main";
2081
2107
  switch (this.network) {
@@ -2106,6 +2132,9 @@ class Connection {
2106
2132
  this.apiUrl = apiUrl;
2107
2133
  }
2108
2134
  }
2135
+ connectInProgress() {
2136
+ return this.status === "connecting" || this.status === "connected";
2137
+ }
2109
2138
  async getTicket(appName, sessionId, version) {
2110
2139
  const response = await fetch(`${this.apiUrl}/api/v1/.connect/pair/tickets`, {
2111
2140
  method: "POST",
@@ -2198,7 +2227,10 @@ class Connection {
2198
2227
  }
2199
2228
  });
2200
2229
  if (!response.ok) {
2201
- throw new Error("Session verification failed.");
2230
+ if (response.status === 401 || response.status === 403) {
2231
+ throw new UnauthorizedError;
2232
+ }
2233
+ throw new Error(`Session verification failed with status ${response.status}.`);
2202
2234
  }
2203
2235
  const data = await response.json();
2204
2236
  const email = data?.email;
@@ -2216,45 +2248,24 @@ class Connection {
2216
2248
  };
2217
2249
  return account;
2218
2250
  }
2219
- websocketUrl(ticketId) {
2220
- return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${ticketId}`;
2221
- }
2222
- attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
2223
- const wsUrl = this.websocketUrl(ticketId);
2224
- const ws = new WebSocket(wsUrl);
2225
- ws.onmessage = onMessage;
2226
- ws.onopen = () => {
2227
- console.log("Connected to ticket server.");
2228
- onOpen?.();
2229
- };
2230
- ws.onclose = (event) => {
2231
- if (this.ws === ws) {
2232
- this.ws = null;
2233
- }
2234
- console.log("Disconnected from ticket server.");
2235
- onClose?.(event);
2236
- };
2237
- ws.onerror = (event) => {
2238
- if (this.ws === ws) {
2239
- this.ws = null;
2240
- }
2241
- onError?.(event);
2242
- };
2243
- this.ws = ws;
2244
- }
2245
2251
  connectWebSocket(ticketId, onMessage) {
2246
- this.ticketId = ticketId;
2252
+ if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) && this.ticketId !== ticketId) {
2253
+ this.ws.close();
2254
+ this.ws = null;
2255
+ }
2256
+ if (this.status === "connecting" || this.status === "connected") {
2257
+ return;
2258
+ }
2247
2259
  this.onMessageHandler = onMessage;
2260
+ this.ticketId = ticketId;
2261
+ this.status = "connecting";
2248
2262
  this.attachWebSocket(ticketId, onMessage);
2249
2263
  }
2250
2264
  reconnect() {
2251
2265
  if (!this.ticketId || !this.onMessageHandler) {
2252
2266
  return Promise.reject(new Error("Cannot reconnect without a known ticket."));
2253
2267
  }
2254
- if (this.reconnectPromise) {
2255
- return this.reconnectPromise;
2256
- }
2257
- this.reconnectPromise = new Promise((resolve, reject) => {
2268
+ return new Promise((resolve, reject) => {
2258
2269
  let opened = false;
2259
2270
  this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
2260
2271
  opened = true;
@@ -2270,31 +2281,51 @@ class Connection {
2270
2281
  }
2271
2282
  reject(new Error("Failed to reconnect to ticket server."));
2272
2283
  });
2273
- }).finally(() => {
2274
- this.reconnectPromise = null;
2275
2284
  });
2276
- return this.reconnectPromise;
2277
2285
  }
2278
- async reconnectWebSocket() {
2279
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
2280
- return;
2281
- }
2282
- return this.reconnect();
2286
+ websocketUrl(ticketId) {
2287
+ return `${this.network === "local" ? "ws" : "wss"}://${this.apiUrl.replace("https://", "").replace("http://", "")}/api/v1/.connect/pair/ws/${encodeURIComponent(ticketId)}`;
2283
2288
  }
2284
- }
2285
-
2286
- // src/errors.ts
2287
- class RequestTimeoutError extends Error {
2288
- constructor(timeout) {
2289
- super(`Request timed out after ${timeout}ms.`);
2289
+ attachWebSocket(ticketId, onMessage, onOpen, onError, onClose) {
2290
+ const wsUrl = this.websocketUrl(ticketId);
2291
+ const ws = new WebSocket(wsUrl);
2292
+ ws.onmessage = onMessage;
2293
+ ws.onopen = () => {
2294
+ this.status = "connected";
2295
+ console.log("[LoopSDK] Connected to ticket server.");
2296
+ onOpen?.();
2297
+ };
2298
+ ws.onclose = (event) => {
2299
+ this.status = "disconnected";
2300
+ if (this.ws === ws) {
2301
+ this.ws = null;
2302
+ }
2303
+ console.log("[LoopSDK] Disconnected from ticket server.");
2304
+ onClose?.(event);
2305
+ };
2306
+ ws.onerror = (event) => {
2307
+ this.status = "disconnected";
2308
+ ws.close();
2309
+ if (this.ws === ws) {
2310
+ this.ws = null;
2311
+ }
2312
+ onError?.(event);
2313
+ };
2314
+ this.ws = ws;
2290
2315
  }
2291
2316
  }
2292
2317
 
2293
- class RejectRequestError extends Error {
2294
- constructor() {
2295
- super("Request was rejected by the wallet.");
2296
- }
2297
- }
2318
+ // src/types.ts
2319
+ var MessageType;
2320
+ ((MessageType2) => {
2321
+ MessageType2["HANDSHAKE_ACCEPT"] = "handshake_accept";
2322
+ MessageType2["HANDSHAKE_REJECT"] = "handshake_reject";
2323
+ MessageType2["RUN_TRANSACTION"] = "run_transaction";
2324
+ MessageType2["RUN_TRANSACTION_RESPONSE"] = "run_transaction_response";
2325
+ MessageType2["SIGN_RAW_MESSAGE"] = "sign_raw_message";
2326
+ MessageType2["SIGN_RAW_MESSAGE_RESPONSE"] = "sign_raw_message_response";
2327
+ MessageType2["REJECT_REQUEST"] = "reject_request";
2328
+ })(MessageType ||= {});
2298
2329
 
2299
2330
  // src/provider.ts
2300
2331
  var DEFAULT_REQUEST_TIMEOUT_MS = 300000;
@@ -2403,24 +2434,24 @@ class Provider {
2403
2434
  }
2404
2435
  async ensureConnected() {
2405
2436
  if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2406
- return;
2437
+ return Promise.resolve();
2407
2438
  }
2408
- if (typeof this.connection.reconnectWebSocket === "function") {
2409
- await this.connection.reconnectWebSocket();
2410
- if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2411
- return;
2412
- }
2439
+ await this.connection.reconnect();
2440
+ if (this.connection.ws && this.connection.ws.readyState === WebSocket.OPEN) {
2441
+ return;
2413
2442
  }
2414
2443
  throw new Error("Not connected.");
2415
2444
  }
2416
2445
  sendRequest(messageType, params = {}, options) {
2417
2446
  return new Promise((resolve, reject) => {
2418
2447
  const requestId = generateRequestId();
2419
- const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2448
+ let requestContext;
2420
2449
  const ensure = async () => {
2421
2450
  try {
2422
2451
  await this.ensureConnected();
2452
+ requestContext = await this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2423
2453
  } catch (error) {
2454
+ console.error("[LoopSDK] error when checking connection status", error);
2424
2455
  this.hooks?.onRequestFinish?.({
2425
2456
  status: "error",
2426
2457
  messageType,
@@ -2444,7 +2475,13 @@ class Provider {
2444
2475
  };
2445
2476
  }
2446
2477
  }
2447
- 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
+ }
2448
2485
  const intervalTime = 300;
2449
2486
  let elapsedTime = 0;
2450
2487
  const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
@@ -2453,6 +2490,18 @@ class Provider {
2453
2490
  if (response) {
2454
2491
  clearInterval(intervalId);
2455
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
+ }
2456
2505
  if (response.type === "reject_request" /* REJECT_REQUEST */) {
2457
2506
  this.hooks?.onRequestFinish?.({
2458
2507
  status: "rejected",
@@ -2491,6 +2540,81 @@ class Provider {
2491
2540
  }
2492
2541
  }
2493
2542
 
2543
+ // src/session.ts
2544
+ var STORAGE_KEY_LOOP_CONNECT = "loop_connect";
2545
+
2546
+ class SessionInfo {
2547
+ sessionId;
2548
+ ticketId;
2549
+ authToken;
2550
+ partyId;
2551
+ publicKey;
2552
+ email;
2553
+ _isAuthorized = false;
2554
+ constructor({ sessionId, ticketId, authToken, partyId, publicKey, email }) {
2555
+ this.sessionId = sessionId;
2556
+ this.ticketId = ticketId;
2557
+ this.authToken = authToken;
2558
+ this.partyId = partyId;
2559
+ this.publicKey = publicKey;
2560
+ this.email = email;
2561
+ }
2562
+ setTicketId(ticketId) {
2563
+ this.ticketId = ticketId;
2564
+ this.save();
2565
+ }
2566
+ authorized() {
2567
+ if (this.ticketId === undefined || this.sessionId === undefined || this.authToken === undefined || this.partyId === undefined || this.publicKey === undefined) {
2568
+ throw new Error("Session cannot be authorized without all required fields.");
2569
+ }
2570
+ this._isAuthorized = true;
2571
+ }
2572
+ isPreAuthorized() {
2573
+ return !this._isAuthorized && this.ticketId !== undefined && this.sessionId !== undefined && this.authToken !== undefined && this.partyId !== undefined && this.publicKey !== undefined;
2574
+ }
2575
+ isAuthorized() {
2576
+ return this._isAuthorized;
2577
+ }
2578
+ save() {
2579
+ localStorage.setItem("loop_connect", this.toJson());
2580
+ }
2581
+ reset() {
2582
+ localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
2583
+ this.sessionId = generateRequestId();
2584
+ this._isAuthorized = false;
2585
+ this.ticketId = undefined;
2586
+ this.authToken = undefined;
2587
+ this.partyId = undefined;
2588
+ this.publicKey = undefined;
2589
+ this.email = undefined;
2590
+ }
2591
+ static fromStorage() {
2592
+ const existingConnectionRaw = localStorage.getItem(STORAGE_KEY_LOOP_CONNECT);
2593
+ if (!existingConnectionRaw) {
2594
+ return new SessionInfo({ sessionId: generateRequestId() });
2595
+ }
2596
+ let session = null;
2597
+ try {
2598
+ session = new SessionInfo(JSON.parse(existingConnectionRaw));
2599
+ } catch (error) {
2600
+ console.error("Failed to parse existing connection info, local storage is corrupted.", error);
2601
+ localStorage.removeItem(STORAGE_KEY_LOOP_CONNECT);
2602
+ session = new SessionInfo({ sessionId: generateRequestId() });
2603
+ }
2604
+ return session;
2605
+ }
2606
+ toJson() {
2607
+ return JSON.stringify({
2608
+ sessionId: this.sessionId,
2609
+ ticketId: this.ticketId,
2610
+ authToken: this.authToken,
2611
+ partyId: this.partyId,
2612
+ publicKey: this.publicKey,
2613
+ email: this.email
2614
+ });
2615
+ }
2616
+ }
2617
+
2494
2618
  // src/extensions/usdc/index.ts
2495
2619
  class UsdcBridge {
2496
2620
  getProvider;
@@ -2570,9 +2694,10 @@ class LoopWallet {
2570
2694
 
2571
2695
  // src/index.ts
2572
2696
  class LoopSDK {
2573
- version = "0.0.1";
2697
+ version = "0.7.3";
2574
2698
  appName = "Unknown";
2575
2699
  connection = null;
2700
+ session = null;
2576
2701
  provider = null;
2577
2702
  openMode = "popup";
2578
2703
  requestSigningMode = "popup";
@@ -2581,7 +2706,6 @@ class LoopSDK {
2581
2706
  onAccept = null;
2582
2707
  onReject = null;
2583
2708
  overlay = null;
2584
- ticketId = null;
2585
2709
  wallet;
2586
2710
  constructor() {
2587
2711
  this.wallet = new LoopWallet(() => this.provider);
@@ -2595,6 +2719,9 @@ class LoopSDK {
2595
2719
  onReject,
2596
2720
  options
2597
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
+ }
2598
2725
  this.appName = appName;
2599
2726
  this.onAccept = onAccept || null;
2600
2727
  this.onReject = onReject || null;
@@ -2609,71 +2736,70 @@ class LoopSDK {
2609
2736
  this.redirectUrl = resolvedOptions.redirectUrl;
2610
2737
  this.connection = new Connection({ network, walletUrl, apiUrl });
2611
2738
  }
2612
- async connect() {
2613
- if (typeof window === "undefined") {
2614
- 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()) {
2615
2745
  return;
2616
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() {
2617
2766
  if (!this.connection) {
2618
2767
  throw new Error("SDK not initialized. Call init() first.");
2619
2768
  }
2620
- const existingConnectionRaw = localStorage.getItem("loop_connect");
2621
- if (existingConnectionRaw) {
2622
- try {
2623
- let canReuseTicket = true;
2624
- const { ticketId, authToken, partyId, publicKey, email } = JSON.parse(existingConnectionRaw);
2625
- if (authToken && partyId && publicKey) {
2626
- try {
2627
- const verifiedAccount = await this.connection.verifySession(authToken);
2628
- if (verifiedAccount.party_id === partyId) {
2629
- this.provider = new Provider({
2630
- connection: this.connection,
2631
- party_id: partyId,
2632
- auth_token: authToken,
2633
- public_key: publicKey,
2634
- email,
2635
- hooks: this.createProviderHooks()
2636
- });
2637
- this.ticketId = ticketId || null;
2638
- this.onAccept?.(this.provider);
2639
- if (ticketId) {
2640
- this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2641
- }
2642
- return;
2643
- } else {
2644
- console.warn("[LoopSDK] Sttored partyId does not march verified account. Clearing cached session.");
2645
- canReuseTicket = false;
2646
- localStorage.removeItem("loop_connect");
2647
- }
2648
- } catch (err) {
2649
- console.error("Auto-login failed, token is invalid. Starting new connection.", err);
2650
- canReuseTicket = false;
2651
- localStorage.removeItem("loop_connect");
2652
- }
2653
- }
2654
- if (ticketId && canReuseTicket) {
2655
- this.ticketId = ticketId;
2656
- const url = new URL("/.connect/", this.connection.walletUrl);
2657
- url.searchParams.set("ticketId", ticketId);
2658
- if (this.redirectUrl) {
2659
- url.searchParams.set("redirectUrl", this.redirectUrl);
2660
- }
2661
- const connectUrl = url.toString();
2662
- this.showQrCode(connectUrl);
2663
- this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2664
- return;
2665
- }
2666
- } catch (error) {
2667
- console.error("Failed to parse existing connection info, creating a new one.", error);
2668
- }
2669
- 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.session?.ticketId) {
2793
+ this.showQrCode(this.buildConnectUrl(this.session.ticketId));
2794
+ return;
2795
+ }
2796
+ if (this.session && this.session.isAuthorized()) {
2797
+ return;
2670
2798
  }
2671
- const sessionId = generateRequestId();
2672
2799
  try {
2673
- const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
2674
- this.ticketId = ticketId;
2675
- localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
2676
- const connectUrl = this.buildConnectUrl(ticketId);
2800
+ const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, this.session.sessionId, this.version);
2801
+ this.session.setTicketId(ticketId);
2802
+ const connectUrl = this.buildConnectUrl(this.session.ticketId);
2677
2803
  this.showQrCode(connectUrl);
2678
2804
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2679
2805
  } catch (error) {
@@ -2683,6 +2809,14 @@ class LoopSDK {
2683
2809
  }
2684
2810
  handleWebSocketMessage(event) {
2685
2811
  const message = JSON.parse(event.data);
2812
+ const errCode = extractErrorCode(message);
2813
+ if (isUnauthCode(errCode)) {
2814
+ console.warn("[LoopSDK] Detected session invalidation:", errCode, {
2815
+ message
2816
+ });
2817
+ this.logout();
2818
+ return;
2819
+ }
2686
2820
  console.log("[LoopSDK] WS message received:", message);
2687
2821
  if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
2688
2822
  console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
@@ -2696,32 +2830,27 @@ class LoopSDK {
2696
2830
  email,
2697
2831
  hooks: this.createProviderHooks()
2698
2832
  });
2699
- const connectionInfoRaw = localStorage.getItem("loop_connect");
2700
- if (connectionInfoRaw) {
2701
- try {
2702
- const connectionInfo = JSON.parse(connectionInfoRaw);
2703
- this.ticketId = connectionInfo.ticketId || this.ticketId;
2704
- connectionInfo.authToken = authToken;
2705
- connectionInfo.partyId = partyId;
2706
- connectionInfo.publicKey = publicKey;
2707
- connectionInfo.email = email;
2708
- localStorage.setItem("loop_connect", JSON.stringify(connectionInfo));
2709
- this.onAccept?.(this.provider);
2710
- this.hideQrCode();
2711
- this.connection?.connectWebSocket(connectionInfo.ticketId, this.handleWebSocketMessage.bind(this));
2712
- console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
2713
- this.popupWindow = null;
2714
- } catch (error) {
2715
- console.error("Failed to update local storage with auth token.", error);
2716
- }
2833
+ try {
2834
+ this.session.authToken = authToken;
2835
+ this.session.partyId = partyId;
2836
+ this.session.publicKey = publicKey;
2837
+ this.session.email = email;
2838
+ this.session.authorized();
2839
+ this.session.save();
2840
+ this.onAccept?.(this.provider);
2841
+ this.hideQrCode();
2842
+ console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
2843
+ this.popupWindow = null;
2844
+ } catch (error) {
2845
+ console.error("Failed to update local storage with auth token.", error);
2717
2846
  }
2718
2847
  }
2719
2848
  } else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
2720
2849
  console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
2721
- localStorage.removeItem("loop_connect");
2722
2850
  this.connection?.ws?.close();
2723
2851
  this.onReject?.();
2724
2852
  this.hideQrCode();
2853
+ this.session?.reset();
2725
2854
  console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
2726
2855
  this.popupWindow = null;
2727
2856
  } else if (this.provider) {
@@ -2746,7 +2875,7 @@ class LoopSDK {
2746
2875
  if (typeof window === "undefined") {
2747
2876
  return null;
2748
2877
  }
2749
- if (!this.ticketId) {
2878
+ if (!this.session?.ticketId) {
2750
2879
  console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
2751
2880
  return null;
2752
2881
  }
@@ -2790,10 +2919,108 @@ class LoopSDK {
2790
2919
  }
2791
2920
  return window.open(url, "_blank", "noopener,noreferrer");
2792
2921
  }
2793
- showQrCode(url) {
2794
- if (typeof window === "undefined" || typeof document === "undefined") {
2922
+ injectModalStyles() {
2923
+ if (document.getElementById("loop-connect-styles"))
2795
2924
  return;
2796
- }
2925
+ const style = document.createElement("style");
2926
+ style.id = "loop-connect-styles";
2927
+ style.textContent = `
2928
+ .loop-connect {
2929
+ position: fixed;
2930
+ inset: 0;
2931
+ background: oklch(0.222 0 0 / 0.85);
2932
+ backdrop-filter: blur(8px);
2933
+ display: flex;
2934
+ justify-content: center;
2935
+ align-items: center;
2936
+ z-index: 10000;
2937
+ font-family: system-ui, -apple-system, sans-serif;
2938
+ animation: fadeIn 0.2s ease-out;
2939
+ }
2940
+ .loop-connect dialog {
2941
+ position: relative;
2942
+ overflow: hidden;
2943
+ background: oklch(0.253 0.008 274.6);
2944
+ box-shadow: 0 4px 24px oklch(0 0 0 / 0.1);
2945
+ border: 1px solid oklch(0.41 0.01 278.4);
2946
+ border-radius: 32px;
2947
+ padding: 24px;
2948
+ display: flex;
2949
+ flex-direction: column;
2950
+ align-items: center;
2951
+ gap: 16px;
2952
+ color: oklch(0.975 0.005 280);
2953
+ }
2954
+ .loop-connect .bg-logo {
2955
+ position: absolute;
2956
+ right: -20px;
2957
+ top: -40px;
2958
+ width: 140px;
2959
+ height: auto;
2960
+ opacity: 0.06;
2961
+ pointer-events: none;
2962
+ }
2963
+ .loop-connect h3 {
2964
+ margin: 0;
2965
+ font-size: 18px;
2966
+ font-weight: 600;
2967
+ letter-spacing: -0.015em;
2968
+ }
2969
+ .loop-connect figure {
2970
+ margin: 0;
2971
+ background: oklch(1 0 0);
2972
+ padding: 8px;
2973
+ border-radius: 24px;
2974
+ display: flex;
2975
+ justify-content: center;
2976
+ border: 2px solid oklch(0.41 0.01 278.4);
2977
+ box-shadow: 0 4px 24px oklch(0 0 0 / 0.1);
2978
+ }
2979
+ .loop-connect img {
2980
+ display: block;
2981
+ width: 225px;
2982
+ height: 225px;
2983
+ }
2984
+ .loop-connect .divider {
2985
+ width: 100%;
2986
+ display: flex;
2987
+ align-items: center;
2988
+ gap: 16px;
2989
+ color: oklch(0.554 0.012 280.3);
2990
+ font-size: 13px;
2991
+ font-weight: 600;
2992
+ }
2993
+ .loop-connect .divider::before,
2994
+ .loop-connect .divider::after {
2995
+ content: "";
2996
+ flex: 1;
2997
+ height: 1px;
2998
+ background: oklch(0.45 0.01 278);
2999
+ }
3000
+ .loop-connect button {
3001
+ background: oklch(0.976 0.101 112.3);
3002
+ border: 1px solid oklch(0.82 0.16 110);
3003
+ color: oklch(0.222 0 0);
3004
+ padding: 16px 32px;
3005
+ border-radius: 24px;
3006
+ font-size: 15px;
3007
+ font-weight: 600;
3008
+ cursor: pointer;
3009
+ transition: all 0.2s ease;
3010
+ width: 100%;
3011
+ }
3012
+ .loop-connect button:hover {
3013
+ background: oklch(0.98 0.105 112.5);
3014
+ }
3015
+ @keyframes fadeIn {
3016
+ from { opacity: 0; }
3017
+ to { opacity: 1; }
3018
+ }
3019
+ `;
3020
+ document.head.appendChild(style);
3021
+ }
3022
+ showQrCode(url) {
3023
+ this.injectModalStyles();
2797
3024
  import_qrcode.default.toDataURL(url, (err, dataUrl) => {
2798
3025
  if (err) {
2799
3026
  console.error("Failed to generate QR code", err);
@@ -2801,42 +3028,53 @@ class LoopSDK {
2801
3028
  }
2802
3029
  const overlay = document.createElement("div");
2803
3030
  overlay.id = "loop-sdk-connect-overlay";
2804
- overlay.className = "loop-sdk-connect-overlay";
2805
- overlay.style.position = "fixed";
2806
- overlay.style.top = "0";
2807
- overlay.style.left = "0";
2808
- overlay.style.width = "100%";
2809
- overlay.style.height = "100%";
2810
- overlay.style.backgroundColor = "rgba(0,0,0,0.9)";
2811
- overlay.style.display = "flex";
2812
- overlay.style.justifyContent = "center";
2813
- overlay.style.alignItems = "center";
2814
- overlay.style.zIndex = "1000";
2815
- overlay.style.flexDirection = "column";
2816
- const content = document.createElement("div");
2817
- content.className = "loop-sdk-connect-content";
2818
- content.style.display = "flex";
2819
- content.style.flexDirection = "column";
2820
- content.style.alignItems = "center";
3031
+ overlay.className = "loop-sdk-connect-overlay loop-connect";
3032
+ const dialog = document.createElement("dialog");
3033
+ dialog.open = true;
3034
+ const bgLogo = document.createElementNS("http://www.w3.org/2000/svg", "svg");
3035
+ bgLogo.setAttribute("class", "bg-logo");
3036
+ bgLogo.setAttribute("viewBox", "0 0 124.05 305.64");
3037
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
3038
+ path.setAttribute("d", "M24.58,99.47L124.05,0v224.42L24.58,124.95c-7.04-7.04-7.04-18.45,0-25.49Z");
3039
+ path.setAttribute("fill", "currentColor");
3040
+ const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
3041
+ rect.setAttribute("x", "12.89");
3042
+ rect.setAttribute("y", "194.48");
3043
+ rect.setAttribute("width", "98.27");
3044
+ rect.setAttribute("height", "98.27");
3045
+ rect.setAttribute("rx", "18.02");
3046
+ rect.setAttribute("ry", "18.02");
3047
+ rect.setAttribute("transform", "translate(-154.1 115.21) rotate(-45)");
3048
+ rect.setAttribute("fill", "currentColor");
3049
+ bgLogo.appendChild(path);
3050
+ bgLogo.appendChild(rect);
3051
+ const title = document.createElement("h3");
3052
+ title.textContent = "Scan with Phone";
3053
+ const figure = document.createElement("figure");
2821
3054
  const img = document.createElement("img");
2822
3055
  img.src = dataUrl;
2823
- content.appendChild(img);
2824
- const link = document.createElement("a");
2825
- link.href = url;
2826
- link.textContent = "Or click here to connect";
2827
- link.style.color = "white";
2828
- link.style.marginTop = "20px";
2829
- link.onclick = (e) => {
2830
- e.preventDefault();
3056
+ img.alt = "QR Code";
3057
+ figure.appendChild(img);
3058
+ const divider = document.createElement("div");
3059
+ divider.className = "divider";
3060
+ divider.textContent = "OR";
3061
+ const button = document.createElement("button");
3062
+ button.type = "button";
3063
+ button.textContent = "Continue in Browser";
3064
+ button.addEventListener("click", () => {
2831
3065
  this.openWallet(url);
2832
- };
2833
- content.appendChild(link);
2834
- overlay.appendChild(content);
2835
- overlay.onclick = (e) => {
3066
+ });
3067
+ dialog.appendChild(bgLogo);
3068
+ dialog.appendChild(title);
3069
+ dialog.appendChild(figure);
3070
+ dialog.appendChild(divider);
3071
+ dialog.appendChild(button);
3072
+ overlay.appendChild(dialog);
3073
+ overlay.addEventListener("click", (e) => {
2836
3074
  if (e.target === overlay) {
2837
3075
  this.hideQrCode();
2838
3076
  }
2839
- };
3077
+ });
2840
3078
  document.body.appendChild(overlay);
2841
3079
  this.overlay = overlay;
2842
3080
  });
@@ -2847,6 +3085,12 @@ class LoopSDK {
2847
3085
  this.overlay = null;
2848
3086
  }
2849
3087
  }
3088
+ logout() {
3089
+ this.session?.reset();
3090
+ this.provider = null;
3091
+ this.connection?.ws?.close();
3092
+ this.hideQrCode();
3093
+ }
2850
3094
  requireProvider() {
2851
3095
  if (!this.provider) {
2852
3096
  throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
@@ -2855,9 +3099,7 @@ class LoopSDK {
2855
3099
  }
2856
3100
  createProviderHooks() {
2857
3101
  return {
2858
- onRequestStart: () => {
2859
- return this.openRequestUi();
2860
- },
3102
+ onRequestStart: () => this.openRequestUi(),
2861
3103
  onRequestFinish: ({ requestContext }) => {
2862
3104
  const win = requestContext;
2863
3105
  if (win) {
package/package.json CHANGED
@@ -1,31 +1,31 @@
1
1
  {
2
- "name": "@fivenorth/loop-sdk",
3
- "version": "0.7.2",
4
- "author": "hello@fivenorth.io",
5
- "main": "dist/index.js",
6
- "module": "dist/index.js",
7
- "devDependencies": {
8
- "@types/bun": "latest"
9
- },
10
- "peerDependencies": {
11
- "typescript": "^5"
12
- },
13
- "files": [
14
- "dist"
15
- ],
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
- "repository": "github:fivenorth-io/loop-sdk",
20
- "scripts": {
21
- "build": "bun build ./src/index.ts --outdir ./dist",
22
- "prepublishOnly": "bun run build",
23
- "start": "bun run src/server.ts"
24
- },
25
- "type": "module",
26
- "types": "dist/index.d.ts",
27
- "dependencies": {
28
- "@types/qrcode": "^1.5.6",
29
- "qrcode": "^1.5.4"
30
- }
2
+ "name": "@fivenorth/loop-sdk",
3
+ "version": "0.7.4",
4
+ "author": "hello@fivenorth.io",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "devDependencies": {
8
+ "@types/bun": "latest"
9
+ },
10
+ "peerDependencies": {
11
+ "typescript": "^5"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": "github:fivenorth-io/loop-sdk",
20
+ "scripts": {
21
+ "build": "bun build ./src/index.ts --outdir ./dist",
22
+ "prepublishOnly": "bun run build",
23
+ "start": "bun run src/server.ts"
24
+ },
25
+ "type": "module",
26
+ "types": "dist/index.d.ts",
27
+ "dependencies": {
28
+ "@types/qrcode": "^1.5.6",
29
+ "qrcode": "^1.5.4"
30
+ }
31
31
  }