@fivenorth/loop-sdk 0.7.2 → 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.
- package/dist/index.js +272 -145
- 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
|
-
|
|
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;
|
|
@@ -2216,45 +2260,24 @@ class Connection {
|
|
|
2216
2260
|
};
|
|
2217
2261
|
return account;
|
|
2218
2262
|
}
|
|
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
2263
|
connectWebSocket(ticketId, onMessage) {
|
|
2246
|
-
this.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
|
+
}
|
|
2247
2271
|
this.onMessageHandler = onMessage;
|
|
2272
|
+
this.ticketId = ticketId;
|
|
2273
|
+
this.status = "connecting";
|
|
2248
2274
|
this.attachWebSocket(ticketId, onMessage);
|
|
2249
2275
|
}
|
|
2250
2276
|
reconnect() {
|
|
2251
2277
|
if (!this.ticketId || !this.onMessageHandler) {
|
|
2252
2278
|
return Promise.reject(new Error("Cannot reconnect without a known ticket."));
|
|
2253
2279
|
}
|
|
2254
|
-
|
|
2255
|
-
return this.reconnectPromise;
|
|
2256
|
-
}
|
|
2257
|
-
this.reconnectPromise = new Promise((resolve, reject) => {
|
|
2280
|
+
return new Promise((resolve, reject) => {
|
|
2258
2281
|
let opened = false;
|
|
2259
2282
|
this.attachWebSocket(this.ticketId, this.onMessageHandler, () => {
|
|
2260
2283
|
opened = true;
|
|
@@ -2270,29 +2293,37 @@ class Connection {
|
|
|
2270
2293
|
}
|
|
2271
2294
|
reject(new Error("Failed to reconnect to ticket server."));
|
|
2272
2295
|
});
|
|
2273
|
-
}).finally(() => {
|
|
2274
|
-
this.reconnectPromise = null;
|
|
2275
2296
|
});
|
|
2276
|
-
return this.reconnectPromise;
|
|
2277
2297
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
return;
|
|
2281
|
-
}
|
|
2282
|
-
return this.reconnect();
|
|
2283
|
-
}
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
// src/errors.ts
|
|
2287
|
-
class RequestTimeoutError extends Error {
|
|
2288
|
-
constructor(timeout) {
|
|
2289
|
-
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)}`;
|
|
2290
2300
|
}
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
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;
|
|
2296
2327
|
}
|
|
2297
2328
|
}
|
|
2298
2329
|
|
|
@@ -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
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
@@ -2568,11 +2617,87 @@ class LoopWallet {
|
|
|
2568
2617
|
}
|
|
2569
2618
|
}
|
|
2570
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
|
+
|
|
2571
2695
|
// src/index.ts
|
|
2572
2696
|
class LoopSDK {
|
|
2573
|
-
version = "0.
|
|
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,70 +2736,68 @@ class LoopSDK {
|
|
|
2609
2736
|
this.redirectUrl = resolvedOptions.redirectUrl;
|
|
2610
2737
|
this.connection = new Connection({ network, walletUrl, apiUrl });
|
|
2611
2738
|
}
|
|
2612
|
-
async
|
|
2613
|
-
if (
|
|
2614
|
-
|
|
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
|
-
|
|
2621
|
-
if (
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
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.connection?.connectInProgress() === true) {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (this.session && this.session.isAuthorized()) {
|
|
2796
|
+
return;
|
|
2670
2797
|
}
|
|
2671
|
-
const sessionId = generateRequestId();
|
|
2672
2798
|
try {
|
|
2673
|
-
const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
|
|
2674
|
-
this.ticketId
|
|
2675
|
-
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);
|
|
2676
2801
|
const connectUrl = this.buildConnectUrl(ticketId);
|
|
2677
2802
|
this.showQrCode(connectUrl);
|
|
2678
2803
|
this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
|
|
@@ -2683,6 +2808,12 @@ class LoopSDK {
|
|
|
2683
2808
|
}
|
|
2684
2809
|
handleWebSocketMessage(event) {
|
|
2685
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
|
+
}
|
|
2686
2817
|
console.log("[LoopSDK] WS message received:", message);
|
|
2687
2818
|
if (message.type === "handshake_accept" /* HANDSHAKE_ACCEPT */) {
|
|
2688
2819
|
console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
|
|
@@ -2696,32 +2827,27 @@ class LoopSDK {
|
|
|
2696
2827
|
email,
|
|
2697
2828
|
hooks: this.createProviderHooks()
|
|
2698
2829
|
});
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
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
|
-
}
|
|
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);
|
|
2717
2843
|
}
|
|
2718
2844
|
}
|
|
2719
2845
|
} else if (message.type === "handshake_reject" /* HANDSHAKE_REJECT */) {
|
|
2720
2846
|
console.log("[LoopSDK] Entering HANDSHAKE_REJECT flow");
|
|
2721
|
-
localStorage.removeItem("loop_connect");
|
|
2722
2847
|
this.connection?.ws?.close();
|
|
2723
2848
|
this.onReject?.();
|
|
2724
2849
|
this.hideQrCode();
|
|
2850
|
+
this.session?.reset();
|
|
2725
2851
|
console.log("[LoopSDK] HANDSHAKE_REJECT: closing popup (if exists)");
|
|
2726
2852
|
this.popupWindow = null;
|
|
2727
2853
|
} else if (this.provider) {
|
|
@@ -2746,7 +2872,7 @@ class LoopSDK {
|
|
|
2746
2872
|
if (typeof window === "undefined") {
|
|
2747
2873
|
return null;
|
|
2748
2874
|
}
|
|
2749
|
-
if (!this.ticketId) {
|
|
2875
|
+
if (!this.session?.ticketId) {
|
|
2750
2876
|
console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
|
|
2751
2877
|
return null;
|
|
2752
2878
|
}
|
|
@@ -2791,9 +2917,6 @@ class LoopSDK {
|
|
|
2791
2917
|
return window.open(url, "_blank", "noopener,noreferrer");
|
|
2792
2918
|
}
|
|
2793
2919
|
showQrCode(url) {
|
|
2794
|
-
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
2920
|
import_qrcode.default.toDataURL(url, (err, dataUrl) => {
|
|
2798
2921
|
if (err) {
|
|
2799
2922
|
console.error("Failed to generate QR code", err);
|
|
@@ -2847,6 +2970,12 @@ class LoopSDK {
|
|
|
2847
2970
|
this.overlay = null;
|
|
2848
2971
|
}
|
|
2849
2972
|
}
|
|
2973
|
+
logout() {
|
|
2974
|
+
this.session?.reset();
|
|
2975
|
+
this.provider = null;
|
|
2976
|
+
this.connection?.ws?.close();
|
|
2977
|
+
this.hideQrCode();
|
|
2978
|
+
}
|
|
2850
2979
|
requireProvider() {
|
|
2851
2980
|
if (!this.provider) {
|
|
2852
2981
|
throw new Error("SDK not connected. Call connect() and wait for acceptance first.");
|
|
@@ -2855,9 +2984,7 @@ class LoopSDK {
|
|
|
2855
2984
|
}
|
|
2856
2985
|
createProviderHooks() {
|
|
2857
2986
|
return {
|
|
2858
|
-
onRequestStart: () =>
|
|
2859
|
-
return this.openRequestUi();
|
|
2860
|
-
},
|
|
2987
|
+
onRequestStart: () => this.openRequestUi(),
|
|
2861
2988
|
onRequestFinish: ({ requestContext }) => {
|
|
2862
2989
|
const win = requestContext;
|
|
2863
2990
|
if (win) {
|