@qrlwallet/connect-ui 0.1.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 +100 -0
- package/dist/index.d.mts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +538 -0
- package/dist/index.mjs +498 -0
- package/package.json +66 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# @qrlwallet/connect-ui
|
|
2
|
+
|
|
3
|
+
The MyQRLWallet pairing dialog as a framework-free web component, for dApps using [`@qrlwallet/connect`](https://github.com/DigitalGuards/myqrlwallet-connect). One import replaces the QR modal every dApp used to hand-copy.
|
|
4
|
+
|
|
5
|
+
- `<qrl-pairing-modal>`: shadow-DOM custom element, themeable with CSS custom properties, dark MyQRLWallet look by default.
|
|
6
|
+
- `showPairingModal(provider)`: one-line helper that wires the modal to a connect provider and resolves when pairing finishes.
|
|
7
|
+
- Zero framework dependencies; works in React, Vue, Svelte and plain HTML alike. Only runtime dependency is the `qrcode` encoder.
|
|
8
|
+
- Purely presentational: consumes only the SDK's public API and contains no cryptography. Keys, sessions and protocol live in `@qrlwallet/connect`.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @qrlwallet/connect @qrlwallet/connect-ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { QRLConnect } from '@qrlwallet/connect';
|
|
20
|
+
import { showPairingModal } from '@qrlwallet/connect-ui';
|
|
21
|
+
|
|
22
|
+
const provider = new QRLConnect({
|
|
23
|
+
dappMetadata: { name: 'My dApp', url: location.origin },
|
|
24
|
+
autoReconnect: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = await showPairingModal(provider);
|
|
28
|
+
// 'connected' -> handshake completed, start using provider.request()
|
|
29
|
+
// 'cancelled' -> user dismissed the dialog
|
|
30
|
+
// 'redirected' -> mobile browser navigated to the wallet deep link
|
|
31
|
+
|
|
32
|
+
if (result === 'connected') {
|
|
33
|
+
const accounts = await provider.request({ method: 'qrl_requestAccounts' });
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`showPairingModal` reuses a stored session URI when one exists. To force a fresh pairing (tear down the old channel and rotate keys), pass `{ fresh: true }`; wire that to your "New connection" affordance outside the modal if you have one. Inside the modal the New connection action already rotates in place.
|
|
38
|
+
|
|
39
|
+
### Options
|
|
40
|
+
|
|
41
|
+
| Option | Default | Meaning |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `fresh` | `false` | Start from `newConnection()` instead of the stored session URI |
|
|
44
|
+
| `walletName` | `"MyQRLWallet"` | Dialog title branding |
|
|
45
|
+
| `walletUrl` | `https://myqrlwallet.com` | Get-the-wallet link under the title (app downloads; intentionally a different host than `webWalletUrl`) |
|
|
46
|
+
| `webWalletUrl` | `https://qrlwallet.com` | Base URL for the "Open web wallet" action; pass `''` to hide it |
|
|
47
|
+
| `container` | `document.body` | Mount point for the modal element |
|
|
48
|
+
| `mobileRedirect` | `true` | On mobile browsers navigate straight to the `qrlconnect://` deep link instead of showing a QR |
|
|
49
|
+
|
|
50
|
+
## Using the element directly
|
|
51
|
+
|
|
52
|
+
If you manage the pairing lifecycle yourself (as the existing dApps do with their `useQrlWallet`-style hooks), render the element and feed it attributes:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { defineQrlPairingModal } from '@qrlwallet/connect-ui';
|
|
56
|
+
|
|
57
|
+
defineQrlPairingModal();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```html
|
|
61
|
+
<qrl-pairing-modal
|
|
62
|
+
uri="qrlconnect://pair?..."
|
|
63
|
+
status="waiting"
|
|
64
|
+
></qrl-pairing-modal>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Attributes: `uri`, `status`, `wallet-name`, `wallet-url`, `web-wallet-url` (absent = default web wallet, empty string = hide the action). Events (bubbling, composed): `qrl-new-connection` when the user asks for a fresh pairing, `qrl-cancel` when the dialog is dismissed (Cancel action, Escape, or backdrop click). The element renders nothing outside its own box: mount and remove it to show and hide.
|
|
68
|
+
|
|
69
|
+
### The web-wallet handoff link
|
|
70
|
+
|
|
71
|
+
"Open web wallet" opens `<web-wallet-url>/dapp-sessions#qrlconnect=<encodeURIComponent(uri)>` in a new tab; the wallet reads the fragment, scrubs it from the address bar, and asks the user to approve. The URI travels in the URL fragment so it never reaches any server. If you build this link yourself, the `encodeURIComponent` step is mandatory: an un-encoded URI truncates at its first `&` or `#`. Requires a wallet deployment that reads the fragment; older deployments ignore it entirely (no pairing starts, and the URI lingers in the address bar), which is why the wallet ingress ships first.
|
|
72
|
+
|
|
73
|
+
## Theming
|
|
74
|
+
|
|
75
|
+
Set CSS custom properties on the element or any ancestor:
|
|
76
|
+
|
|
77
|
+
| Property | Default | Role |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `--qrl-modal-accent` | `#f97316` | Left card border, hover + focus accents |
|
|
80
|
+
| `--qrl-modal-bg` | `#0f172a` | Card background |
|
|
81
|
+
| `--qrl-modal-fg` | `#e2e8f0` | Primary text |
|
|
82
|
+
| `--qrl-modal-muted` | `#94a3b8` | Secondary text |
|
|
83
|
+
| `--qrl-modal-link` | `#38bdf8` | Links and link-style buttons |
|
|
84
|
+
| `--qrl-modal-border` | `rgba(148,163,184,.25)` | Card + button borders |
|
|
85
|
+
| `--qrl-modal-radius` | `12px` | Card corner radius |
|
|
86
|
+
| `--qrl-modal-backdrop` | `rgba(2,6,23,.8)` | Backdrop overlay |
|
|
87
|
+
| `--qrl-modal-font` | system stack | Font family |
|
|
88
|
+
| `--qrl-modal-z` | `2147483000` | Backdrop z-index |
|
|
89
|
+
|
|
90
|
+
## Accessibility
|
|
91
|
+
|
|
92
|
+
`role="dialog"` with `aria-modal`, labelled title, focus moved into the dialog on open and restored on close, Tab focus trap, Escape to dismiss, `aria-live` status line.
|
|
93
|
+
|
|
94
|
+
## Why a separate package
|
|
95
|
+
|
|
96
|
+
The core SDK keeps a small, auditable, crypto-fenced surface. UI code and the QR encoder dependency deliberately live here instead, so dApps that build their own pairing UI never pull them in.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
declare const QRL_PAIRING_MODAL_TAG = "qrl-pairing-modal";
|
|
2
|
+
declare class QrlPairingModal extends HTMLElement {
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
private readonly shadow;
|
|
5
|
+
private readonly card;
|
|
6
|
+
private readonly titleEl;
|
|
7
|
+
private readonly walletLink;
|
|
8
|
+
private readonly qrBox;
|
|
9
|
+
private readonly statusEl;
|
|
10
|
+
private readonly webLink;
|
|
11
|
+
private readonly openLink;
|
|
12
|
+
private readonly copyLabel;
|
|
13
|
+
private qrToken;
|
|
14
|
+
private lastQrUri;
|
|
15
|
+
private copyTimer;
|
|
16
|
+
private restoreFocus;
|
|
17
|
+
constructor();
|
|
18
|
+
get uri(): string | null;
|
|
19
|
+
set uri(value: string | null);
|
|
20
|
+
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void;
|
|
21
|
+
connectedCallback(): void;
|
|
22
|
+
disconnectedCallback(): void;
|
|
23
|
+
private fire;
|
|
24
|
+
private syncAll;
|
|
25
|
+
private syncWallet;
|
|
26
|
+
private syncStatus;
|
|
27
|
+
private syncUri;
|
|
28
|
+
/**
|
|
29
|
+
* The web-wallet handoff: an https link carrying the pairing URI in the
|
|
30
|
+
* URL fragment (never sent to servers; the wallet scrubs and stages it
|
|
31
|
+
* behind its consent modal). Attribute absent = default web wallet;
|
|
32
|
+
* attribute set but empty = feature disabled. Hidden links carry no href,
|
|
33
|
+
* which also keeps them out of the focus trap.
|
|
34
|
+
*/
|
|
35
|
+
private syncWebWalletLink;
|
|
36
|
+
private renderQr;
|
|
37
|
+
private copyUri;
|
|
38
|
+
private showCopyFeedback;
|
|
39
|
+
private focusables;
|
|
40
|
+
private handleKeydown;
|
|
41
|
+
}
|
|
42
|
+
declare function defineQrlPairingModal(tagName?: string): void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structural subset of QRLConnectProvider that the modal needs. Kept
|
|
46
|
+
* duck-typed so @qrlwallet/connect stays an optional peer dependency;
|
|
47
|
+
* a compile-time test asserts the real provider satisfies it.
|
|
48
|
+
*/
|
|
49
|
+
interface PairingProvider {
|
|
50
|
+
/** Returns the pairing URI, reusing a stored session when available. */
|
|
51
|
+
getConnectionURI(): Promise<string>;
|
|
52
|
+
/** Tears down the existing pairing and rotates to a fresh channel/keys. */
|
|
53
|
+
newConnection(): Promise<string>;
|
|
54
|
+
isMobile(): boolean;
|
|
55
|
+
on(event: 'connect', listener: (info: {
|
|
56
|
+
chainId: string;
|
|
57
|
+
}) => void): unknown;
|
|
58
|
+
on(event: 'statusChanged', listener: (status: string) => void): unknown;
|
|
59
|
+
off(event: 'connect', listener: (info: {
|
|
60
|
+
chainId: string;
|
|
61
|
+
}) => void): unknown;
|
|
62
|
+
off(event: 'statusChanged', listener: (status: string) => void): unknown;
|
|
63
|
+
}
|
|
64
|
+
type PairingResult = 'connected' | 'cancelled' | 'redirected';
|
|
65
|
+
interface ShowPairingOptions {
|
|
66
|
+
/** Tear down any existing pairing and rotate channel/keys first. */
|
|
67
|
+
fresh?: boolean;
|
|
68
|
+
/** Shown in the dialog title. Default "MyQRLWallet". */
|
|
69
|
+
walletName?: string;
|
|
70
|
+
/** Get-the-wallet link under the title. Default https://myqrlwallet.com. */
|
|
71
|
+
walletUrl?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Base URL for the "Open web wallet" handoff (the pairing URI travels in
|
|
74
|
+
* the URL fragment). Default https://qrlwallet.com; pass '' to hide the
|
|
75
|
+
* web-wallet action.
|
|
76
|
+
*/
|
|
77
|
+
webWalletUrl?: string;
|
|
78
|
+
/** Where to mount the modal. Default document.body. */
|
|
79
|
+
container?: HTMLElement;
|
|
80
|
+
/**
|
|
81
|
+
* On mobile browsers navigate straight to the qrlconnect:// deep link
|
|
82
|
+
* instead of showing a QR (the wallet app opens in the foreground).
|
|
83
|
+
* Default true.
|
|
84
|
+
*/
|
|
85
|
+
mobileRedirect?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Shows the pairing dialog and resolves when the pairing reaches a
|
|
89
|
+
* terminal state: 'connected' (handshake completed), 'cancelled' (user
|
|
90
|
+
* dismissed the dialog), or 'redirected' (mobile deep-link navigation;
|
|
91
|
+
* the page is likely being backgrounded).
|
|
92
|
+
*/
|
|
93
|
+
declare function showPairingModal(provider: PairingProvider, options?: ShowPairingOptions): Promise<PairingResult>;
|
|
94
|
+
|
|
95
|
+
export { type PairingProvider, type PairingResult, QRL_PAIRING_MODAL_TAG, QrlPairingModal, type ShowPairingOptions, defineQrlPairingModal, showPairingModal };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
declare const QRL_PAIRING_MODAL_TAG = "qrl-pairing-modal";
|
|
2
|
+
declare class QrlPairingModal extends HTMLElement {
|
|
3
|
+
static get observedAttributes(): string[];
|
|
4
|
+
private readonly shadow;
|
|
5
|
+
private readonly card;
|
|
6
|
+
private readonly titleEl;
|
|
7
|
+
private readonly walletLink;
|
|
8
|
+
private readonly qrBox;
|
|
9
|
+
private readonly statusEl;
|
|
10
|
+
private readonly webLink;
|
|
11
|
+
private readonly openLink;
|
|
12
|
+
private readonly copyLabel;
|
|
13
|
+
private qrToken;
|
|
14
|
+
private lastQrUri;
|
|
15
|
+
private copyTimer;
|
|
16
|
+
private restoreFocus;
|
|
17
|
+
constructor();
|
|
18
|
+
get uri(): string | null;
|
|
19
|
+
set uri(value: string | null);
|
|
20
|
+
attributeChangedCallback(name: string, _oldValue: string | null, _newValue: string | null): void;
|
|
21
|
+
connectedCallback(): void;
|
|
22
|
+
disconnectedCallback(): void;
|
|
23
|
+
private fire;
|
|
24
|
+
private syncAll;
|
|
25
|
+
private syncWallet;
|
|
26
|
+
private syncStatus;
|
|
27
|
+
private syncUri;
|
|
28
|
+
/**
|
|
29
|
+
* The web-wallet handoff: an https link carrying the pairing URI in the
|
|
30
|
+
* URL fragment (never sent to servers; the wallet scrubs and stages it
|
|
31
|
+
* behind its consent modal). Attribute absent = default web wallet;
|
|
32
|
+
* attribute set but empty = feature disabled. Hidden links carry no href,
|
|
33
|
+
* which also keeps them out of the focus trap.
|
|
34
|
+
*/
|
|
35
|
+
private syncWebWalletLink;
|
|
36
|
+
private renderQr;
|
|
37
|
+
private copyUri;
|
|
38
|
+
private showCopyFeedback;
|
|
39
|
+
private focusables;
|
|
40
|
+
private handleKeydown;
|
|
41
|
+
}
|
|
42
|
+
declare function defineQrlPairingModal(tagName?: string): void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Structural subset of QRLConnectProvider that the modal needs. Kept
|
|
46
|
+
* duck-typed so @qrlwallet/connect stays an optional peer dependency;
|
|
47
|
+
* a compile-time test asserts the real provider satisfies it.
|
|
48
|
+
*/
|
|
49
|
+
interface PairingProvider {
|
|
50
|
+
/** Returns the pairing URI, reusing a stored session when available. */
|
|
51
|
+
getConnectionURI(): Promise<string>;
|
|
52
|
+
/** Tears down the existing pairing and rotates to a fresh channel/keys. */
|
|
53
|
+
newConnection(): Promise<string>;
|
|
54
|
+
isMobile(): boolean;
|
|
55
|
+
on(event: 'connect', listener: (info: {
|
|
56
|
+
chainId: string;
|
|
57
|
+
}) => void): unknown;
|
|
58
|
+
on(event: 'statusChanged', listener: (status: string) => void): unknown;
|
|
59
|
+
off(event: 'connect', listener: (info: {
|
|
60
|
+
chainId: string;
|
|
61
|
+
}) => void): unknown;
|
|
62
|
+
off(event: 'statusChanged', listener: (status: string) => void): unknown;
|
|
63
|
+
}
|
|
64
|
+
type PairingResult = 'connected' | 'cancelled' | 'redirected';
|
|
65
|
+
interface ShowPairingOptions {
|
|
66
|
+
/** Tear down any existing pairing and rotate channel/keys first. */
|
|
67
|
+
fresh?: boolean;
|
|
68
|
+
/** Shown in the dialog title. Default "MyQRLWallet". */
|
|
69
|
+
walletName?: string;
|
|
70
|
+
/** Get-the-wallet link under the title. Default https://myqrlwallet.com. */
|
|
71
|
+
walletUrl?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Base URL for the "Open web wallet" handoff (the pairing URI travels in
|
|
74
|
+
* the URL fragment). Default https://qrlwallet.com; pass '' to hide the
|
|
75
|
+
* web-wallet action.
|
|
76
|
+
*/
|
|
77
|
+
webWalletUrl?: string;
|
|
78
|
+
/** Where to mount the modal. Default document.body. */
|
|
79
|
+
container?: HTMLElement;
|
|
80
|
+
/**
|
|
81
|
+
* On mobile browsers navigate straight to the qrlconnect:// deep link
|
|
82
|
+
* instead of showing a QR (the wallet app opens in the foreground).
|
|
83
|
+
* Default true.
|
|
84
|
+
*/
|
|
85
|
+
mobileRedirect?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Shows the pairing dialog and resolves when the pairing reaches a
|
|
89
|
+
* terminal state: 'connected' (handshake completed), 'cancelled' (user
|
|
90
|
+
* dismissed the dialog), or 'redirected' (mobile deep-link navigation;
|
|
91
|
+
* the page is likely being backgrounded).
|
|
92
|
+
*/
|
|
93
|
+
declare function showPairingModal(provider: PairingProvider, options?: ShowPairingOptions): Promise<PairingResult>;
|
|
94
|
+
|
|
95
|
+
export { type PairingProvider, type PairingResult, QRL_PAIRING_MODAL_TAG, QrlPairingModal, type ShowPairingOptions, defineQrlPairingModal, showPairingModal };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
QRL_PAIRING_MODAL_TAG: () => QRL_PAIRING_MODAL_TAG,
|
|
34
|
+
QrlPairingModal: () => QrlPairingModal,
|
|
35
|
+
defineQrlPairingModal: () => defineQrlPairingModal,
|
|
36
|
+
showPairingModal: () => showPairingModal
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/qr.ts
|
|
41
|
+
var import_qrcode = __toESM(require("qrcode"));
|
|
42
|
+
async function qrSvg(text) {
|
|
43
|
+
return import_qrcode.default.toString(text, {
|
|
44
|
+
type: "svg",
|
|
45
|
+
margin: 2,
|
|
46
|
+
errorCorrectionLevel: "M",
|
|
47
|
+
color: { dark: "#000000", light: "#ffffff" }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/styles.ts
|
|
52
|
+
var modalStyles = `
|
|
53
|
+
:host {
|
|
54
|
+
--qrl-modal-accent: #f97316;
|
|
55
|
+
--qrl-modal-bg: #0f172a;
|
|
56
|
+
--qrl-modal-fg: #e2e8f0;
|
|
57
|
+
--qrl-modal-muted: #94a3b8;
|
|
58
|
+
--qrl-modal-link: #38bdf8;
|
|
59
|
+
--qrl-modal-border: rgba(148, 163, 184, 0.25);
|
|
60
|
+
--qrl-modal-radius: 12px;
|
|
61
|
+
--qrl-modal-backdrop: rgba(2, 6, 23, 0.8);
|
|
62
|
+
--qrl-modal-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
63
|
+
--qrl-modal-z: 2147483000;
|
|
64
|
+
}
|
|
65
|
+
.backdrop {
|
|
66
|
+
position: fixed;
|
|
67
|
+
inset: 0;
|
|
68
|
+
z-index: var(--qrl-modal-z);
|
|
69
|
+
display: grid;
|
|
70
|
+
place-items: center;
|
|
71
|
+
padding: 16px;
|
|
72
|
+
background: var(--qrl-modal-backdrop);
|
|
73
|
+
backdrop-filter: blur(4px);
|
|
74
|
+
}
|
|
75
|
+
.card {
|
|
76
|
+
width: 100%;
|
|
77
|
+
max-width: 24rem;
|
|
78
|
+
background: var(--qrl-modal-bg);
|
|
79
|
+
color: var(--qrl-modal-fg);
|
|
80
|
+
border: 1px solid var(--qrl-modal-border);
|
|
81
|
+
border-left: 2px solid var(--qrl-modal-accent);
|
|
82
|
+
border-radius: var(--qrl-modal-radius);
|
|
83
|
+
font-family: var(--qrl-modal-font);
|
|
84
|
+
padding: 20px;
|
|
85
|
+
box-sizing: border-box;
|
|
86
|
+
text-align: center;
|
|
87
|
+
outline: none;
|
|
88
|
+
}
|
|
89
|
+
h2 {
|
|
90
|
+
margin: 0 0 6px;
|
|
91
|
+
font-size: 1.125rem;
|
|
92
|
+
font-weight: 700;
|
|
93
|
+
}
|
|
94
|
+
.sub {
|
|
95
|
+
margin: 0 0 12px;
|
|
96
|
+
font-size: 0.75rem;
|
|
97
|
+
color: var(--qrl-modal-muted);
|
|
98
|
+
line-height: 1.5;
|
|
99
|
+
}
|
|
100
|
+
.sub a {
|
|
101
|
+
color: var(--qrl-modal-link);
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
}
|
|
104
|
+
.sub a:hover {
|
|
105
|
+
text-decoration: underline;
|
|
106
|
+
}
|
|
107
|
+
.qr {
|
|
108
|
+
display: grid;
|
|
109
|
+
place-items: center;
|
|
110
|
+
margin: 0 auto 12px;
|
|
111
|
+
width: 240px;
|
|
112
|
+
min-height: 240px;
|
|
113
|
+
background: #ffffff;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
padding: 8px;
|
|
116
|
+
color: #334155;
|
|
117
|
+
font-size: 0.8rem;
|
|
118
|
+
}
|
|
119
|
+
.qr svg {
|
|
120
|
+
display: block;
|
|
121
|
+
width: 240px;
|
|
122
|
+
height: 240px;
|
|
123
|
+
}
|
|
124
|
+
.status {
|
|
125
|
+
margin: 0 0 12px;
|
|
126
|
+
font-size: 0.75rem;
|
|
127
|
+
color: var(--qrl-modal-muted);
|
|
128
|
+
min-height: 1em;
|
|
129
|
+
}
|
|
130
|
+
.actions {
|
|
131
|
+
display: grid;
|
|
132
|
+
grid-template-columns: 1fr 1fr;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
margin-bottom: 12px;
|
|
135
|
+
}
|
|
136
|
+
.btn {
|
|
137
|
+
display: inline-flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: center;
|
|
140
|
+
gap: 6px;
|
|
141
|
+
padding: 8px 10px;
|
|
142
|
+
font-size: 0.8125rem;
|
|
143
|
+
font-weight: 500;
|
|
144
|
+
color: var(--qrl-modal-fg);
|
|
145
|
+
background: transparent;
|
|
146
|
+
border: 1px solid var(--qrl-modal-border);
|
|
147
|
+
border-radius: 8px;
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
text-decoration: none;
|
|
150
|
+
font-family: inherit;
|
|
151
|
+
}
|
|
152
|
+
.btn:hover {
|
|
153
|
+
border-color: var(--qrl-modal-accent);
|
|
154
|
+
}
|
|
155
|
+
.btn.wide {
|
|
156
|
+
grid-column: 1 / -1;
|
|
157
|
+
}
|
|
158
|
+
.btn[hidden] {
|
|
159
|
+
display: none;
|
|
160
|
+
}
|
|
161
|
+
.btn:focus-visible,
|
|
162
|
+
.link:focus-visible {
|
|
163
|
+
outline: 2px solid var(--qrl-modal-accent);
|
|
164
|
+
outline-offset: 2px;
|
|
165
|
+
}
|
|
166
|
+
.hint {
|
|
167
|
+
margin: 0 0 12px;
|
|
168
|
+
font-size: 0.75rem;
|
|
169
|
+
line-height: 1.6;
|
|
170
|
+
color: var(--qrl-modal-muted);
|
|
171
|
+
}
|
|
172
|
+
.links {
|
|
173
|
+
display: flex;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
gap: 16px;
|
|
176
|
+
}
|
|
177
|
+
.link {
|
|
178
|
+
display: inline-flex;
|
|
179
|
+
align-items: center;
|
|
180
|
+
gap: 5px;
|
|
181
|
+
padding: 0;
|
|
182
|
+
background: none;
|
|
183
|
+
border: none;
|
|
184
|
+
font: inherit;
|
|
185
|
+
font-size: 0.875rem;
|
|
186
|
+
color: var(--qrl-modal-link);
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
.link:hover {
|
|
190
|
+
text-decoration: underline;
|
|
191
|
+
}
|
|
192
|
+
.icon {
|
|
193
|
+
width: 14px;
|
|
194
|
+
height: 14px;
|
|
195
|
+
flex: none;
|
|
196
|
+
}
|
|
197
|
+
.icon svg {
|
|
198
|
+
display: block;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 100%;
|
|
201
|
+
}
|
|
202
|
+
`;
|
|
203
|
+
var iconSvg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
204
|
+
var ICON_EXTERNAL_LINK = iconSvg(
|
|
205
|
+
'<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>'
|
|
206
|
+
);
|
|
207
|
+
var ICON_COPY = iconSvg(
|
|
208
|
+
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'
|
|
209
|
+
);
|
|
210
|
+
var ICON_REFRESH = iconSvg(
|
|
211
|
+
'<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>'
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// src/element.ts
|
|
215
|
+
var QRL_PAIRING_MODAL_TAG = "qrl-pairing-modal";
|
|
216
|
+
var COPY_FEEDBACK_MS = 1500;
|
|
217
|
+
var isPairingUri = (uri) => uri.startsWith("qrlconnect:");
|
|
218
|
+
var isWebUrl = (url) => url.startsWith("https://") || url.startsWith("http://");
|
|
219
|
+
function makeIcon(svg) {
|
|
220
|
+
const span = document.createElement("span");
|
|
221
|
+
span.className = "icon";
|
|
222
|
+
span.innerHTML = svg;
|
|
223
|
+
return span;
|
|
224
|
+
}
|
|
225
|
+
var QrlPairingModal = class extends HTMLElement {
|
|
226
|
+
constructor() {
|
|
227
|
+
super();
|
|
228
|
+
this.qrToken = 0;
|
|
229
|
+
this.lastQrUri = null;
|
|
230
|
+
this.copyTimer = null;
|
|
231
|
+
this.restoreFocus = null;
|
|
232
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
233
|
+
const style = document.createElement("style");
|
|
234
|
+
style.textContent = modalStyles;
|
|
235
|
+
const backdrop = document.createElement("div");
|
|
236
|
+
backdrop.className = "backdrop";
|
|
237
|
+
backdrop.addEventListener("mousedown", (event) => {
|
|
238
|
+
if (event.target === backdrop) this.fire("qrl-cancel");
|
|
239
|
+
});
|
|
240
|
+
this.card = document.createElement("div");
|
|
241
|
+
this.card.className = "card";
|
|
242
|
+
this.card.setAttribute("role", "dialog");
|
|
243
|
+
this.card.setAttribute("aria-modal", "true");
|
|
244
|
+
this.card.setAttribute("aria-labelledby", "qrl-pairing-title");
|
|
245
|
+
this.card.tabIndex = -1;
|
|
246
|
+
this.titleEl = document.createElement("h2");
|
|
247
|
+
this.titleEl.id = "qrl-pairing-title";
|
|
248
|
+
const sub = document.createElement("p");
|
|
249
|
+
sub.className = "sub";
|
|
250
|
+
sub.append("Scan with the mobile app, or open the web wallet. Get the apps at ");
|
|
251
|
+
this.walletLink = document.createElement("a");
|
|
252
|
+
this.walletLink.target = "_blank";
|
|
253
|
+
this.walletLink.rel = "noreferrer";
|
|
254
|
+
sub.append(this.walletLink);
|
|
255
|
+
this.qrBox = document.createElement("div");
|
|
256
|
+
this.qrBox.className = "qr";
|
|
257
|
+
this.qrBox.setAttribute("aria-label", "qrlconnect pairing QR code");
|
|
258
|
+
this.statusEl = document.createElement("p");
|
|
259
|
+
this.statusEl.className = "status";
|
|
260
|
+
this.statusEl.setAttribute("aria-live", "polite");
|
|
261
|
+
const actions = document.createElement("div");
|
|
262
|
+
actions.className = "actions";
|
|
263
|
+
this.webLink = document.createElement("a");
|
|
264
|
+
this.webLink.className = "btn wide web";
|
|
265
|
+
this.webLink.target = "_blank";
|
|
266
|
+
this.webLink.rel = "noreferrer";
|
|
267
|
+
this.webLink.title = "Opens the web wallet in a new tab to approve this connection";
|
|
268
|
+
this.webLink.append(makeIcon(ICON_EXTERNAL_LINK), "Open web wallet");
|
|
269
|
+
this.openLink = document.createElement("a");
|
|
270
|
+
this.openLink.className = "btn desktop";
|
|
271
|
+
this.openLink.title = "Opens the MyQRLWallet desktop app if installed";
|
|
272
|
+
this.openLink.append(makeIcon(ICON_EXTERNAL_LINK), "Open desktop app");
|
|
273
|
+
const copyBtn = document.createElement("button");
|
|
274
|
+
copyBtn.type = "button";
|
|
275
|
+
copyBtn.className = "btn";
|
|
276
|
+
this.copyLabel = document.createElement("span");
|
|
277
|
+
this.copyLabel.textContent = "Copy code";
|
|
278
|
+
copyBtn.append(makeIcon(ICON_COPY), this.copyLabel);
|
|
279
|
+
copyBtn.addEventListener("click", () => {
|
|
280
|
+
this.copyUri();
|
|
281
|
+
});
|
|
282
|
+
actions.append(this.webLink, this.openLink, copyBtn);
|
|
283
|
+
const hint = document.createElement("p");
|
|
284
|
+
hint.className = "hint";
|
|
285
|
+
hint.textContent = "No protocol handler? Copy the code and paste it under dApp Sessions in the desktop or web wallet.";
|
|
286
|
+
const links = document.createElement("div");
|
|
287
|
+
links.className = "links";
|
|
288
|
+
const newBtn = document.createElement("button");
|
|
289
|
+
newBtn.type = "button";
|
|
290
|
+
newBtn.className = "link";
|
|
291
|
+
newBtn.append(makeIcon(ICON_REFRESH), "New connection");
|
|
292
|
+
newBtn.addEventListener("click", () => {
|
|
293
|
+
this.fire("qrl-new-connection");
|
|
294
|
+
});
|
|
295
|
+
const cancelBtn = document.createElement("button");
|
|
296
|
+
cancelBtn.type = "button";
|
|
297
|
+
cancelBtn.className = "link";
|
|
298
|
+
cancelBtn.textContent = "Cancel";
|
|
299
|
+
cancelBtn.addEventListener("click", () => {
|
|
300
|
+
this.fire("qrl-cancel");
|
|
301
|
+
});
|
|
302
|
+
links.append(newBtn, cancelBtn);
|
|
303
|
+
this.card.append(this.titleEl, sub, this.qrBox, this.statusEl, actions, hint, links);
|
|
304
|
+
backdrop.append(this.card);
|
|
305
|
+
this.shadow.append(style, backdrop);
|
|
306
|
+
this.addEventListener("keydown", (event) => {
|
|
307
|
+
this.handleKeydown(event);
|
|
308
|
+
});
|
|
309
|
+
this.syncAll();
|
|
310
|
+
}
|
|
311
|
+
static get observedAttributes() {
|
|
312
|
+
return ["uri", "status", "wallet-name", "wallet-url", "web-wallet-url"];
|
|
313
|
+
}
|
|
314
|
+
get uri() {
|
|
315
|
+
return this.getAttribute("uri");
|
|
316
|
+
}
|
|
317
|
+
set uri(value) {
|
|
318
|
+
if (value === null) this.removeAttribute("uri");
|
|
319
|
+
else this.setAttribute("uri", value);
|
|
320
|
+
}
|
|
321
|
+
attributeChangedCallback(name, _oldValue, _newValue) {
|
|
322
|
+
switch (name) {
|
|
323
|
+
case "uri":
|
|
324
|
+
this.syncUri();
|
|
325
|
+
this.syncWebWalletLink();
|
|
326
|
+
break;
|
|
327
|
+
case "status":
|
|
328
|
+
this.syncStatus();
|
|
329
|
+
break;
|
|
330
|
+
case "wallet-name":
|
|
331
|
+
case "wallet-url":
|
|
332
|
+
this.syncWallet();
|
|
333
|
+
break;
|
|
334
|
+
case "web-wallet-url":
|
|
335
|
+
this.syncWebWalletLink();
|
|
336
|
+
break;
|
|
337
|
+
default:
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
connectedCallback() {
|
|
342
|
+
const active = document.activeElement;
|
|
343
|
+
this.restoreFocus = active instanceof HTMLElement ? active : null;
|
|
344
|
+
queueMicrotask(() => {
|
|
345
|
+
if (this.isConnected) this.card.focus();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
disconnectedCallback() {
|
|
349
|
+
if (this.copyTimer !== null) clearTimeout(this.copyTimer);
|
|
350
|
+
this.copyTimer = null;
|
|
351
|
+
if (this.restoreFocus?.isConnected) this.restoreFocus.focus();
|
|
352
|
+
this.restoreFocus = null;
|
|
353
|
+
this.fire("qrl-cancel");
|
|
354
|
+
}
|
|
355
|
+
fire(name) {
|
|
356
|
+
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }));
|
|
357
|
+
}
|
|
358
|
+
syncAll() {
|
|
359
|
+
this.syncWallet();
|
|
360
|
+
this.syncStatus();
|
|
361
|
+
this.syncUri();
|
|
362
|
+
this.syncWebWalletLink();
|
|
363
|
+
}
|
|
364
|
+
syncWallet() {
|
|
365
|
+
const name = this.getAttribute("wallet-name") ?? "MyQRLWallet";
|
|
366
|
+
const url = this.getAttribute("wallet-url") ?? "https://myqrlwallet.com";
|
|
367
|
+
this.titleEl.textContent = `Pair ${name}`;
|
|
368
|
+
this.walletLink.textContent = url.replace(/^https?:\/\//, "");
|
|
369
|
+
if (isWebUrl(url)) this.walletLink.setAttribute("href", url);
|
|
370
|
+
else this.walletLink.removeAttribute("href");
|
|
371
|
+
}
|
|
372
|
+
syncStatus() {
|
|
373
|
+
const status = this.getAttribute("status");
|
|
374
|
+
this.statusEl.textContent = status !== null && status !== "" ? `status: ${status}` : "";
|
|
375
|
+
}
|
|
376
|
+
syncUri() {
|
|
377
|
+
const uri = this.getAttribute("uri");
|
|
378
|
+
if (uri !== null && isPairingUri(uri)) this.openLink.setAttribute("href", uri);
|
|
379
|
+
else this.openLink.removeAttribute("href");
|
|
380
|
+
if (uri === this.lastQrUri) return;
|
|
381
|
+
this.lastQrUri = uri;
|
|
382
|
+
this.renderQr(uri);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The web-wallet handoff: an https link carrying the pairing URI in the
|
|
386
|
+
* URL fragment (never sent to servers; the wallet scrubs and stages it
|
|
387
|
+
* behind its consent modal). Attribute absent = default web wallet;
|
|
388
|
+
* attribute set but empty = feature disabled. Hidden links carry no href,
|
|
389
|
+
* which also keeps them out of the focus trap.
|
|
390
|
+
*/
|
|
391
|
+
syncWebWalletLink() {
|
|
392
|
+
const attr = this.getAttribute("web-wallet-url");
|
|
393
|
+
const base = attr ?? "https://qrlwallet.com";
|
|
394
|
+
const uri = this.getAttribute("uri");
|
|
395
|
+
const enabled = base !== "" && isWebUrl(base) && uri !== null && isPairingUri(uri);
|
|
396
|
+
if (!enabled) {
|
|
397
|
+
this.webLink.removeAttribute("href");
|
|
398
|
+
this.webLink.hidden = true;
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.webLink.hidden = false;
|
|
402
|
+
this.webLink.setAttribute(
|
|
403
|
+
"href",
|
|
404
|
+
`${base.replace(/\/+$/, "")}/dapp-sessions#qrlconnect=${encodeURIComponent(uri)}`
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
renderQr(uri) {
|
|
408
|
+
const token = ++this.qrToken;
|
|
409
|
+
if (uri === null || uri === "") {
|
|
410
|
+
this.qrBox.textContent = "";
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
this.qrBox.textContent = "Generating\u2026";
|
|
414
|
+
void qrSvg(uri).then((svg) => {
|
|
415
|
+
if (token !== this.qrToken) return;
|
|
416
|
+
this.qrBox.innerHTML = svg;
|
|
417
|
+
}).catch(() => {
|
|
418
|
+
if (token === this.qrToken) this.qrBox.textContent = "Could not render the QR code";
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
copyUri() {
|
|
422
|
+
const uri = this.getAttribute("uri") ?? "";
|
|
423
|
+
const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
|
|
424
|
+
if (uri === "" || clipboard === void 0) {
|
|
425
|
+
this.showCopyFeedback("Copy failed");
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
void clipboard.writeText(uri).then(() => {
|
|
429
|
+
this.showCopyFeedback("Copied!");
|
|
430
|
+
}).catch(() => {
|
|
431
|
+
this.showCopyFeedback("Copy failed");
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
showCopyFeedback(text) {
|
|
435
|
+
this.copyLabel.textContent = text;
|
|
436
|
+
if (this.copyTimer !== null) clearTimeout(this.copyTimer);
|
|
437
|
+
this.copyTimer = setTimeout(() => {
|
|
438
|
+
this.copyLabel.textContent = "Copy code";
|
|
439
|
+
this.copyTimer = null;
|
|
440
|
+
}, COPY_FEEDBACK_MS);
|
|
441
|
+
}
|
|
442
|
+
focusables() {
|
|
443
|
+
const nodes = this.shadow.querySelectorAll("a[href], button");
|
|
444
|
+
const result = [];
|
|
445
|
+
nodes.forEach((node) => {
|
|
446
|
+
if (node instanceof HTMLElement) result.push(node);
|
|
447
|
+
});
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
handleKeydown(event) {
|
|
451
|
+
if (event.key === "Escape") {
|
|
452
|
+
event.stopPropagation();
|
|
453
|
+
this.fire("qrl-cancel");
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
if (event.key !== "Tab") return;
|
|
457
|
+
const items = this.focusables();
|
|
458
|
+
const first = items[0];
|
|
459
|
+
const last = items[items.length - 1];
|
|
460
|
+
if (first === void 0 || last === void 0) return;
|
|
461
|
+
const active = this.shadow.activeElement;
|
|
462
|
+
if (event.shiftKey && (active === first || active === this.card)) {
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
last.focus();
|
|
465
|
+
} else if (!event.shiftKey && active === last) {
|
|
466
|
+
event.preventDefault();
|
|
467
|
+
first.focus();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
function defineQrlPairingModal(tagName = QRL_PAIRING_MODAL_TAG) {
|
|
472
|
+
if (typeof customElements === "undefined") return;
|
|
473
|
+
if (customElements.get(tagName) === void 0) customElements.define(tagName, QrlPairingModal);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/show.ts
|
|
477
|
+
async function showPairingModal(provider, options = {}) {
|
|
478
|
+
const uri = options.fresh === true ? await provider.newConnection() : await provider.getConnectionURI();
|
|
479
|
+
if ((options.mobileRedirect ?? true) && provider.isMobile()) {
|
|
480
|
+
if (!isPairingUri(uri)) {
|
|
481
|
+
throw new Error("refusing to redirect: pairing URI does not use the qrlconnect: scheme");
|
|
482
|
+
}
|
|
483
|
+
window.location.href = uri;
|
|
484
|
+
return "redirected";
|
|
485
|
+
}
|
|
486
|
+
defineQrlPairingModal();
|
|
487
|
+
const modal = new QrlPairingModal();
|
|
488
|
+
modal.setAttribute("uri", uri);
|
|
489
|
+
if (options.walletName !== void 0) modal.setAttribute("wallet-name", options.walletName);
|
|
490
|
+
if (options.walletUrl !== void 0) modal.setAttribute("wallet-url", options.walletUrl);
|
|
491
|
+
if (options.webWalletUrl !== void 0) {
|
|
492
|
+
modal.setAttribute("web-wallet-url", options.webWalletUrl);
|
|
493
|
+
}
|
|
494
|
+
const container = options.container ?? document.body;
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
let settled = false;
|
|
497
|
+
const onStatus = (status) => {
|
|
498
|
+
modal.setAttribute("status", status);
|
|
499
|
+
};
|
|
500
|
+
const onConnect = () => {
|
|
501
|
+
finish("connected");
|
|
502
|
+
};
|
|
503
|
+
function finish(result) {
|
|
504
|
+
if (settled) return;
|
|
505
|
+
settled = true;
|
|
506
|
+
provider.off("connect", onConnect);
|
|
507
|
+
provider.off("statusChanged", onStatus);
|
|
508
|
+
modal.remove();
|
|
509
|
+
resolve(result);
|
|
510
|
+
}
|
|
511
|
+
modal.addEventListener("qrl-cancel", () => {
|
|
512
|
+
finish("cancelled");
|
|
513
|
+
});
|
|
514
|
+
modal.addEventListener("qrl-new-connection", () => {
|
|
515
|
+
modal.setAttribute("status", "rotating\u2026");
|
|
516
|
+
void provider.newConnection().then((fresh) => {
|
|
517
|
+
if (!settled) modal.setAttribute("uri", fresh);
|
|
518
|
+
}).catch((err) => {
|
|
519
|
+
if (!settled) {
|
|
520
|
+
modal.setAttribute(
|
|
521
|
+
"status",
|
|
522
|
+
err instanceof Error ? err.message : "could not rotate the connection"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
provider.on("statusChanged", onStatus);
|
|
528
|
+
provider.on("connect", onConnect);
|
|
529
|
+
container.appendChild(modal);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
533
|
+
0 && (module.exports = {
|
|
534
|
+
QRL_PAIRING_MODAL_TAG,
|
|
535
|
+
QrlPairingModal,
|
|
536
|
+
defineQrlPairingModal,
|
|
537
|
+
showPairingModal
|
|
538
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
// src/qr.ts
|
|
2
|
+
import QRCode from "qrcode";
|
|
3
|
+
async function qrSvg(text) {
|
|
4
|
+
return QRCode.toString(text, {
|
|
5
|
+
type: "svg",
|
|
6
|
+
margin: 2,
|
|
7
|
+
errorCorrectionLevel: "M",
|
|
8
|
+
color: { dark: "#000000", light: "#ffffff" }
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/styles.ts
|
|
13
|
+
var modalStyles = `
|
|
14
|
+
:host {
|
|
15
|
+
--qrl-modal-accent: #f97316;
|
|
16
|
+
--qrl-modal-bg: #0f172a;
|
|
17
|
+
--qrl-modal-fg: #e2e8f0;
|
|
18
|
+
--qrl-modal-muted: #94a3b8;
|
|
19
|
+
--qrl-modal-link: #38bdf8;
|
|
20
|
+
--qrl-modal-border: rgba(148, 163, 184, 0.25);
|
|
21
|
+
--qrl-modal-radius: 12px;
|
|
22
|
+
--qrl-modal-backdrop: rgba(2, 6, 23, 0.8);
|
|
23
|
+
--qrl-modal-font: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
24
|
+
--qrl-modal-z: 2147483000;
|
|
25
|
+
}
|
|
26
|
+
.backdrop {
|
|
27
|
+
position: fixed;
|
|
28
|
+
inset: 0;
|
|
29
|
+
z-index: var(--qrl-modal-z);
|
|
30
|
+
display: grid;
|
|
31
|
+
place-items: center;
|
|
32
|
+
padding: 16px;
|
|
33
|
+
background: var(--qrl-modal-backdrop);
|
|
34
|
+
backdrop-filter: blur(4px);
|
|
35
|
+
}
|
|
36
|
+
.card {
|
|
37
|
+
width: 100%;
|
|
38
|
+
max-width: 24rem;
|
|
39
|
+
background: var(--qrl-modal-bg);
|
|
40
|
+
color: var(--qrl-modal-fg);
|
|
41
|
+
border: 1px solid var(--qrl-modal-border);
|
|
42
|
+
border-left: 2px solid var(--qrl-modal-accent);
|
|
43
|
+
border-radius: var(--qrl-modal-radius);
|
|
44
|
+
font-family: var(--qrl-modal-font);
|
|
45
|
+
padding: 20px;
|
|
46
|
+
box-sizing: border-box;
|
|
47
|
+
text-align: center;
|
|
48
|
+
outline: none;
|
|
49
|
+
}
|
|
50
|
+
h2 {
|
|
51
|
+
margin: 0 0 6px;
|
|
52
|
+
font-size: 1.125rem;
|
|
53
|
+
font-weight: 700;
|
|
54
|
+
}
|
|
55
|
+
.sub {
|
|
56
|
+
margin: 0 0 12px;
|
|
57
|
+
font-size: 0.75rem;
|
|
58
|
+
color: var(--qrl-modal-muted);
|
|
59
|
+
line-height: 1.5;
|
|
60
|
+
}
|
|
61
|
+
.sub a {
|
|
62
|
+
color: var(--qrl-modal-link);
|
|
63
|
+
text-decoration: none;
|
|
64
|
+
}
|
|
65
|
+
.sub a:hover {
|
|
66
|
+
text-decoration: underline;
|
|
67
|
+
}
|
|
68
|
+
.qr {
|
|
69
|
+
display: grid;
|
|
70
|
+
place-items: center;
|
|
71
|
+
margin: 0 auto 12px;
|
|
72
|
+
width: 240px;
|
|
73
|
+
min-height: 240px;
|
|
74
|
+
background: #ffffff;
|
|
75
|
+
border-radius: 8px;
|
|
76
|
+
padding: 8px;
|
|
77
|
+
color: #334155;
|
|
78
|
+
font-size: 0.8rem;
|
|
79
|
+
}
|
|
80
|
+
.qr svg {
|
|
81
|
+
display: block;
|
|
82
|
+
width: 240px;
|
|
83
|
+
height: 240px;
|
|
84
|
+
}
|
|
85
|
+
.status {
|
|
86
|
+
margin: 0 0 12px;
|
|
87
|
+
font-size: 0.75rem;
|
|
88
|
+
color: var(--qrl-modal-muted);
|
|
89
|
+
min-height: 1em;
|
|
90
|
+
}
|
|
91
|
+
.actions {
|
|
92
|
+
display: grid;
|
|
93
|
+
grid-template-columns: 1fr 1fr;
|
|
94
|
+
gap: 8px;
|
|
95
|
+
margin-bottom: 12px;
|
|
96
|
+
}
|
|
97
|
+
.btn {
|
|
98
|
+
display: inline-flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
gap: 6px;
|
|
102
|
+
padding: 8px 10px;
|
|
103
|
+
font-size: 0.8125rem;
|
|
104
|
+
font-weight: 500;
|
|
105
|
+
color: var(--qrl-modal-fg);
|
|
106
|
+
background: transparent;
|
|
107
|
+
border: 1px solid var(--qrl-modal-border);
|
|
108
|
+
border-radius: 8px;
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
text-decoration: none;
|
|
111
|
+
font-family: inherit;
|
|
112
|
+
}
|
|
113
|
+
.btn:hover {
|
|
114
|
+
border-color: var(--qrl-modal-accent);
|
|
115
|
+
}
|
|
116
|
+
.btn.wide {
|
|
117
|
+
grid-column: 1 / -1;
|
|
118
|
+
}
|
|
119
|
+
.btn[hidden] {
|
|
120
|
+
display: none;
|
|
121
|
+
}
|
|
122
|
+
.btn:focus-visible,
|
|
123
|
+
.link:focus-visible {
|
|
124
|
+
outline: 2px solid var(--qrl-modal-accent);
|
|
125
|
+
outline-offset: 2px;
|
|
126
|
+
}
|
|
127
|
+
.hint {
|
|
128
|
+
margin: 0 0 12px;
|
|
129
|
+
font-size: 0.75rem;
|
|
130
|
+
line-height: 1.6;
|
|
131
|
+
color: var(--qrl-modal-muted);
|
|
132
|
+
}
|
|
133
|
+
.links {
|
|
134
|
+
display: flex;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
gap: 16px;
|
|
137
|
+
}
|
|
138
|
+
.link {
|
|
139
|
+
display: inline-flex;
|
|
140
|
+
align-items: center;
|
|
141
|
+
gap: 5px;
|
|
142
|
+
padding: 0;
|
|
143
|
+
background: none;
|
|
144
|
+
border: none;
|
|
145
|
+
font: inherit;
|
|
146
|
+
font-size: 0.875rem;
|
|
147
|
+
color: var(--qrl-modal-link);
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
.link:hover {
|
|
151
|
+
text-decoration: underline;
|
|
152
|
+
}
|
|
153
|
+
.icon {
|
|
154
|
+
width: 14px;
|
|
155
|
+
height: 14px;
|
|
156
|
+
flex: none;
|
|
157
|
+
}
|
|
158
|
+
.icon svg {
|
|
159
|
+
display: block;
|
|
160
|
+
width: 100%;
|
|
161
|
+
height: 100%;
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
var iconSvg = (body) => `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
165
|
+
var ICON_EXTERNAL_LINK = iconSvg(
|
|
166
|
+
'<path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>'
|
|
167
|
+
);
|
|
168
|
+
var ICON_COPY = iconSvg(
|
|
169
|
+
'<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'
|
|
170
|
+
);
|
|
171
|
+
var ICON_REFRESH = iconSvg(
|
|
172
|
+
'<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/>'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// src/element.ts
|
|
176
|
+
var QRL_PAIRING_MODAL_TAG = "qrl-pairing-modal";
|
|
177
|
+
var COPY_FEEDBACK_MS = 1500;
|
|
178
|
+
var isPairingUri = (uri) => uri.startsWith("qrlconnect:");
|
|
179
|
+
var isWebUrl = (url) => url.startsWith("https://") || url.startsWith("http://");
|
|
180
|
+
function makeIcon(svg) {
|
|
181
|
+
const span = document.createElement("span");
|
|
182
|
+
span.className = "icon";
|
|
183
|
+
span.innerHTML = svg;
|
|
184
|
+
return span;
|
|
185
|
+
}
|
|
186
|
+
var QrlPairingModal = class extends HTMLElement {
|
|
187
|
+
constructor() {
|
|
188
|
+
super();
|
|
189
|
+
this.qrToken = 0;
|
|
190
|
+
this.lastQrUri = null;
|
|
191
|
+
this.copyTimer = null;
|
|
192
|
+
this.restoreFocus = null;
|
|
193
|
+
this.shadow = this.attachShadow({ mode: "open" });
|
|
194
|
+
const style = document.createElement("style");
|
|
195
|
+
style.textContent = modalStyles;
|
|
196
|
+
const backdrop = document.createElement("div");
|
|
197
|
+
backdrop.className = "backdrop";
|
|
198
|
+
backdrop.addEventListener("mousedown", (event) => {
|
|
199
|
+
if (event.target === backdrop) this.fire("qrl-cancel");
|
|
200
|
+
});
|
|
201
|
+
this.card = document.createElement("div");
|
|
202
|
+
this.card.className = "card";
|
|
203
|
+
this.card.setAttribute("role", "dialog");
|
|
204
|
+
this.card.setAttribute("aria-modal", "true");
|
|
205
|
+
this.card.setAttribute("aria-labelledby", "qrl-pairing-title");
|
|
206
|
+
this.card.tabIndex = -1;
|
|
207
|
+
this.titleEl = document.createElement("h2");
|
|
208
|
+
this.titleEl.id = "qrl-pairing-title";
|
|
209
|
+
const sub = document.createElement("p");
|
|
210
|
+
sub.className = "sub";
|
|
211
|
+
sub.append("Scan with the mobile app, or open the web wallet. Get the apps at ");
|
|
212
|
+
this.walletLink = document.createElement("a");
|
|
213
|
+
this.walletLink.target = "_blank";
|
|
214
|
+
this.walletLink.rel = "noreferrer";
|
|
215
|
+
sub.append(this.walletLink);
|
|
216
|
+
this.qrBox = document.createElement("div");
|
|
217
|
+
this.qrBox.className = "qr";
|
|
218
|
+
this.qrBox.setAttribute("aria-label", "qrlconnect pairing QR code");
|
|
219
|
+
this.statusEl = document.createElement("p");
|
|
220
|
+
this.statusEl.className = "status";
|
|
221
|
+
this.statusEl.setAttribute("aria-live", "polite");
|
|
222
|
+
const actions = document.createElement("div");
|
|
223
|
+
actions.className = "actions";
|
|
224
|
+
this.webLink = document.createElement("a");
|
|
225
|
+
this.webLink.className = "btn wide web";
|
|
226
|
+
this.webLink.target = "_blank";
|
|
227
|
+
this.webLink.rel = "noreferrer";
|
|
228
|
+
this.webLink.title = "Opens the web wallet in a new tab to approve this connection";
|
|
229
|
+
this.webLink.append(makeIcon(ICON_EXTERNAL_LINK), "Open web wallet");
|
|
230
|
+
this.openLink = document.createElement("a");
|
|
231
|
+
this.openLink.className = "btn desktop";
|
|
232
|
+
this.openLink.title = "Opens the MyQRLWallet desktop app if installed";
|
|
233
|
+
this.openLink.append(makeIcon(ICON_EXTERNAL_LINK), "Open desktop app");
|
|
234
|
+
const copyBtn = document.createElement("button");
|
|
235
|
+
copyBtn.type = "button";
|
|
236
|
+
copyBtn.className = "btn";
|
|
237
|
+
this.copyLabel = document.createElement("span");
|
|
238
|
+
this.copyLabel.textContent = "Copy code";
|
|
239
|
+
copyBtn.append(makeIcon(ICON_COPY), this.copyLabel);
|
|
240
|
+
copyBtn.addEventListener("click", () => {
|
|
241
|
+
this.copyUri();
|
|
242
|
+
});
|
|
243
|
+
actions.append(this.webLink, this.openLink, copyBtn);
|
|
244
|
+
const hint = document.createElement("p");
|
|
245
|
+
hint.className = "hint";
|
|
246
|
+
hint.textContent = "No protocol handler? Copy the code and paste it under dApp Sessions in the desktop or web wallet.";
|
|
247
|
+
const links = document.createElement("div");
|
|
248
|
+
links.className = "links";
|
|
249
|
+
const newBtn = document.createElement("button");
|
|
250
|
+
newBtn.type = "button";
|
|
251
|
+
newBtn.className = "link";
|
|
252
|
+
newBtn.append(makeIcon(ICON_REFRESH), "New connection");
|
|
253
|
+
newBtn.addEventListener("click", () => {
|
|
254
|
+
this.fire("qrl-new-connection");
|
|
255
|
+
});
|
|
256
|
+
const cancelBtn = document.createElement("button");
|
|
257
|
+
cancelBtn.type = "button";
|
|
258
|
+
cancelBtn.className = "link";
|
|
259
|
+
cancelBtn.textContent = "Cancel";
|
|
260
|
+
cancelBtn.addEventListener("click", () => {
|
|
261
|
+
this.fire("qrl-cancel");
|
|
262
|
+
});
|
|
263
|
+
links.append(newBtn, cancelBtn);
|
|
264
|
+
this.card.append(this.titleEl, sub, this.qrBox, this.statusEl, actions, hint, links);
|
|
265
|
+
backdrop.append(this.card);
|
|
266
|
+
this.shadow.append(style, backdrop);
|
|
267
|
+
this.addEventListener("keydown", (event) => {
|
|
268
|
+
this.handleKeydown(event);
|
|
269
|
+
});
|
|
270
|
+
this.syncAll();
|
|
271
|
+
}
|
|
272
|
+
static get observedAttributes() {
|
|
273
|
+
return ["uri", "status", "wallet-name", "wallet-url", "web-wallet-url"];
|
|
274
|
+
}
|
|
275
|
+
get uri() {
|
|
276
|
+
return this.getAttribute("uri");
|
|
277
|
+
}
|
|
278
|
+
set uri(value) {
|
|
279
|
+
if (value === null) this.removeAttribute("uri");
|
|
280
|
+
else this.setAttribute("uri", value);
|
|
281
|
+
}
|
|
282
|
+
attributeChangedCallback(name, _oldValue, _newValue) {
|
|
283
|
+
switch (name) {
|
|
284
|
+
case "uri":
|
|
285
|
+
this.syncUri();
|
|
286
|
+
this.syncWebWalletLink();
|
|
287
|
+
break;
|
|
288
|
+
case "status":
|
|
289
|
+
this.syncStatus();
|
|
290
|
+
break;
|
|
291
|
+
case "wallet-name":
|
|
292
|
+
case "wallet-url":
|
|
293
|
+
this.syncWallet();
|
|
294
|
+
break;
|
|
295
|
+
case "web-wallet-url":
|
|
296
|
+
this.syncWebWalletLink();
|
|
297
|
+
break;
|
|
298
|
+
default:
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
connectedCallback() {
|
|
303
|
+
const active = document.activeElement;
|
|
304
|
+
this.restoreFocus = active instanceof HTMLElement ? active : null;
|
|
305
|
+
queueMicrotask(() => {
|
|
306
|
+
if (this.isConnected) this.card.focus();
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
disconnectedCallback() {
|
|
310
|
+
if (this.copyTimer !== null) clearTimeout(this.copyTimer);
|
|
311
|
+
this.copyTimer = null;
|
|
312
|
+
if (this.restoreFocus?.isConnected) this.restoreFocus.focus();
|
|
313
|
+
this.restoreFocus = null;
|
|
314
|
+
this.fire("qrl-cancel");
|
|
315
|
+
}
|
|
316
|
+
fire(name) {
|
|
317
|
+
this.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true }));
|
|
318
|
+
}
|
|
319
|
+
syncAll() {
|
|
320
|
+
this.syncWallet();
|
|
321
|
+
this.syncStatus();
|
|
322
|
+
this.syncUri();
|
|
323
|
+
this.syncWebWalletLink();
|
|
324
|
+
}
|
|
325
|
+
syncWallet() {
|
|
326
|
+
const name = this.getAttribute("wallet-name") ?? "MyQRLWallet";
|
|
327
|
+
const url = this.getAttribute("wallet-url") ?? "https://myqrlwallet.com";
|
|
328
|
+
this.titleEl.textContent = `Pair ${name}`;
|
|
329
|
+
this.walletLink.textContent = url.replace(/^https?:\/\//, "");
|
|
330
|
+
if (isWebUrl(url)) this.walletLink.setAttribute("href", url);
|
|
331
|
+
else this.walletLink.removeAttribute("href");
|
|
332
|
+
}
|
|
333
|
+
syncStatus() {
|
|
334
|
+
const status = this.getAttribute("status");
|
|
335
|
+
this.statusEl.textContent = status !== null && status !== "" ? `status: ${status}` : "";
|
|
336
|
+
}
|
|
337
|
+
syncUri() {
|
|
338
|
+
const uri = this.getAttribute("uri");
|
|
339
|
+
if (uri !== null && isPairingUri(uri)) this.openLink.setAttribute("href", uri);
|
|
340
|
+
else this.openLink.removeAttribute("href");
|
|
341
|
+
if (uri === this.lastQrUri) return;
|
|
342
|
+
this.lastQrUri = uri;
|
|
343
|
+
this.renderQr(uri);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The web-wallet handoff: an https link carrying the pairing URI in the
|
|
347
|
+
* URL fragment (never sent to servers; the wallet scrubs and stages it
|
|
348
|
+
* behind its consent modal). Attribute absent = default web wallet;
|
|
349
|
+
* attribute set but empty = feature disabled. Hidden links carry no href,
|
|
350
|
+
* which also keeps them out of the focus trap.
|
|
351
|
+
*/
|
|
352
|
+
syncWebWalletLink() {
|
|
353
|
+
const attr = this.getAttribute("web-wallet-url");
|
|
354
|
+
const base = attr ?? "https://qrlwallet.com";
|
|
355
|
+
const uri = this.getAttribute("uri");
|
|
356
|
+
const enabled = base !== "" && isWebUrl(base) && uri !== null && isPairingUri(uri);
|
|
357
|
+
if (!enabled) {
|
|
358
|
+
this.webLink.removeAttribute("href");
|
|
359
|
+
this.webLink.hidden = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
this.webLink.hidden = false;
|
|
363
|
+
this.webLink.setAttribute(
|
|
364
|
+
"href",
|
|
365
|
+
`${base.replace(/\/+$/, "")}/dapp-sessions#qrlconnect=${encodeURIComponent(uri)}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
renderQr(uri) {
|
|
369
|
+
const token = ++this.qrToken;
|
|
370
|
+
if (uri === null || uri === "") {
|
|
371
|
+
this.qrBox.textContent = "";
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
this.qrBox.textContent = "Generating\u2026";
|
|
375
|
+
void qrSvg(uri).then((svg) => {
|
|
376
|
+
if (token !== this.qrToken) return;
|
|
377
|
+
this.qrBox.innerHTML = svg;
|
|
378
|
+
}).catch(() => {
|
|
379
|
+
if (token === this.qrToken) this.qrBox.textContent = "Could not render the QR code";
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
copyUri() {
|
|
383
|
+
const uri = this.getAttribute("uri") ?? "";
|
|
384
|
+
const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
|
|
385
|
+
if (uri === "" || clipboard === void 0) {
|
|
386
|
+
this.showCopyFeedback("Copy failed");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
void clipboard.writeText(uri).then(() => {
|
|
390
|
+
this.showCopyFeedback("Copied!");
|
|
391
|
+
}).catch(() => {
|
|
392
|
+
this.showCopyFeedback("Copy failed");
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
showCopyFeedback(text) {
|
|
396
|
+
this.copyLabel.textContent = text;
|
|
397
|
+
if (this.copyTimer !== null) clearTimeout(this.copyTimer);
|
|
398
|
+
this.copyTimer = setTimeout(() => {
|
|
399
|
+
this.copyLabel.textContent = "Copy code";
|
|
400
|
+
this.copyTimer = null;
|
|
401
|
+
}, COPY_FEEDBACK_MS);
|
|
402
|
+
}
|
|
403
|
+
focusables() {
|
|
404
|
+
const nodes = this.shadow.querySelectorAll("a[href], button");
|
|
405
|
+
const result = [];
|
|
406
|
+
nodes.forEach((node) => {
|
|
407
|
+
if (node instanceof HTMLElement) result.push(node);
|
|
408
|
+
});
|
|
409
|
+
return result;
|
|
410
|
+
}
|
|
411
|
+
handleKeydown(event) {
|
|
412
|
+
if (event.key === "Escape") {
|
|
413
|
+
event.stopPropagation();
|
|
414
|
+
this.fire("qrl-cancel");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (event.key !== "Tab") return;
|
|
418
|
+
const items = this.focusables();
|
|
419
|
+
const first = items[0];
|
|
420
|
+
const last = items[items.length - 1];
|
|
421
|
+
if (first === void 0 || last === void 0) return;
|
|
422
|
+
const active = this.shadow.activeElement;
|
|
423
|
+
if (event.shiftKey && (active === first || active === this.card)) {
|
|
424
|
+
event.preventDefault();
|
|
425
|
+
last.focus();
|
|
426
|
+
} else if (!event.shiftKey && active === last) {
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
first.focus();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
function defineQrlPairingModal(tagName = QRL_PAIRING_MODAL_TAG) {
|
|
433
|
+
if (typeof customElements === "undefined") return;
|
|
434
|
+
if (customElements.get(tagName) === void 0) customElements.define(tagName, QrlPairingModal);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/show.ts
|
|
438
|
+
async function showPairingModal(provider, options = {}) {
|
|
439
|
+
const uri = options.fresh === true ? await provider.newConnection() : await provider.getConnectionURI();
|
|
440
|
+
if ((options.mobileRedirect ?? true) && provider.isMobile()) {
|
|
441
|
+
if (!isPairingUri(uri)) {
|
|
442
|
+
throw new Error("refusing to redirect: pairing URI does not use the qrlconnect: scheme");
|
|
443
|
+
}
|
|
444
|
+
window.location.href = uri;
|
|
445
|
+
return "redirected";
|
|
446
|
+
}
|
|
447
|
+
defineQrlPairingModal();
|
|
448
|
+
const modal = new QrlPairingModal();
|
|
449
|
+
modal.setAttribute("uri", uri);
|
|
450
|
+
if (options.walletName !== void 0) modal.setAttribute("wallet-name", options.walletName);
|
|
451
|
+
if (options.walletUrl !== void 0) modal.setAttribute("wallet-url", options.walletUrl);
|
|
452
|
+
if (options.webWalletUrl !== void 0) {
|
|
453
|
+
modal.setAttribute("web-wallet-url", options.webWalletUrl);
|
|
454
|
+
}
|
|
455
|
+
const container = options.container ?? document.body;
|
|
456
|
+
return new Promise((resolve) => {
|
|
457
|
+
let settled = false;
|
|
458
|
+
const onStatus = (status) => {
|
|
459
|
+
modal.setAttribute("status", status);
|
|
460
|
+
};
|
|
461
|
+
const onConnect = () => {
|
|
462
|
+
finish("connected");
|
|
463
|
+
};
|
|
464
|
+
function finish(result) {
|
|
465
|
+
if (settled) return;
|
|
466
|
+
settled = true;
|
|
467
|
+
provider.off("connect", onConnect);
|
|
468
|
+
provider.off("statusChanged", onStatus);
|
|
469
|
+
modal.remove();
|
|
470
|
+
resolve(result);
|
|
471
|
+
}
|
|
472
|
+
modal.addEventListener("qrl-cancel", () => {
|
|
473
|
+
finish("cancelled");
|
|
474
|
+
});
|
|
475
|
+
modal.addEventListener("qrl-new-connection", () => {
|
|
476
|
+
modal.setAttribute("status", "rotating\u2026");
|
|
477
|
+
void provider.newConnection().then((fresh) => {
|
|
478
|
+
if (!settled) modal.setAttribute("uri", fresh);
|
|
479
|
+
}).catch((err) => {
|
|
480
|
+
if (!settled) {
|
|
481
|
+
modal.setAttribute(
|
|
482
|
+
"status",
|
|
483
|
+
err instanceof Error ? err.message : "could not rotate the connection"
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
provider.on("statusChanged", onStatus);
|
|
489
|
+
provider.on("connect", onConnect);
|
|
490
|
+
container.appendChild(modal);
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
export {
|
|
494
|
+
QRL_PAIRING_MODAL_TAG,
|
|
495
|
+
QrlPairingModal,
|
|
496
|
+
defineQrlPairingModal,
|
|
497
|
+
showPairingModal
|
|
498
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@qrlwallet/connect-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-agnostic pairing modal for @qrlwallet/connect: <qrl-pairing-modal> web component plus a one-line showPairingModal() helper",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"lint": "eslint src/ test/ --ext .ts",
|
|
18
|
+
"lint:fix": "eslint src/ test/ --ext .ts --fix",
|
|
19
|
+
"format": "prettier --write src/ test/",
|
|
20
|
+
"format:check": "prettier --check src/ test/",
|
|
21
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm test && npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"qrl",
|
|
25
|
+
"zond",
|
|
26
|
+
"wallet",
|
|
27
|
+
"connect",
|
|
28
|
+
"dapp",
|
|
29
|
+
"qr-code",
|
|
30
|
+
"web-component",
|
|
31
|
+
"modal"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/DigitalGuards/myqrlwallet-connect.git",
|
|
37
|
+
"directory": "ui"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"qrcode": "^1.5.4"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"@qrlwallet/connect": ">=3.1.0 <4"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"@qrlwallet/connect": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@qrlwallet/connect": "file:..",
|
|
55
|
+
"@types/qrcode": "^1.5.5",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
57
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
58
|
+
"eslint": "^8.57.0",
|
|
59
|
+
"eslint-config-prettier": "^9.1.0",
|
|
60
|
+
"jsdom": "^25.0.1",
|
|
61
|
+
"prettier": "^3.4.0",
|
|
62
|
+
"tsup": "^8.4.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vitest": "^3.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|