@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 +7 -0
- package/dist/index.js +98 -8
- package/package.json +1 -1
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
|
-
|
|
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({
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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) {
|