@qrkit/react 0.0.1 → 0.2.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 +116 -20
- package/dist/index.cjs +491 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +135 -1
- package/dist/index.d.ts +135 -1
- package/dist/index.js +454 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +183 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
React context, hooks, and drop-in components for QR-based airgapped wallet flows.
|
|
4
4
|
|
|
5
|
-
Builds on [`@qrkit/core`](../core). Provides a provider/context
|
|
5
|
+
Builds on [`@qrkit/core`](../core). Provides a provider/context, ready-to-use modals, and composable hooks for connection scanning and transaction signing.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -10,43 +10,139 @@ Builds on [`@qrkit/core`](../core). Provides a provider/context model, ready-to-
|
|
|
10
10
|
pnpm add @qrkit/react
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Import the default styles (or bring your own):
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import '@qrkit/react/styles.css'
|
|
17
|
+
```
|
|
18
|
+
|
|
13
19
|
React 18+ is required as a peer dependency.
|
|
14
20
|
|
|
15
|
-
##
|
|
21
|
+
## Quick start — drop-in components
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
Wrap your app in `QRKitProvider` and call `connect()` / `sign()` from anywhere. The modals appear automatically.
|
|
18
24
|
|
|
19
25
|
```tsx
|
|
20
|
-
import { QRKitProvider } from '@qrkit/react'
|
|
26
|
+
import { QRKitProvider, useQRKit } from '@qrkit/react'
|
|
27
|
+
import '@qrkit/react/styles.css'
|
|
28
|
+
|
|
29
|
+
export function App() {
|
|
30
|
+
return (
|
|
31
|
+
<QRKitProvider appName="My dApp">
|
|
32
|
+
<Wallet />
|
|
33
|
+
</QRKitProvider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function Wallet() {
|
|
38
|
+
const { account, connect, disconnect, sign } = useQRKit()
|
|
39
|
+
|
|
40
|
+
async function handleSign() {
|
|
41
|
+
if (!account) return
|
|
42
|
+
const sig = await sign({
|
|
43
|
+
message: 'Hello from My dApp',
|
|
44
|
+
address: account.address,
|
|
45
|
+
sourceFingerprint: account.chain === 'evm' ? account.sourceFingerprint : undefined,
|
|
46
|
+
})
|
|
47
|
+
console.log('Signature:', sig)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!account) return <button onClick={connect}>Connect wallet</button>
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div>
|
|
54
|
+
<p>{account.address}</p>
|
|
55
|
+
<button onClick={handleSign}>Sign message</button>
|
|
56
|
+
<button onClick={disconnect}>Disconnect</button>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Theming
|
|
63
|
+
|
|
64
|
+
Pass a `theme` prop to override CSS variables. Defaults follow Material Design 3 and automatically adapt to light/dark system preference.
|
|
21
65
|
|
|
22
|
-
|
|
23
|
-
|
|
66
|
+
```tsx
|
|
67
|
+
<QRKitProvider theme={{ accent: '#ff6b00', radius: '8px' }}>
|
|
68
|
+
...
|
|
24
69
|
</QRKitProvider>
|
|
25
70
|
```
|
|
26
71
|
|
|
27
|
-
|
|
72
|
+
Or override in CSS directly:
|
|
28
73
|
|
|
29
|
-
```
|
|
30
|
-
|
|
74
|
+
```css
|
|
75
|
+
.qrkit {
|
|
76
|
+
--qrkit-accent: #ff6b00;
|
|
77
|
+
--qrkit-radius: 8px;
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Variable | Default (light) | Default (dark) |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `--qrkit-accent` | `#6750A4` | `#D0BCFF` |
|
|
84
|
+
| `--qrkit-bg` | `#FFFBFE` | `#1C1B1F` |
|
|
85
|
+
| `--qrkit-text` | `#1C1B1F` | `#E6E1E5` |
|
|
86
|
+
| `--qrkit-text-muted` | `#49454F` | `#CAC4D0` |
|
|
87
|
+
| `--qrkit-radius` | `12px` | `12px` |
|
|
88
|
+
|
|
89
|
+
## Low-level hooks
|
|
90
|
+
|
|
91
|
+
### Batteries-included
|
|
92
|
+
|
|
93
|
+
Use `useQRScanner` and `useQRDisplay` when you want custom layouts but keep the built-in camera and QR rendering libraries.
|
|
31
94
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
95
|
+
```tsx
|
|
96
|
+
import { useQRScanner, useQRDisplay } from '@qrkit/react'
|
|
97
|
+
|
|
98
|
+
const { videoRef, progress, error } = useQRScanner({ onScan, enabled })
|
|
99
|
+
const { canvasRef, frame, total } = useQRDisplay({ parts })
|
|
35
100
|
```
|
|
36
101
|
|
|
37
|
-
###
|
|
102
|
+
### Bring your own scanner / renderer
|
|
103
|
+
|
|
104
|
+
Use `useURDecoder` and `useQRParts` to plug in any scanning or rendering library.
|
|
38
105
|
|
|
39
106
|
```tsx
|
|
40
|
-
import {
|
|
107
|
+
import { useURDecoder, useQRParts } from '@qrkit/react'
|
|
108
|
+
|
|
109
|
+
// Feed raw QR strings from any source
|
|
110
|
+
const { receivePart, progress } = useURDecoder({ onScan })
|
|
111
|
+
|
|
112
|
+
// Get the current part string to render with any library
|
|
113
|
+
const { part, frame, total } = useQRParts({ parts })
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API
|
|
41
117
|
|
|
42
|
-
|
|
43
|
-
<QRKitConnect onConnected={(session) => console.log(session)} />
|
|
118
|
+
### `QRKitProvider`
|
|
44
119
|
|
|
45
|
-
|
|
46
|
-
|
|
120
|
+
| Prop | Type | Default |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| `appName` | `string` | `"qrkit"` |
|
|
123
|
+
| `theme` | `QRKitTheme` | MD3 defaults |
|
|
47
124
|
|
|
48
|
-
|
|
49
|
-
|
|
125
|
+
### `useQRKit`
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const { account, connect, disconnect, sign } = useQRKit()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| | Type |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `account` | `Account \| null` |
|
|
134
|
+
| `connect` | `() => void` |
|
|
135
|
+
| `disconnect` | `() => void` |
|
|
136
|
+
| `sign` | `(request: SignRequest) => Promise<string>` |
|
|
137
|
+
|
|
138
|
+
### `SignRequest`
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
interface SignRequest {
|
|
142
|
+
message: string
|
|
143
|
+
address: string
|
|
144
|
+
sourceFingerprint: number | undefined
|
|
145
|
+
}
|
|
50
146
|
```
|
|
51
147
|
|
|
52
148
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,493 @@
|
|
|
1
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
|
+
ConnectModal: () => ConnectModal,
|
|
34
|
+
QRDisplay: () => QRDisplay,
|
|
35
|
+
QRKitProvider: () => QRKitProvider,
|
|
36
|
+
QRScanner: () => QRScanner,
|
|
37
|
+
SignModal: () => SignModal,
|
|
38
|
+
useQRDisplay: () => useQRDisplay,
|
|
39
|
+
useQRKit: () => useQRKit,
|
|
40
|
+
useQRParts: () => useQRParts,
|
|
41
|
+
useQRScanner: () => useQRScanner,
|
|
42
|
+
useURDecoder: () => useURDecoder
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/context.tsx
|
|
47
|
+
var import_react8 = require("react");
|
|
48
|
+
var import_react_dom = require("react-dom");
|
|
49
|
+
|
|
50
|
+
// src/components/ConnectModal.tsx
|
|
51
|
+
var import_react4 = require("react");
|
|
52
|
+
var import_core = require("@qrkit/core");
|
|
53
|
+
|
|
54
|
+
// src/components/Modal.tsx
|
|
55
|
+
var import_react = require("react");
|
|
56
|
+
var import_focus_trap = require("focus-trap");
|
|
57
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
58
|
+
function Modal({ title, onClose, children, className }) {
|
|
59
|
+
const containerRef = (0, import_react.useRef)(null);
|
|
60
|
+
(0, import_react.useEffect)(() => {
|
|
61
|
+
const el = containerRef.current;
|
|
62
|
+
const trap = el ? (0, import_focus_trap.createFocusTrap)(el, {
|
|
63
|
+
escapeDeactivates: false,
|
|
64
|
+
allowOutsideClick: true
|
|
65
|
+
}) : null;
|
|
66
|
+
trap?.activate();
|
|
67
|
+
return () => {
|
|
68
|
+
trap?.deactivate();
|
|
69
|
+
};
|
|
70
|
+
}, [onClose]);
|
|
71
|
+
(0, import_react.useEffect)(() => {
|
|
72
|
+
const handleKey = (e) => {
|
|
73
|
+
if (e.key === "Escape") onClose();
|
|
74
|
+
};
|
|
75
|
+
document.addEventListener("keydown", handleKey);
|
|
76
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
77
|
+
}, [onClose]);
|
|
78
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
79
|
+
"div",
|
|
80
|
+
{
|
|
81
|
+
className: "qrkit qrkit-backdrop",
|
|
82
|
+
onClick: onClose,
|
|
83
|
+
role: "dialog",
|
|
84
|
+
"aria-modal": "true",
|
|
85
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
86
|
+
"div",
|
|
87
|
+
{
|
|
88
|
+
ref: containerRef,
|
|
89
|
+
className: `qrkit-modal${className ? ` ${className}` : ""}`,
|
|
90
|
+
onClick: (e) => e.stopPropagation(),
|
|
91
|
+
children: [
|
|
92
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "qrkit-modal-header", children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "qrkit-modal-title", children: title }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { className: "qrkit-close-btn", onClick: onClose, "aria-label": "Close", children: "\u2715" })
|
|
95
|
+
] }),
|
|
96
|
+
children
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/hooks/useQRScanner.ts
|
|
105
|
+
var import_react3 = require("react");
|
|
106
|
+
var import_jsqr = __toESM(require("jsqr"), 1);
|
|
107
|
+
|
|
108
|
+
// src/hooks/useURDecoder.ts
|
|
109
|
+
var import_react2 = require("react");
|
|
110
|
+
var import_bc_ur = require("@qrkit/bc-ur");
|
|
111
|
+
function useURDecoder({ onScan }) {
|
|
112
|
+
const decoderRef = (0, import_react2.useRef)(new import_bc_ur.UrFountainDecoder());
|
|
113
|
+
const onScanRef = (0, import_react2.useRef)(onScan);
|
|
114
|
+
const [progress, setProgress] = (0, import_react2.useState)(null);
|
|
115
|
+
onScanRef.current = onScan;
|
|
116
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
117
|
+
decoderRef.current = new import_bc_ur.UrFountainDecoder();
|
|
118
|
+
setProgress(null);
|
|
119
|
+
}, []);
|
|
120
|
+
const receivePart = (0, import_react2.useCallback)(
|
|
121
|
+
(data) => {
|
|
122
|
+
if (!data.toLowerCase().startsWith("ur:")) {
|
|
123
|
+
return onScanRef.current(data) !== false;
|
|
124
|
+
}
|
|
125
|
+
decoderRef.current.receivePartUr(data.toLowerCase());
|
|
126
|
+
setProgress(Math.round(decoderRef.current.estimatedPercentComplete() * 100));
|
|
127
|
+
if (!decoderRef.current.isComplete()) return false;
|
|
128
|
+
const ur = decoderRef.current.resultUr;
|
|
129
|
+
const scanned = { type: ur.type, cbor: ur.getPayloadCbor() };
|
|
130
|
+
if (onScanRef.current(scanned) !== false) return true;
|
|
131
|
+
reset();
|
|
132
|
+
return false;
|
|
133
|
+
},
|
|
134
|
+
[reset]
|
|
135
|
+
);
|
|
136
|
+
return { receivePart, progress, reset };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/hooks/useQRScanner.ts
|
|
140
|
+
function useQRScanner({
|
|
141
|
+
onScan,
|
|
142
|
+
enabled = true
|
|
143
|
+
}) {
|
|
144
|
+
const videoRef = (0, import_react3.useRef)(null);
|
|
145
|
+
const rafRef = (0, import_react3.useRef)(null);
|
|
146
|
+
const canvasRef = (0, import_react3.useRef)(null);
|
|
147
|
+
const doneRef = (0, import_react3.useRef)(false);
|
|
148
|
+
const [error, setError] = (0, import_react3.useState)(null);
|
|
149
|
+
const { receivePart, progress } = useURDecoder({ onScan });
|
|
150
|
+
const receivePartRef = (0, import_react3.useRef)(receivePart);
|
|
151
|
+
receivePartRef.current = receivePart;
|
|
152
|
+
const processFrame = (0, import_react3.useCallback)(() => {
|
|
153
|
+
const video = videoRef.current;
|
|
154
|
+
const canvas = canvasRef.current;
|
|
155
|
+
if (!video || !canvas || doneRef.current) return;
|
|
156
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
157
|
+
if (!ctx) return;
|
|
158
|
+
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
|
159
|
+
canvas.width = video.videoWidth;
|
|
160
|
+
canvas.height = video.videoHeight;
|
|
161
|
+
ctx.drawImage(video, 0, 0);
|
|
162
|
+
let imageData;
|
|
163
|
+
try {
|
|
164
|
+
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
165
|
+
} catch {
|
|
166
|
+
setError(
|
|
167
|
+
"Canvas access blocked. Disable fingerprinting protection for this site to scan QR codes."
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
{
|
|
172
|
+
const code = (0, import_jsqr.default)(imageData.data, imageData.width, imageData.height);
|
|
173
|
+
if (code) {
|
|
174
|
+
const done = receivePartRef.current(code.data);
|
|
175
|
+
if (done) {
|
|
176
|
+
doneRef.current = true;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
rafRef.current = requestAnimationFrame(processFrame);
|
|
183
|
+
}, []);
|
|
184
|
+
(0, import_react3.useEffect)(() => {
|
|
185
|
+
if (!enabled || !videoRef.current) return;
|
|
186
|
+
doneRef.current = false;
|
|
187
|
+
canvasRef.current = document.createElement("canvas");
|
|
188
|
+
let stream = null;
|
|
189
|
+
navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then((s) => {
|
|
190
|
+
stream = s;
|
|
191
|
+
if (videoRef.current) {
|
|
192
|
+
videoRef.current.srcObject = stream;
|
|
193
|
+
videoRef.current.play().catch(() => {
|
|
194
|
+
});
|
|
195
|
+
rafRef.current = requestAnimationFrame(processFrame);
|
|
196
|
+
}
|
|
197
|
+
}).catch(() => {
|
|
198
|
+
setError("Camera access denied. Please allow camera permissions.");
|
|
199
|
+
});
|
|
200
|
+
return () => {
|
|
201
|
+
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
|
202
|
+
stream?.getTracks().forEach((t) => t.stop());
|
|
203
|
+
canvasRef.current = null;
|
|
204
|
+
};
|
|
205
|
+
}, [enabled, processFrame]);
|
|
206
|
+
return { videoRef, progress, error };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/components/QRScanner.tsx
|
|
210
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
211
|
+
function QRScanner({ onScan, hint, enabled = true, className }) {
|
|
212
|
+
const { videoRef, progress, error } = useQRScanner({ onScan, enabled });
|
|
213
|
+
if (error) {
|
|
214
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: `qrkit-scanner-error${className ? ` ${className}` : ""}`, children: error });
|
|
215
|
+
}
|
|
216
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: `qrkit-scanner-wrap${className ? ` ${className}` : ""}`, children: [
|
|
217
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "qrkit-scanner-video" }),
|
|
218
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "qrkit-scanner-overlay", children: [
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "qrkit-scanner-corner tl" }),
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "qrkit-scanner-corner tr" }),
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "qrkit-scanner-corner bl" }),
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "qrkit-scanner-corner br" })
|
|
223
|
+
] }),
|
|
224
|
+
progress !== null && progress < 100 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "qrkit-scanner-progress", children: [
|
|
225
|
+
progress,
|
|
226
|
+
"%"
|
|
227
|
+
] }),
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
229
|
+
"p",
|
|
230
|
+
{
|
|
231
|
+
className: "qrkit-hint",
|
|
232
|
+
style: { position: "absolute", bottom: 8, left: 0, right: 0 },
|
|
233
|
+
children: progress !== null && progress < 100 ? "Keep scanning \u2014 animated QR in progress\u2026" : hint ?? "Point camera at the QR code"
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
] });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/components/ConnectModal.tsx
|
|
240
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
241
|
+
function ConnectModal({ onConnect, onClose }) {
|
|
242
|
+
const handleScan = (0, import_react4.useCallback)(
|
|
243
|
+
(data) => {
|
|
244
|
+
try {
|
|
245
|
+
const accounts = (0, import_core.parseConnection)(data, { chains: ["evm"] });
|
|
246
|
+
const account = accounts[0];
|
|
247
|
+
if (!account) return false;
|
|
248
|
+
onConnect(account);
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[onConnect]
|
|
254
|
+
);
|
|
255
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(Modal, { title: "Connect Wallet", onClose, children: [
|
|
256
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "qrkit-step", children: [
|
|
257
|
+
"On your hardware wallet, go to ",
|
|
258
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: "Connect software wallet" }),
|
|
259
|
+
" and point the screen at this camera."
|
|
260
|
+
] }),
|
|
261
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(QRScanner, { onScan: handleScan, hint: "Scan the wallet's connection QR code" })
|
|
262
|
+
] });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/components/SignModal.tsx
|
|
266
|
+
var import_react7 = require("react");
|
|
267
|
+
var import_core2 = require("@qrkit/core");
|
|
268
|
+
|
|
269
|
+
// src/hooks/useQRDisplay.ts
|
|
270
|
+
var import_react6 = require("react");
|
|
271
|
+
var import_qrcode = __toESM(require("qrcode"), 1);
|
|
272
|
+
|
|
273
|
+
// src/hooks/useQRParts.ts
|
|
274
|
+
var import_react5 = require("react");
|
|
275
|
+
function useQRParts({
|
|
276
|
+
parts,
|
|
277
|
+
interval = 200
|
|
278
|
+
}) {
|
|
279
|
+
const [frame, setFrame] = (0, import_react5.useState)(0);
|
|
280
|
+
const frameRef = (0, import_react5.useRef)(0);
|
|
281
|
+
(0, import_react5.useEffect)(() => {
|
|
282
|
+
frameRef.current = 0;
|
|
283
|
+
setFrame(0);
|
|
284
|
+
}, [parts]);
|
|
285
|
+
(0, import_react5.useEffect)(() => {
|
|
286
|
+
if (parts.length <= 1) return;
|
|
287
|
+
const id = setInterval(() => {
|
|
288
|
+
frameRef.current = (frameRef.current + 1) % parts.length;
|
|
289
|
+
setFrame(frameRef.current);
|
|
290
|
+
}, interval);
|
|
291
|
+
return () => clearInterval(id);
|
|
292
|
+
}, [parts, interval]);
|
|
293
|
+
return {
|
|
294
|
+
part: parts[frameRef.current % Math.max(parts.length, 1)] ?? "",
|
|
295
|
+
frame,
|
|
296
|
+
total: parts.length
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/hooks/useQRDisplay.ts
|
|
301
|
+
function useQRDisplay({
|
|
302
|
+
parts,
|
|
303
|
+
interval,
|
|
304
|
+
size = 300
|
|
305
|
+
}) {
|
|
306
|
+
const canvasRef = (0, import_react6.useRef)(null);
|
|
307
|
+
const { part, frame, total } = useQRParts({ parts, interval });
|
|
308
|
+
(0, import_react6.useEffect)(() => {
|
|
309
|
+
if (!part || !canvasRef.current) return;
|
|
310
|
+
import_qrcode.default.toCanvas(canvasRef.current, part, {
|
|
311
|
+
width: size,
|
|
312
|
+
margin: 2,
|
|
313
|
+
errorCorrectionLevel: "M"
|
|
314
|
+
}).catch(() => {
|
|
315
|
+
});
|
|
316
|
+
}, [part, size]);
|
|
317
|
+
return { canvasRef, frame, total };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/components/QRDisplay.tsx
|
|
321
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
322
|
+
function QRDisplay({ parts, interval, size = 300, className }) {
|
|
323
|
+
const { canvasRef, frame, total } = useQRDisplay({ parts, interval, size });
|
|
324
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `qrkit-qr-wrap${className ? ` ${className}` : ""}`, children: [
|
|
325
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("canvas", { ref: canvasRef, className: "qrkit-qr-canvas", width: size, height: size }),
|
|
326
|
+
total > 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { className: "qrkit-hint", children: [
|
|
327
|
+
"Frame ",
|
|
328
|
+
frame + 1,
|
|
329
|
+
" / ",
|
|
330
|
+
total,
|
|
331
|
+
" \u2014 keep Shell pointed at the screen"
|
|
332
|
+
] })
|
|
333
|
+
] });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/components/SignModal.tsx
|
|
337
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
338
|
+
function SignModal({ request, appName, onSign, onReject }) {
|
|
339
|
+
const [step, setStep] = (0, import_react7.useState)("display");
|
|
340
|
+
const parts = (0, import_core2.buildEthSignRequestURParts)(
|
|
341
|
+
request.message,
|
|
342
|
+
request.address,
|
|
343
|
+
request.sourceFingerprint,
|
|
344
|
+
appName
|
|
345
|
+
);
|
|
346
|
+
const handleScan = (0, import_react7.useCallback)(
|
|
347
|
+
(data) => {
|
|
348
|
+
try {
|
|
349
|
+
const sig = (0, import_core2.parseEthSignature)(data);
|
|
350
|
+
onSign(sig);
|
|
351
|
+
} catch {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
[onSign]
|
|
356
|
+
);
|
|
357
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
|
|
358
|
+
Modal,
|
|
359
|
+
{
|
|
360
|
+
title: step === "display" ? "Sign Request" : "Scan Signature",
|
|
361
|
+
onClose: onReject,
|
|
362
|
+
children: [
|
|
363
|
+
step === "display" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
364
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "qrkit-step", children: "Point your hardware wallet camera at this QR code to approve the sign request." }),
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QRDisplay, { parts }),
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "qrkit-btn qrkit-btn-primary", onClick: () => setStep("scan"), children: "Wallet signed \u2014 scan response" }),
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { className: "qrkit-btn qrkit-btn-ghost", onClick: onReject, children: "Cancel" })
|
|
368
|
+
] }),
|
|
369
|
+
step === "scan" && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
|
|
370
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "qrkit-step", children: "On your hardware wallet, show the signature QR and point it at this camera." }),
|
|
371
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(QRScanner, { onScan: handleScan, hint: "Scan the wallet's signature QR code" }),
|
|
372
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
373
|
+
"button",
|
|
374
|
+
{
|
|
375
|
+
className: "qrkit-btn qrkit-btn-ghost",
|
|
376
|
+
onClick: () => setStep("display"),
|
|
377
|
+
children: "\u2190 Back"
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
] })
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/context.tsx
|
|
387
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
388
|
+
var QRKitContext = (0, import_react8.createContext)(null);
|
|
389
|
+
function buildThemeStyle(theme) {
|
|
390
|
+
const vars = {
|
|
391
|
+
"--qrkit-accent": theme.accent,
|
|
392
|
+
"--qrkit-bg": theme.background,
|
|
393
|
+
"--qrkit-backdrop": theme.backdrop,
|
|
394
|
+
"--qrkit-text": theme.text,
|
|
395
|
+
"--qrkit-text-muted": theme.textMuted,
|
|
396
|
+
"--qrkit-radius": theme.radius,
|
|
397
|
+
"--qrkit-font": theme.fontFamily
|
|
398
|
+
};
|
|
399
|
+
const declarations = Object.entries(vars).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
400
|
+
return declarations ? `.qrkit {
|
|
401
|
+
${declarations}
|
|
402
|
+
}` : "";
|
|
403
|
+
}
|
|
404
|
+
function QRKitProvider({
|
|
405
|
+
children,
|
|
406
|
+
theme = {},
|
|
407
|
+
appName = "qrkit"
|
|
408
|
+
}) {
|
|
409
|
+
const [account, setAccount] = (0, import_react8.useState)(null);
|
|
410
|
+
const [connectOpen, setConnectOpen] = (0, import_react8.useState)(false);
|
|
411
|
+
const [pendingSign, setPendingSign] = (0, import_react8.useState)(null);
|
|
412
|
+
const pendingSignRef = (0, import_react8.useRef)(null);
|
|
413
|
+
const themeStyle = (0, import_react8.useMemo)(() => buildThemeStyle(theme), [theme]);
|
|
414
|
+
(0, import_react8.useEffect)(() => {
|
|
415
|
+
if (!themeStyle) return;
|
|
416
|
+
const el = document.createElement("style");
|
|
417
|
+
el.setAttribute("data-qrkit-theme", "");
|
|
418
|
+
el.textContent = themeStyle;
|
|
419
|
+
document.head.appendChild(el);
|
|
420
|
+
return () => el.remove();
|
|
421
|
+
}, [themeStyle]);
|
|
422
|
+
const connect = (0, import_react8.useCallback)(() => setConnectOpen(true), []);
|
|
423
|
+
const disconnect = (0, import_react8.useCallback)(() => setAccount(null), []);
|
|
424
|
+
const handleConnect = (0, import_react8.useCallback)((acc) => {
|
|
425
|
+
setAccount(acc);
|
|
426
|
+
setConnectOpen(false);
|
|
427
|
+
}, []);
|
|
428
|
+
const sign = (0, import_react8.useCallback)((request) => {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const pending = { request, resolve, reject };
|
|
431
|
+
pendingSignRef.current = pending;
|
|
432
|
+
setPendingSign(pending);
|
|
433
|
+
});
|
|
434
|
+
}, []);
|
|
435
|
+
const handleSign = (0, import_react8.useCallback)((sig) => {
|
|
436
|
+
pendingSignRef.current?.resolve(sig);
|
|
437
|
+
pendingSignRef.current = null;
|
|
438
|
+
setPendingSign(null);
|
|
439
|
+
}, []);
|
|
440
|
+
const handleReject = (0, import_react8.useCallback)(() => {
|
|
441
|
+
pendingSignRef.current?.reject(new Error("User rejected the sign request"));
|
|
442
|
+
pendingSignRef.current = null;
|
|
443
|
+
setPendingSign(null);
|
|
444
|
+
}, []);
|
|
445
|
+
const value = (0, import_react8.useMemo)(
|
|
446
|
+
() => ({ account, connect, disconnect, sign }),
|
|
447
|
+
[account, connect, disconnect, sign]
|
|
448
|
+
);
|
|
449
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(QRKitContext.Provider, { value, children: [
|
|
450
|
+
children,
|
|
451
|
+
connectOpen && (0, import_react_dom.createPortal)(
|
|
452
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
453
|
+
ConnectModal,
|
|
454
|
+
{
|
|
455
|
+
onConnect: handleConnect,
|
|
456
|
+
onClose: () => setConnectOpen(false)
|
|
457
|
+
}
|
|
458
|
+
),
|
|
459
|
+
document.body
|
|
460
|
+
),
|
|
461
|
+
pendingSign && (0, import_react_dom.createPortal)(
|
|
462
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
|
|
463
|
+
SignModal,
|
|
464
|
+
{
|
|
465
|
+
request: pendingSign.request,
|
|
466
|
+
appName,
|
|
467
|
+
onSign: handleSign,
|
|
468
|
+
onReject: handleReject
|
|
469
|
+
}
|
|
470
|
+
),
|
|
471
|
+
document.body
|
|
472
|
+
)
|
|
473
|
+
] });
|
|
474
|
+
}
|
|
475
|
+
function useQRKit() {
|
|
476
|
+
const ctx = (0, import_react8.useContext)(QRKitContext);
|
|
477
|
+
if (!ctx) throw new Error("useQRKit must be used within a QRKitProvider");
|
|
478
|
+
return ctx;
|
|
479
|
+
}
|
|
480
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
481
|
+
0 && (module.exports = {
|
|
482
|
+
ConnectModal,
|
|
483
|
+
QRDisplay,
|
|
484
|
+
QRKitProvider,
|
|
485
|
+
QRScanner,
|
|
486
|
+
SignModal,
|
|
487
|
+
useQRDisplay,
|
|
488
|
+
useQRKit,
|
|
489
|
+
useQRParts,
|
|
490
|
+
useQRScanner,
|
|
491
|
+
useURDecoder
|
|
492
|
+
});
|
|
2
493
|
//# sourceMappingURL=index.cjs.map
|