@fivenorth/loop-sdk 0.7.0 → 0.7.1

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 (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +127 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -44,6 +44,7 @@ loop.init({
44
44
  network: 'local', // or 'devnet', 'mainnet'
45
45
  options: {
46
46
  openMode: 'popup', // 'popup' (default) or 'tab'
47
+ requestSigningMode: 'popup', // 'popup' (default) or 'tab'
47
48
  redirectUrl: 'https://myapp.com/after-connect', // optional redirect after approval
48
49
  },
49
50
  onAccept: (provider) => {
@@ -61,6 +62,7 @@ The `init` method takes a configuration object with the following properties:
61
62
  - `network`: The network to connect to. Can be `local`, `devnet`, or `mainnet`.
62
63
  - `options`: Optional object containing:
63
64
  - `openMode`: Controls how Loop opens: `'popup'` (default) or `'tab'`.
65
+ - `requestSigningMode`: Controls how signing/transaction requests open the wallet UI after you're connected: `'popup'` (default) or `'tab'`.
64
66
  - `redirectUrl`: Optional redirect URL the wallet will navigate back to after successful approval. If omitted, user stays on Loop dashboard.
65
67
  - `onAccept`: A callback function that is called when the user accepts the connection. It receives a `provider` object.
66
68
  - `onReject`: A callback function that is called when the user rejects the connection.
@@ -74,6 +76,7 @@ loop.connect();
74
76
  ```
75
77
 
76
78
  This will open a modal with a QR code for the user to scan with their Loop wallet.
79
+ If you set `requestSigningMode` to `'popup'` (or `'tab'`), each signing/transaction request will also open the wallet dashboard and auto-close the popup once the wallet responds.
77
80
 
78
81
  ### 3. Using the Provider
79
82
 
@@ -132,7 +135,10 @@ const damlCommand = {
132
135
  };
133
136
 
134
137
  try {
135
- const result = await provider.submitTransaction(damlCommand);
138
+ const result = await provider.submitTransaction(damlCommand, {
139
+ // Optional: show a custom message in the wallet prompt
140
+ message: 'Transfer 10 CC to RetailStore',
141
+ });
136
142
  console.log('Transaction successful:', result);
137
143
  } catch (error) {
138
144
  console.error('Transaction failed:', error);
@@ -165,6 +171,8 @@ await loop.wallet.transfer(
165
171
  instrument_id: 'Amulet', // optional
166
172
  },
167
173
  {
174
+ // Optional: show a custom message in the wallet prompt
175
+ message: 'Send 5 CC to Alice',
168
176
  requestedAt: new Date().toISOString(), // optional
169
177
  executeBefore: new Date(Date.now() + 24*60*60*1000).toISOString(), // optional
170
178
  requestTimeout: 5 * 60 * 1000, // optional (ms), defaults to 5 minutes
@@ -192,6 +200,7 @@ await loop.wallet.extension.usdcBridge.withdrawalUSDCxToEthereum(
192
200
  '10.5', // amount in USDCx
193
201
  {
194
202
  reference: 'optional memo',
203
+ message: 'Withdraw 10.5 USDCx to 0xabc', // optional custom prompt text
195
204
  requestTimeout: 5 * 60 * 1000, // optional override (ms)
196
205
  },
197
206
  );
package/dist/index.js CHANGED
@@ -2324,7 +2324,8 @@ class Provider {
2324
2324
  auth_token;
2325
2325
  requests = new Map;
2326
2326
  requestTimeout = DEFAULT_REQUEST_TIMEOUT_MS;
2327
- constructor({ connection, party_id, public_key, auth_token, email }) {
2327
+ hooks;
2328
+ constructor({ connection, party_id, public_key, auth_token, email, hooks }) {
2328
2329
  if (!connection) {
2329
2330
  throw new Error("Provider requires a connection object.");
2330
2331
  }
@@ -2333,6 +2334,7 @@ class Provider {
2333
2334
  this.public_key = public_key;
2334
2335
  this.email = email;
2335
2336
  this.auth_token = auth_token;
2337
+ this.hooks = hooks;
2336
2338
  }
2337
2339
  getAuthToken() {
2338
2340
  return this.auth_token;
@@ -2355,6 +2357,7 @@ class Provider {
2355
2357
  async transfer(recipient, amount, instrument, options) {
2356
2358
  const amountStr = typeof amount === "number" ? amount.toString() : amount;
2357
2359
  const { requestedAt, executeBefore, requestTimeout } = options || {};
2360
+ const message = options?.message;
2358
2361
  const resolveDate = (value, fallbackMs) => {
2359
2362
  if (value instanceof Date) {
2360
2363
  return value.toISOString();
@@ -2387,7 +2390,7 @@ class Provider {
2387
2390
  actAs: preparedPayload.actAs,
2388
2391
  readAs: preparedPayload.readAs,
2389
2392
  synchronizerId: preparedPayload.synchronizerId
2390
- }, { requestTimeout });
2393
+ }, { requestTimeout, message });
2391
2394
  }
2392
2395
  async signMessage(message) {
2393
2396
  return this.sendRequest("sign_raw_message" /* SIGN_RAW_MESSAGE */, message);
@@ -2406,19 +2409,36 @@ class Provider {
2406
2409
  }
2407
2410
  sendRequest(messageType, params = {}, options) {
2408
2411
  return new Promise((resolve, reject) => {
2412
+ const requestId = generateRequestId();
2413
+ const requestContext = this.hooks?.onRequestStart?.(messageType, options?.requestLabel);
2409
2414
  const ensure = async () => {
2410
2415
  try {
2411
2416
  await this.ensureConnected();
2412
2417
  } catch (error) {
2418
+ this.hooks?.onRequestFinish?.({
2419
+ status: "error",
2420
+ messageType,
2421
+ requestLabel: options?.requestLabel,
2422
+ requestContext
2423
+ });
2413
2424
  reject(error);
2414
2425
  return;
2415
2426
  }
2416
- const requestId = generateRequestId();
2417
- this.connection.ws.send(JSON.stringify({
2427
+ const requestBody = {
2418
2428
  request_id: requestId,
2419
2429
  type: messageType,
2420
2430
  payload: params
2421
- }));
2431
+ };
2432
+ if (options?.message) {
2433
+ requestBody.ticket = { message: options.message };
2434
+ if (typeof params === "object" && params !== null && !Array.isArray(params)) {
2435
+ requestBody.payload = {
2436
+ ...params,
2437
+ ticket: { message: options.message }
2438
+ };
2439
+ }
2440
+ }
2441
+ this.connection.ws.send(JSON.stringify(requestBody));
2422
2442
  const intervalTime = 300;
2423
2443
  let elapsedTime = 0;
2424
2444
  const timeoutMs = options?.requestTimeout ?? this.requestTimeout;
@@ -2428,8 +2448,20 @@ class Provider {
2428
2448
  clearInterval(intervalId);
2429
2449
  this.requests.delete(requestId);
2430
2450
  if (response.type === "reject_request" /* REJECT_REQUEST */) {
2451
+ this.hooks?.onRequestFinish?.({
2452
+ status: "rejected",
2453
+ messageType,
2454
+ requestLabel: options?.requestLabel,
2455
+ requestContext
2456
+ });
2431
2457
  reject(new RejectRequestError);
2432
2458
  } else {
2459
+ this.hooks?.onRequestFinish?.({
2460
+ status: "success",
2461
+ messageType,
2462
+ requestLabel: options?.requestLabel,
2463
+ requestContext
2464
+ });
2433
2465
  resolve(response.payload);
2434
2466
  }
2435
2467
  } else {
@@ -2437,6 +2469,12 @@ class Provider {
2437
2469
  if (elapsedTime >= timeoutMs) {
2438
2470
  clearInterval(intervalId);
2439
2471
  this.requests.delete(requestId);
2472
+ this.hooks?.onRequestFinish?.({
2473
+ status: "timeout",
2474
+ messageType,
2475
+ requestLabel: options?.requestLabel,
2476
+ requestContext
2477
+ });
2440
2478
  reject(new RequestTimeoutError(timeoutMs));
2441
2479
  }
2442
2480
  }
@@ -2475,7 +2513,7 @@ class UsdcBridge {
2475
2513
  actAs: preparedPayload.actAs,
2476
2514
  readAs: preparedPayload.readAs,
2477
2515
  synchronizerId: preparedPayload.synchronizerId
2478
- }, { requestTimeout: options?.requestTimeout }));
2516
+ }, { requestTimeout: options?.requestTimeout, message: options?.message }));
2479
2517
  }
2480
2518
  }
2481
2519
  async function prepareUsdcWithdraw(connection, authToken, params) {
@@ -2531,6 +2569,7 @@ class LoopSDK {
2531
2569
  connection = null;
2532
2570
  provider = null;
2533
2571
  openMode = "popup";
2572
+ requestSigningMode = "popup";
2534
2573
  popupWindow = null;
2535
2574
  redirectUrl;
2536
2575
  onAccept = null;
@@ -2555,10 +2594,12 @@ class LoopSDK {
2555
2594
  this.onReject = onReject || null;
2556
2595
  const resolvedOptions = {
2557
2596
  openMode: "popup",
2597
+ requestSigningMode: "popup",
2558
2598
  redirectUrl: undefined,
2559
2599
  ...options ?? {}
2560
2600
  };
2561
2601
  this.openMode = resolvedOptions.openMode;
2602
+ this.requestSigningMode = resolvedOptions.requestSigningMode;
2562
2603
  this.redirectUrl = resolvedOptions.redirectUrl;
2563
2604
  this.connection = new Connection({ network, walletUrl, apiUrl });
2564
2605
  }
@@ -2579,7 +2620,15 @@ class LoopSDK {
2579
2620
  try {
2580
2621
  const verifiedAccount = await this.connection.verifySession(authToken);
2581
2622
  if (verifiedAccount.party_id === partyId) {
2582
- this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey, email });
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;
2583
2632
  this.onAccept?.(this.provider);
2584
2633
  if (ticketId) {
2585
2634
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
@@ -2618,12 +2667,7 @@ class LoopSDK {
2618
2667
  const { ticket_id: ticketId } = await this.connection.getTicket(this.appName, sessionId, this.version);
2619
2668
  this.ticketId = ticketId;
2620
2669
  localStorage.setItem("loop_connect", JSON.stringify({ sessionId, ticketId }));
2621
- const url = new URL("/.connect/", this.connection.walletUrl);
2622
- url.searchParams.set("ticketId", ticketId);
2623
- if (this.redirectUrl) {
2624
- url.searchParams.set("redirectUrl", this.redirectUrl);
2625
- }
2626
- const connectUrl = url.toString();
2670
+ const connectUrl = this.buildConnectUrl(ticketId);
2627
2671
  this.showQrCode(connectUrl);
2628
2672
  this.connection.connectWebSocket(ticketId, this.handleWebSocketMessage.bind(this));
2629
2673
  } catch (error) {
@@ -2638,11 +2682,19 @@ class LoopSDK {
2638
2682
  console.log("[LoopSDK] Entering HANDSHAKE_ACCEPT flow");
2639
2683
  const { authToken, partyId, publicKey, email } = message.payload || {};
2640
2684
  if (authToken && partyId && publicKey) {
2641
- this.provider = new Provider({ connection: this.connection, party_id: partyId, auth_token: authToken, public_key: publicKey, email });
2685
+ this.provider = new Provider({
2686
+ connection: this.connection,
2687
+ party_id: partyId,
2688
+ auth_token: authToken,
2689
+ public_key: publicKey,
2690
+ email,
2691
+ hooks: this.createProviderHooks()
2692
+ });
2642
2693
  const connectionInfoRaw = localStorage.getItem("loop_connect");
2643
2694
  if (connectionInfoRaw) {
2644
2695
  try {
2645
2696
  const connectionInfo = JSON.parse(connectionInfoRaw);
2697
+ this.ticketId = connectionInfo.ticketId || this.ticketId;
2646
2698
  connectionInfo.authToken = authToken;
2647
2699
  connectionInfo.partyId = partyId;
2648
2700
  connectionInfo.publicKey = publicKey;
@@ -2670,11 +2722,51 @@ class LoopSDK {
2670
2722
  this.provider.handleResponse(message);
2671
2723
  }
2672
2724
  }
2673
- openWallet(url) {
2725
+ buildConnectUrl(ticketId) {
2726
+ const url = new URL("/.connect/", this.connection.walletUrl);
2727
+ url.searchParams.set("ticketId", ticketId);
2728
+ if (this.redirectUrl) {
2729
+ url.searchParams.set("redirectUrl", this.redirectUrl);
2730
+ }
2731
+ return url.toString();
2732
+ }
2733
+ buildDashboardUrl() {
2734
+ if (!this.connection) {
2735
+ throw new Error("Connection not initialized");
2736
+ }
2737
+ return this.connection.walletUrl;
2738
+ }
2739
+ openRequestUi() {
2674
2740
  if (typeof window === "undefined") {
2675
- return;
2741
+ return null;
2742
+ }
2743
+ if (!this.ticketId) {
2744
+ console.warn("[LoopSDK] Cannot open wallet UI for request: no active ticket.");
2745
+ return null;
2746
+ }
2747
+ const dashboardUrl = this.buildDashboardUrl();
2748
+ const targetMode = this.requestSigningMode === "tab" ? "tab" : "popup";
2749
+ const opened = this.openWallet(dashboardUrl, targetMode);
2750
+ if (opened) {
2751
+ this.popupWindow = opened;
2752
+ return opened;
2753
+ }
2754
+ return null;
2755
+ }
2756
+ closePopupIfExists() {
2757
+ if (this.popupWindow && !this.popupWindow.closed) {
2758
+ try {
2759
+ this.popupWindow.close();
2760
+ } catch {}
2676
2761
  }
2677
- if (this.openMode === "popup") {
2762
+ this.popupWindow = null;
2763
+ }
2764
+ openWallet(url, mode) {
2765
+ if (typeof window === "undefined") {
2766
+ return null;
2767
+ }
2768
+ const targetMode = mode || this.openMode;
2769
+ if (targetMode === "popup") {
2678
2770
  const width = 480;
2679
2771
  const height = 720;
2680
2772
  const left = (window.innerWidth - width) / 2 + window.screenX;
@@ -2682,16 +2774,15 @@ class LoopSDK {
2682
2774
  const features = `width=${width},height=${height},` + `left=${left},top=${top},` + "menubar=no,toolbar=no,location=no," + "resizable=yes,scrollbars=yes,status=no";
2683
2775
  const popup = window.open(url, "loop-wallet", features);
2684
2776
  if (!popup) {
2685
- window.open(url, "_blank", "noopener,noreferrer");
2686
- return;
2777
+ return window.open(url, "_blank", "noopener,noreferrer");
2687
2778
  }
2688
2779
  this.popupWindow = popup;
2689
2780
  try {
2690
2781
  popup.focus();
2691
2782
  } catch {}
2692
- return;
2783
+ return popup;
2693
2784
  }
2694
- window.open(url, "_blank", "noopener,noreferrer");
2785
+ return window.open(url, "_blank", "noopener,noreferrer");
2695
2786
  }
2696
2787
  showQrCode(url) {
2697
2788
  if (typeof window === "undefined" || typeof document === "undefined") {
@@ -2756,6 +2847,21 @@ class LoopSDK {
2756
2847
  }
2757
2848
  return this.provider;
2758
2849
  }
2850
+ createProviderHooks() {
2851
+ return {
2852
+ onRequestStart: () => {
2853
+ return this.openRequestUi();
2854
+ },
2855
+ onRequestFinish: ({ requestContext }) => {
2856
+ const win = requestContext;
2857
+ if (win) {
2858
+ setTimeout(() => {
2859
+ this.closePopupIfExists();
2860
+ }, 800);
2861
+ }
2862
+ }
2863
+ };
2864
+ }
2759
2865
  }
2760
2866
  var loop = new LoopSDK;
2761
2867
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fivenorth/loop-sdk",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "author": "hello@fivenorth.io",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",