@fivenorth/loop-sdk 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,6 +42,10 @@ Before you can connect, you need to initialize the SDK. This is typically done o
42
42
  loop.init({
43
43
  appName: 'My Awesome dApp',
44
44
  network: 'local', // or 'devnet', 'mainnet'
45
+ options: {
46
+ openMode: 'popup', // 'popup' (default) or 'tab'
47
+ redirectUrl: 'https://myapp.com/after-connect', // optional redirect after approval
48
+ },
45
49
  onAccept: (provider) => {
46
50
  console.log('Connected!', provider);
47
51
  // You can now use the provider to interact with the wallet
@@ -55,6 +59,9 @@ loop.init({
55
59
  The `init` method takes a configuration object with the following properties:
56
60
  - `appName`: The name of your application, which will be displayed to the user in the Loop wallet.
57
61
  - `network`: The network to connect to. Can be `local`, `devnet`, or `mainnet`.
62
+ - `options`: Optional object containing:
63
+ - `openMode`: Controls how Loop opens: `'popup'` (default) or `'tab'`.
64
+ - `redirectUrl`: Optional redirect URL the wallet will navigate back to after successful approval. If omitted, user stays on Loop dashboard.
58
65
  - `onAccept`: A callback function that is called when the user accepts the connection. It receives a `provider` object.
59
66
  - `onReject`: A callback function that is called when the user rejects the connection.
60
67
 
package/dist/index.js CHANGED
@@ -2165,13 +2165,15 @@ class Connection {
2165
2165
  throw new Error("Session verification failed.");
2166
2166
  }
2167
2167
  const data = await response.json();
2168
+ const email = data?.email;
2168
2169
  if (!data?.party_id || !data?.public_key) {
2169
2170
  throw new Error("Invalid session verification response.");
2170
2171
  }
2171
2172
  const account = {
2172
2173
  party_id: data?.party_id,
2173
2174
  auth_token: authToken,
2174
- public_key: data?.public_key
2175
+ public_key: data?.public_key,
2176
+ email
2175
2177
  };
2176
2178
  return account;
2177
2179
  }
@@ -2205,8 +2207,25 @@ class RejectRequestError extends Error {
2205
2207
  }
2206
2208
 
2207
2209
  // src/provider.ts
2210
+ function generateUUID() {
2211
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => {
2212
+ const gCrypto = globalThis.crypto;
2213
+ if (!gCrypto?.getRandomValues) {
2214
+ const n2 = Number(c);
2215
+ return (n2 ^ Math.random() * 16 >> n2 / 4).toString(16);
2216
+ }
2217
+ const arr = gCrypto.getRandomValues(new Uint8Array(1));
2218
+ const byte = arr[0];
2219
+ const n = Number(c);
2220
+ return (n ^ (byte & 15) >> n / 4).toString(16);
2221
+ });
2222
+ }
2208
2223
  function generateRequestId() {
2209
- return crypto.randomUUID();
2224
+ const gCrypto = globalThis.crypto;
2225
+ if (gCrypto?.randomUUID) {
2226
+ return gCrypto.randomUUID();
2227
+ }
2228
+ return generateUUID();
2210
2229
  }
2211
2230
 
2212
2231
  class Provider {
@@ -2288,15 +2307,33 @@ class LoopSDK {
2288
2307
  appName = "Unknown";
2289
2308
  connection = null;
2290
2309
  provider = null;
2310
+ openMode = "popup";
2311
+ popupWindow = null;
2312
+ redirectUrl;
2291
2313
  onAccept = null;
2292
2314
  onReject = null;
2293
2315
  overlay = null;
2294
2316
  ticketId = null;
2295
2317
  constructor() {}
2296
- init({ appName, network, walletUrl, apiUrl, onAccept, onReject }) {
2318
+ init({
2319
+ appName,
2320
+ network,
2321
+ walletUrl,
2322
+ apiUrl,
2323
+ onAccept,
2324
+ onReject,
2325
+ options
2326
+ }) {
2297
2327
  this.appName = appName;
2298
2328
  this.onAccept = onAccept || null;
2299
2329
  this.onReject = onReject || null;
2330
+ const resolvedOptions = {
2331
+ openMode: "popup",
2332
+ redirectUrl: undefined,
2333
+ ...options ?? {}
2334
+ };
2335
+ this.openMode = resolvedOptions.openMode;
2336
+ this.redirectUrl = resolvedOptions.redirectUrl;
2300
2337
  this.connection = new Connection({ network, walletUrl, apiUrl });
2301
2338
  }
2302
2339
  async connect() {
@@ -2310,6 +2347,7 @@ class LoopSDK {
2310
2347
  const existingConnectionRaw = localStorage.getItem("loop_connect");
2311
2348
  if (existingConnectionRaw) {
2312
2349
  try {
2350
+ let canReuseTicket = true;
2313
2351
  const { ticketId, authToken, partyId, publicKey, email } = JSON.parse(existingConnectionRaw);
2314
2352
  if (authToken && partyId && publicKey) {
2315
2353
  try {
@@ -2321,14 +2359,25 @@ class LoopSDK {
2321
2359
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2322
2360
  }
2323
2361
  return;
2362
+ } else {
2363
+ console.warn("[LoopSDK] Sttored partyId does not march verified account. Clearing cached session.");
2364
+ canReuseTicket = false;
2365
+ localStorage.removeItem("loop_connect");
2324
2366
  }
2325
2367
  } catch (err) {
2326
2368
  console.error("Auto-login failed, token is invalid. Starting new connection.", err);
2369
+ canReuseTicket = false;
2370
+ localStorage.removeItem("loop_connect");
2327
2371
  }
2328
2372
  }
2329
- if (ticketId) {
2373
+ if (ticketId && canReuseTicket) {
2330
2374
  this.ticketId = ticketId;
2331
- const connectUrl = `${this.connection.walletUrl}/.connect/?ticketId=${ticketId}`;
2375
+ const url = new URL("/.connect/", this.connection.walletUrl);
2376
+ url.searchParams.set("ticketId", ticketId);
2377
+ if (this.redirectUrl) {
2378
+ url.searchParams.set("redirectUrl", this.redirectUrl);
2379
+ }
2380
+ const connectUrl = url.toString();
2332
2381
  this.showQrCode(connectUrl);
2333
2382
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2334
2383
  return;
@@ -2343,7 +2392,12 @@ class LoopSDK {
2343
2392
  const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
2344
2393
  this.ticketId = ticketId;
2345
2394
  localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
2346
- const connectUrl = `${this.connection.walletUrl}/.connect/?ticketId=${ticketId}`;
2395
+ const url = new URL("/.connect/", this.connection.walletUrl);
2396
+ url.searchParams.set("ticketId", ticketId);
2397
+ if (this.redirectUrl) {
2398
+ url.searchParams.set("redirectUrl", this.redirectUrl);
2399
+ }
2400
+ const connectUrl = url.toString();
2347
2401
  this.showQrCode(connectUrl);
2348
2402
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2349
2403
  } catch (error) {
@@ -2353,7 +2407,9 @@ class LoopSDK {
2353
2407
  }
2354
2408
  handleWebSocketMessage(event) {
2355
2409
  const message = JSON.parse(event.data);
2410
+ console.log("[LoopSDK] WS message received:", message);
2356
2411
  if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
2412
+ console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
2357
2413
  const { authToken, partyId, publicKey, email } = message.payload || {};
2358
2414
  if (authToken && partyId && publicKey) {
2359
2415
  this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey, email });
@@ -2369,20 +2425,51 @@ class LoopSDK {
2369
2425
  this.onAccept?.(this.provider);
2370
2426
  this.hideQrCode();
2371
2427
  this.connection?.connectWebSocket(connectionInfo.ticketId, this.handleWebSocketMessage.bind(this));
2428
+ console.log("[LoopSDK] HANDSHAKE_ACCEPT: closing popup (if exists)");
2429
+ this.popupWindow = null;
2372
2430
  } catch (error) {
2373
2431
  console.error("Failed to update local storage with auth token.", error);
2374
2432
  }
2375
2433
  }
2376
2434
  }
2377
2435
  } else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
2436
+ console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
2378
2437
  localStorage.removeItem("loop_connect");
2379
2438
  this.connection?.ws?.close();
2380
2439
  this.onReject?.();
2381
2440
  this.hideQrCode();
2441
+ console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
2442
+ if (this.popupWindow && !this.popupWindow.closed) {
2443
+ this.popupWindow.close();
2444
+ }
2445
+ this.popupWindow = null;
2382
2446
  } else if (this.provider) {
2383
2447
  this.provider.handleResponse(message);
2384
2448
  }
2385
2449
  }
2450
+ openWallet(url) {
2451
+ if (typeof window === "undefined") {
2452
+ return;
2453
+ }
2454
+ if (this.openMode === "popup") {
2455
+ const width = 480;
2456
+ const height = 720;
2457
+ const left = (window.innerWidth - width) / 2 + window.screenX;
2458
+ const top = (window.innerWidth - height) / 2 + window.screenY;
2459
+ const features = `width=${width},height=${height},` + `left=${left},top=${top},` + "menubar=no,toolbar=no,location=no," + "resizable=yes,scrollbars=yes,status=no";
2460
+ const popup = window.open(url, "loop-wallet", features);
2461
+ if (!popup) {
2462
+ window.open(url, "_blank", "noopener,noreferrer");
2463
+ return;
2464
+ }
2465
+ this.popupWindow = popup;
2466
+ try {
2467
+ popup.focus();
2468
+ } catch {}
2469
+ return;
2470
+ }
2471
+ window.open(url, "_blank", "noopener,noreferrer");
2472
+ }
2386
2473
  showQrCode(url) {
2387
2474
  if (typeof window === "undefined" || typeof document === "undefined") {
2388
2475
  return;
@@ -2398,7 +2485,7 @@ class LoopSDK {
2398
2485
  overlay.style.left = "0";
2399
2486
  overlay.style.width = "100%";
2400
2487
  overlay.style.height = "100%";
2401
- overlay.style.backgroundColor = "rgba(0,0,0,0.5)";
2488
+ overlay.style.backgroundColor = "rgba(0,0,0,0.9)";
2402
2489
  overlay.style.display = "flex";
2403
2490
  overlay.style.justifyContent = "center";
2404
2491
  overlay.style.alignItems = "center";
@@ -2412,7 +2499,10 @@ class LoopSDK {
2412
2499
  link.textContent = "Or click here to connect";
2413
2500
  link.style.color = "white";
2414
2501
  link.style.marginTop = "20px";
2415
- link.target = "_blank";
2502
+ link.onclick = (e) => {
2503
+ e.preventDefault();
2504
+ this.openWallet(url);
2505
+ };
2416
2506
  overlay.appendChild(link);
2417
2507
  overlay.onclick = (e) => {
2418
2508
  if (e.target === overlay) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivenorth/loop-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "author": "hello@fivenorth.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",