@qrkit/react 0.0.1 → 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 +116 -20
- package/dist/index.cjs +429 -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 +384 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +183 -0
- package/package.json +9 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,2 +1,136 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Account, ScannedUR } from '@qrkit/core';
|
|
1
3
|
|
|
2
|
-
|
|
4
|
+
interface QRKitTheme {
|
|
5
|
+
/** Primary accent color. Defaults follow MD3: light #6750A4, dark #D0BCFF */
|
|
6
|
+
accent?: string;
|
|
7
|
+
/** Modal background color. Defaults follow MD3: light #FFFBFE, dark #1C1B1F */
|
|
8
|
+
background?: string;
|
|
9
|
+
/** Modal backdrop color. Default: rgba(0,0,0,0.6) */
|
|
10
|
+
backdrop?: string;
|
|
11
|
+
/** Text color. Defaults follow MD3: light #1C1B1F, dark #E6E1E5 */
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Muted/secondary text color. Defaults follow MD3: light #49454F, dark #CAC4D0 */
|
|
14
|
+
textMuted?: string;
|
|
15
|
+
/** Border radius. Default: 12px (MD3 "medium") */
|
|
16
|
+
radius?: string;
|
|
17
|
+
/** Font family. Default: inherit */
|
|
18
|
+
fontFamily?: string;
|
|
19
|
+
}
|
|
20
|
+
interface SignRequest {
|
|
21
|
+
message: string;
|
|
22
|
+
address: string;
|
|
23
|
+
sourceFingerprint: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
interface QRKitContextValue {
|
|
26
|
+
account: Account | null;
|
|
27
|
+
connect: () => void;
|
|
28
|
+
disconnect: () => void;
|
|
29
|
+
/** Open the sign modal and resolve with the hex signature. */
|
|
30
|
+
sign: (request: SignRequest) => Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
interface QRKitProviderProps {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
theme?: QRKitTheme;
|
|
35
|
+
/** App name shown in the sign request origin field. Default: "qrkit" */
|
|
36
|
+
appName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare function QRKitProvider({ children, theme, appName }: QRKitProviderProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
declare function useQRKit(): QRKitContextValue;
|
|
41
|
+
|
|
42
|
+
interface ConnectModalProps {
|
|
43
|
+
onConnect: (account: Account) => void;
|
|
44
|
+
onClose: () => void;
|
|
45
|
+
}
|
|
46
|
+
declare function ConnectModal({ onConnect, onClose }: ConnectModalProps): react_jsx_runtime.JSX.Element;
|
|
47
|
+
|
|
48
|
+
interface SignModalProps {
|
|
49
|
+
request: SignRequest;
|
|
50
|
+
appName: string;
|
|
51
|
+
onSign: (signature: string) => void;
|
|
52
|
+
onReject: () => void;
|
|
53
|
+
}
|
|
54
|
+
declare function SignModal({ request, appName, onSign, onReject }: SignModalProps): react_jsx_runtime.JSX.Element;
|
|
55
|
+
|
|
56
|
+
interface QRScannerProps {
|
|
57
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
58
|
+
hint?: string;
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
declare function QRScanner({ onScan, hint, enabled, className }: QRScannerProps): react_jsx_runtime.JSX.Element;
|
|
63
|
+
|
|
64
|
+
interface QRDisplayProps {
|
|
65
|
+
parts: string[];
|
|
66
|
+
interval?: number;
|
|
67
|
+
size?: number;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
declare function QRDisplay({ parts, interval, size, className }: QRDisplayProps): react_jsx_runtime.JSX.Element;
|
|
71
|
+
|
|
72
|
+
interface UseQRScannerOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Called when a QR code is decoded.
|
|
75
|
+
* Return false to keep scanning (e.g. on parse error), void/true to stop.
|
|
76
|
+
*/
|
|
77
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface UseQRScannerResult {
|
|
81
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
82
|
+
/** 0–100 while scanning an animated UR, null otherwise */
|
|
83
|
+
progress: number | null;
|
|
84
|
+
error: string | null;
|
|
85
|
+
}
|
|
86
|
+
declare function useQRScanner({ onScan, enabled, }: UseQRScannerOptions): UseQRScannerResult;
|
|
87
|
+
|
|
88
|
+
interface UseQRDisplayOptions {
|
|
89
|
+
/** QR parts to cycle through. Single-frame: pass an array with one element. */
|
|
90
|
+
parts: string[];
|
|
91
|
+
/** Interval between frames in ms. Default: 200 */
|
|
92
|
+
interval?: number;
|
|
93
|
+
/** Canvas size in pixels. Default: 300 */
|
|
94
|
+
size?: number;
|
|
95
|
+
}
|
|
96
|
+
interface UseQRDisplayResult {
|
|
97
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
98
|
+
/** Current frame index */
|
|
99
|
+
frame: number;
|
|
100
|
+
total: number;
|
|
101
|
+
}
|
|
102
|
+
declare function useQRDisplay({ parts, interval, size, }: UseQRDisplayOptions): UseQRDisplayResult;
|
|
103
|
+
|
|
104
|
+
interface UseURDecoderOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Called when a QR string is decoded.
|
|
107
|
+
* Return false to keep scanning (e.g. on parse error), void/true to stop.
|
|
108
|
+
*/
|
|
109
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
110
|
+
}
|
|
111
|
+
interface UseURDecoderResult {
|
|
112
|
+
/** Feed a raw QR string into the decoder. Returns true when scanning is done. */
|
|
113
|
+
receivePart: (data: string) => boolean;
|
|
114
|
+
/** 0–100 while assembling an animated UR, null otherwise */
|
|
115
|
+
progress: number | null;
|
|
116
|
+
/** Reset decoder state (e.g. to start a new scan) */
|
|
117
|
+
reset: () => void;
|
|
118
|
+
}
|
|
119
|
+
declare function useURDecoder({ onScan }: UseURDecoderOptions): UseURDecoderResult;
|
|
120
|
+
|
|
121
|
+
interface UseQRPartsOptions {
|
|
122
|
+
/** QR parts to cycle through. Single-frame: pass an array with one element. */
|
|
123
|
+
parts: string[];
|
|
124
|
+
/** Interval between frames in ms. Default: 200 */
|
|
125
|
+
interval?: number;
|
|
126
|
+
}
|
|
127
|
+
interface UseQRPartsResult {
|
|
128
|
+
/** The current QR string to render */
|
|
129
|
+
part: string;
|
|
130
|
+
/** Current frame index (0-based) */
|
|
131
|
+
frame: number;
|
|
132
|
+
total: number;
|
|
133
|
+
}
|
|
134
|
+
declare function useQRParts({ parts, interval, }: UseQRPartsOptions): UseQRPartsResult;
|
|
135
|
+
|
|
136
|
+
export { ConnectModal, type ConnectModalProps, QRDisplay, type QRDisplayProps, type QRKitContextValue, QRKitProvider, type QRKitProviderProps, type QRKitTheme, QRScanner, type QRScannerProps, SignModal, type SignModalProps, type SignRequest, type UseQRDisplayOptions, type UseQRDisplayResult, type UseQRPartsOptions, type UseQRPartsResult, type UseQRScannerOptions, type UseQRScannerResult, type UseURDecoderOptions, type UseURDecoderResult, useQRDisplay, useQRKit, useQRParts, useQRScanner, useURDecoder };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,136 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { Account, ScannedUR } from '@qrkit/core';
|
|
1
3
|
|
|
2
|
-
|
|
4
|
+
interface QRKitTheme {
|
|
5
|
+
/** Primary accent color. Defaults follow MD3: light #6750A4, dark #D0BCFF */
|
|
6
|
+
accent?: string;
|
|
7
|
+
/** Modal background color. Defaults follow MD3: light #FFFBFE, dark #1C1B1F */
|
|
8
|
+
background?: string;
|
|
9
|
+
/** Modal backdrop color. Default: rgba(0,0,0,0.6) */
|
|
10
|
+
backdrop?: string;
|
|
11
|
+
/** Text color. Defaults follow MD3: light #1C1B1F, dark #E6E1E5 */
|
|
12
|
+
text?: string;
|
|
13
|
+
/** Muted/secondary text color. Defaults follow MD3: light #49454F, dark #CAC4D0 */
|
|
14
|
+
textMuted?: string;
|
|
15
|
+
/** Border radius. Default: 12px (MD3 "medium") */
|
|
16
|
+
radius?: string;
|
|
17
|
+
/** Font family. Default: inherit */
|
|
18
|
+
fontFamily?: string;
|
|
19
|
+
}
|
|
20
|
+
interface SignRequest {
|
|
21
|
+
message: string;
|
|
22
|
+
address: string;
|
|
23
|
+
sourceFingerprint: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
interface QRKitContextValue {
|
|
26
|
+
account: Account | null;
|
|
27
|
+
connect: () => void;
|
|
28
|
+
disconnect: () => void;
|
|
29
|
+
/** Open the sign modal and resolve with the hex signature. */
|
|
30
|
+
sign: (request: SignRequest) => Promise<string>;
|
|
31
|
+
}
|
|
32
|
+
interface QRKitProviderProps {
|
|
33
|
+
children: React.ReactNode;
|
|
34
|
+
theme?: QRKitTheme;
|
|
35
|
+
/** App name shown in the sign request origin field. Default: "qrkit" */
|
|
36
|
+
appName?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
declare function QRKitProvider({ children, theme, appName }: QRKitProviderProps): react_jsx_runtime.JSX.Element;
|
|
40
|
+
declare function useQRKit(): QRKitContextValue;
|
|
41
|
+
|
|
42
|
+
interface ConnectModalProps {
|
|
43
|
+
onConnect: (account: Account) => void;
|
|
44
|
+
onClose: () => void;
|
|
45
|
+
}
|
|
46
|
+
declare function ConnectModal({ onConnect, onClose }: ConnectModalProps): react_jsx_runtime.JSX.Element;
|
|
47
|
+
|
|
48
|
+
interface SignModalProps {
|
|
49
|
+
request: SignRequest;
|
|
50
|
+
appName: string;
|
|
51
|
+
onSign: (signature: string) => void;
|
|
52
|
+
onReject: () => void;
|
|
53
|
+
}
|
|
54
|
+
declare function SignModal({ request, appName, onSign, onReject }: SignModalProps): react_jsx_runtime.JSX.Element;
|
|
55
|
+
|
|
56
|
+
interface QRScannerProps {
|
|
57
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
58
|
+
hint?: string;
|
|
59
|
+
enabled?: boolean;
|
|
60
|
+
className?: string;
|
|
61
|
+
}
|
|
62
|
+
declare function QRScanner({ onScan, hint, enabled, className }: QRScannerProps): react_jsx_runtime.JSX.Element;
|
|
63
|
+
|
|
64
|
+
interface QRDisplayProps {
|
|
65
|
+
parts: string[];
|
|
66
|
+
interval?: number;
|
|
67
|
+
size?: number;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
declare function QRDisplay({ parts, interval, size, className }: QRDisplayProps): react_jsx_runtime.JSX.Element;
|
|
71
|
+
|
|
72
|
+
interface UseQRScannerOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Called when a QR code is decoded.
|
|
75
|
+
* Return false to keep scanning (e.g. on parse error), void/true to stop.
|
|
76
|
+
*/
|
|
77
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
}
|
|
80
|
+
interface UseQRScannerResult {
|
|
81
|
+
videoRef: React.RefObject<HTMLVideoElement | null>;
|
|
82
|
+
/** 0–100 while scanning an animated UR, null otherwise */
|
|
83
|
+
progress: number | null;
|
|
84
|
+
error: string | null;
|
|
85
|
+
}
|
|
86
|
+
declare function useQRScanner({ onScan, enabled, }: UseQRScannerOptions): UseQRScannerResult;
|
|
87
|
+
|
|
88
|
+
interface UseQRDisplayOptions {
|
|
89
|
+
/** QR parts to cycle through. Single-frame: pass an array with one element. */
|
|
90
|
+
parts: string[];
|
|
91
|
+
/** Interval between frames in ms. Default: 200 */
|
|
92
|
+
interval?: number;
|
|
93
|
+
/** Canvas size in pixels. Default: 300 */
|
|
94
|
+
size?: number;
|
|
95
|
+
}
|
|
96
|
+
interface UseQRDisplayResult {
|
|
97
|
+
canvasRef: React.RefObject<HTMLCanvasElement | null>;
|
|
98
|
+
/** Current frame index */
|
|
99
|
+
frame: number;
|
|
100
|
+
total: number;
|
|
101
|
+
}
|
|
102
|
+
declare function useQRDisplay({ parts, interval, size, }: UseQRDisplayOptions): UseQRDisplayResult;
|
|
103
|
+
|
|
104
|
+
interface UseURDecoderOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Called when a QR string is decoded.
|
|
107
|
+
* Return false to keep scanning (e.g. on parse error), void/true to stop.
|
|
108
|
+
*/
|
|
109
|
+
onScan: (result: ScannedUR | string) => boolean | void;
|
|
110
|
+
}
|
|
111
|
+
interface UseURDecoderResult {
|
|
112
|
+
/** Feed a raw QR string into the decoder. Returns true when scanning is done. */
|
|
113
|
+
receivePart: (data: string) => boolean;
|
|
114
|
+
/** 0–100 while assembling an animated UR, null otherwise */
|
|
115
|
+
progress: number | null;
|
|
116
|
+
/** Reset decoder state (e.g. to start a new scan) */
|
|
117
|
+
reset: () => void;
|
|
118
|
+
}
|
|
119
|
+
declare function useURDecoder({ onScan }: UseURDecoderOptions): UseURDecoderResult;
|
|
120
|
+
|
|
121
|
+
interface UseQRPartsOptions {
|
|
122
|
+
/** QR parts to cycle through. Single-frame: pass an array with one element. */
|
|
123
|
+
parts: string[];
|
|
124
|
+
/** Interval between frames in ms. Default: 200 */
|
|
125
|
+
interval?: number;
|
|
126
|
+
}
|
|
127
|
+
interface UseQRPartsResult {
|
|
128
|
+
/** The current QR string to render */
|
|
129
|
+
part: string;
|
|
130
|
+
/** Current frame index (0-based) */
|
|
131
|
+
frame: number;
|
|
132
|
+
total: number;
|
|
133
|
+
}
|
|
134
|
+
declare function useQRParts({ parts, interval, }: UseQRPartsOptions): UseQRPartsResult;
|
|
135
|
+
|
|
136
|
+
export { ConnectModal, type ConnectModalProps, QRDisplay, type QRDisplayProps, type QRKitContextValue, QRKitProvider, type QRKitProviderProps, type QRKitTheme, QRScanner, type QRScannerProps, SignModal, type SignModalProps, type SignRequest, type UseQRDisplayOptions, type UseQRDisplayResult, type UseQRPartsOptions, type UseQRPartsResult, type UseQRScannerOptions, type UseQRScannerResult, type UseURDecoderOptions, type UseURDecoderResult, useQRDisplay, useQRKit, useQRParts, useQRScanner, useURDecoder };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,385 @@
|
|
|
1
|
+
// src/context.tsx
|
|
2
|
+
import { createContext, useCallback as useCallback5, useContext, useEffect as useEffect5, useMemo, useRef as useRef6, useState as useState5 } from "react";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
|
|
5
|
+
// src/components/ConnectModal.tsx
|
|
6
|
+
import { useCallback as useCallback3 } from "react";
|
|
7
|
+
import { parseConnection } from "@qrkit/core";
|
|
8
|
+
|
|
9
|
+
// src/components/Modal.tsx
|
|
10
|
+
import { useEffect, useRef } from "react";
|
|
11
|
+
import { createFocusTrap } from "focus-trap";
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
function Modal({ title, onClose, children, className }) {
|
|
14
|
+
const containerRef = useRef(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const el = containerRef.current;
|
|
17
|
+
const trap = el ? createFocusTrap(el, {
|
|
18
|
+
escapeDeactivates: true,
|
|
19
|
+
onDeactivate: onClose,
|
|
20
|
+
allowOutsideClick: true
|
|
21
|
+
}) : null;
|
|
22
|
+
trap?.activate();
|
|
23
|
+
return () => {
|
|
24
|
+
trap?.deactivate();
|
|
25
|
+
};
|
|
26
|
+
}, [onClose]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleKey = (e) => {
|
|
29
|
+
if (e.key === "Escape") onClose();
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("keydown", handleKey);
|
|
32
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
33
|
+
}, [onClose]);
|
|
34
|
+
return /* @__PURE__ */ jsx("div", { className: "qrkit qrkit-backdrop", onClick: onClose, role: "dialog", "aria-modal": "true", children: /* @__PURE__ */ jsxs(
|
|
35
|
+
"div",
|
|
36
|
+
{
|
|
37
|
+
ref: containerRef,
|
|
38
|
+
className: `qrkit-modal${className ? ` ${className}` : ""}`,
|
|
39
|
+
onClick: (e) => e.stopPropagation(),
|
|
40
|
+
children: [
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "qrkit-modal-header", children: [
|
|
42
|
+
/* @__PURE__ */ jsx("h2", { className: "qrkit-modal-title", children: title }),
|
|
43
|
+
/* @__PURE__ */ jsx("button", { className: "qrkit-close-btn", onClick: onClose, "aria-label": "Close", children: "\u2715" })
|
|
44
|
+
] }),
|
|
45
|
+
children
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/hooks/useQRScanner.ts
|
|
52
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
|
|
53
|
+
import QrScanner from "qr-scanner";
|
|
54
|
+
|
|
55
|
+
// src/hooks/useURDecoder.ts
|
|
56
|
+
import { useCallback, useRef as useRef2, useState } from "react";
|
|
57
|
+
import { URDecoder } from "@ngraveio/bc-ur";
|
|
58
|
+
function useURDecoder({ onScan }) {
|
|
59
|
+
const decoderRef = useRef2(new URDecoder());
|
|
60
|
+
const onScanRef = useRef2(onScan);
|
|
61
|
+
const [progress, setProgress] = useState(null);
|
|
62
|
+
onScanRef.current = onScan;
|
|
63
|
+
const reset = useCallback(() => {
|
|
64
|
+
decoderRef.current = new URDecoder();
|
|
65
|
+
setProgress(null);
|
|
66
|
+
}, []);
|
|
67
|
+
const receivePart = useCallback(
|
|
68
|
+
(data) => {
|
|
69
|
+
if (!data.toLowerCase().startsWith("ur:")) {
|
|
70
|
+
return onScanRef.current(data) !== false;
|
|
71
|
+
}
|
|
72
|
+
decoderRef.current.receivePart(data.toLowerCase());
|
|
73
|
+
setProgress(Math.round(decoderRef.current.estimatedPercentComplete() * 100));
|
|
74
|
+
if (!decoderRef.current.isComplete()) return false;
|
|
75
|
+
const ur = decoderRef.current.resultUR();
|
|
76
|
+
const scanned = { type: ur.type, cbor: new Uint8Array(ur.cbor) };
|
|
77
|
+
if (onScanRef.current(scanned) !== false) return true;
|
|
78
|
+
reset();
|
|
79
|
+
return false;
|
|
80
|
+
},
|
|
81
|
+
[reset]
|
|
82
|
+
);
|
|
83
|
+
return { receivePart, progress, reset };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/hooks/useQRScanner.ts
|
|
87
|
+
function useQRScanner({
|
|
88
|
+
onScan,
|
|
89
|
+
enabled = true
|
|
90
|
+
}) {
|
|
91
|
+
const videoRef = useRef3(null);
|
|
92
|
+
const scannerRef = useRef3(null);
|
|
93
|
+
const [error, setError] = useState2(null);
|
|
94
|
+
const { receivePart, progress } = useURDecoder({ onScan });
|
|
95
|
+
const processResult = useCallback2(
|
|
96
|
+
(data, scanner) => {
|
|
97
|
+
const done = receivePart(data);
|
|
98
|
+
if (done) scanner.stop();
|
|
99
|
+
},
|
|
100
|
+
[receivePart]
|
|
101
|
+
);
|
|
102
|
+
useEffect2(() => {
|
|
103
|
+
if (!enabled || !videoRef.current) return;
|
|
104
|
+
const scanner = new QrScanner(
|
|
105
|
+
videoRef.current,
|
|
106
|
+
(result) => processResult(result.data, scanner),
|
|
107
|
+
{
|
|
108
|
+
preferredCamera: "environment",
|
|
109
|
+
highlightScanRegion: false,
|
|
110
|
+
highlightCodeOutline: false
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
scannerRef.current = scanner;
|
|
114
|
+
scanner.start().catch(() => {
|
|
115
|
+
setError("Camera access denied. Please allow camera permissions.");
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
scanner.stop();
|
|
119
|
+
scanner.destroy();
|
|
120
|
+
scannerRef.current = null;
|
|
121
|
+
};
|
|
122
|
+
}, [enabled, processResult]);
|
|
123
|
+
return { videoRef, progress, error };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/components/QRScanner.tsx
|
|
127
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
128
|
+
function QRScanner({ onScan, hint, enabled = true, className }) {
|
|
129
|
+
const { videoRef, progress, error } = useQRScanner({ onScan, enabled });
|
|
130
|
+
if (error) {
|
|
131
|
+
return /* @__PURE__ */ jsx2("div", { className: `qrkit-scanner-error${className ? ` ${className}` : ""}`, children: error });
|
|
132
|
+
}
|
|
133
|
+
return /* @__PURE__ */ jsxs2("div", { className: `qrkit-scanner-wrap${className ? ` ${className}` : ""}`, children: [
|
|
134
|
+
/* @__PURE__ */ jsx2("video", { ref: videoRef, autoPlay: true, playsInline: true, muted: true, className: "qrkit-scanner-video" }),
|
|
135
|
+
/* @__PURE__ */ jsxs2("div", { className: "qrkit-scanner-overlay", children: [
|
|
136
|
+
/* @__PURE__ */ jsx2("div", { className: "qrkit-scanner-corner tl" }),
|
|
137
|
+
/* @__PURE__ */ jsx2("div", { className: "qrkit-scanner-corner tr" }),
|
|
138
|
+
/* @__PURE__ */ jsx2("div", { className: "qrkit-scanner-corner bl" }),
|
|
139
|
+
/* @__PURE__ */ jsx2("div", { className: "qrkit-scanner-corner br" })
|
|
140
|
+
] }),
|
|
141
|
+
progress !== null && progress < 100 && /* @__PURE__ */ jsxs2("div", { className: "qrkit-scanner-progress", children: [
|
|
142
|
+
progress,
|
|
143
|
+
"%"
|
|
144
|
+
] }),
|
|
145
|
+
/* @__PURE__ */ jsx2("p", { className: "qrkit-hint", style: { position: "absolute", bottom: 8, left: 0, right: 0 }, children: progress !== null && progress < 100 ? "Keep scanning \u2014 animated QR in progress\u2026" : hint ?? "Point camera at the QR code" })
|
|
146
|
+
] });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/components/ConnectModal.tsx
|
|
150
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
151
|
+
function ConnectModal({ onConnect, onClose }) {
|
|
152
|
+
const handleScan = useCallback3(
|
|
153
|
+
(data) => {
|
|
154
|
+
try {
|
|
155
|
+
const accounts = parseConnection(data, { chains: ["evm"] });
|
|
156
|
+
const account = accounts[0];
|
|
157
|
+
if (!account) return false;
|
|
158
|
+
onConnect(account);
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[onConnect]
|
|
164
|
+
);
|
|
165
|
+
return /* @__PURE__ */ jsxs3(Modal, { title: "Connect Wallet", onClose, children: [
|
|
166
|
+
/* @__PURE__ */ jsxs3("p", { className: "qrkit-step", children: [
|
|
167
|
+
"On your hardware wallet, go to ",
|
|
168
|
+
/* @__PURE__ */ jsx3("strong", { children: "Connect software wallet" }),
|
|
169
|
+
" and point the screen at this camera."
|
|
170
|
+
] }),
|
|
171
|
+
/* @__PURE__ */ jsx3(QRScanner, { onScan: handleScan, hint: "Scan the wallet's connection QR code" })
|
|
172
|
+
] });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/components/SignModal.tsx
|
|
176
|
+
import { useCallback as useCallback4, useState as useState4 } from "react";
|
|
177
|
+
import { buildEthSignRequestURParts, parseEthSignature } from "@qrkit/core";
|
|
178
|
+
|
|
179
|
+
// src/hooks/useQRDisplay.ts
|
|
180
|
+
import { useEffect as useEffect4, useRef as useRef5 } from "react";
|
|
181
|
+
import QRCode from "qrcode";
|
|
182
|
+
|
|
183
|
+
// src/hooks/useQRParts.ts
|
|
184
|
+
import { useEffect as useEffect3, useRef as useRef4, useState as useState3 } from "react";
|
|
185
|
+
function useQRParts({
|
|
186
|
+
parts,
|
|
187
|
+
interval = 200
|
|
188
|
+
}) {
|
|
189
|
+
const [frame, setFrame] = useState3(0);
|
|
190
|
+
const frameRef = useRef4(0);
|
|
191
|
+
useEffect3(() => {
|
|
192
|
+
frameRef.current = 0;
|
|
193
|
+
setFrame(0);
|
|
194
|
+
}, [parts]);
|
|
195
|
+
useEffect3(() => {
|
|
196
|
+
if (parts.length <= 1) return;
|
|
197
|
+
const id = setInterval(() => {
|
|
198
|
+
frameRef.current = (frameRef.current + 1) % parts.length;
|
|
199
|
+
setFrame(frameRef.current);
|
|
200
|
+
}, interval);
|
|
201
|
+
return () => clearInterval(id);
|
|
202
|
+
}, [parts, interval]);
|
|
203
|
+
return {
|
|
204
|
+
part: parts[frameRef.current % Math.max(parts.length, 1)] ?? "",
|
|
205
|
+
frame,
|
|
206
|
+
total: parts.length
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/hooks/useQRDisplay.ts
|
|
211
|
+
function useQRDisplay({
|
|
212
|
+
parts,
|
|
213
|
+
interval,
|
|
214
|
+
size = 300
|
|
215
|
+
}) {
|
|
216
|
+
const canvasRef = useRef5(null);
|
|
217
|
+
const { part, frame, total } = useQRParts({ parts, interval });
|
|
218
|
+
useEffect4(() => {
|
|
219
|
+
if (!part || !canvasRef.current) return;
|
|
220
|
+
QRCode.toCanvas(canvasRef.current, part, {
|
|
221
|
+
width: size,
|
|
222
|
+
margin: 2,
|
|
223
|
+
errorCorrectionLevel: "M"
|
|
224
|
+
}).catch(() => {
|
|
225
|
+
});
|
|
226
|
+
}, [part, size]);
|
|
227
|
+
return { canvasRef, frame, total };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/components/QRDisplay.tsx
|
|
231
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
232
|
+
function QRDisplay({ parts, interval, size = 300, className }) {
|
|
233
|
+
const { canvasRef, frame, total } = useQRDisplay({ parts, interval, size });
|
|
234
|
+
return /* @__PURE__ */ jsxs4("div", { className: `qrkit-qr-wrap${className ? ` ${className}` : ""}`, children: [
|
|
235
|
+
/* @__PURE__ */ jsx4("canvas", { ref: canvasRef, className: "qrkit-qr-canvas", width: size, height: size }),
|
|
236
|
+
total > 1 && /* @__PURE__ */ jsxs4("p", { className: "qrkit-hint", children: [
|
|
237
|
+
"Frame ",
|
|
238
|
+
frame + 1,
|
|
239
|
+
" / ",
|
|
240
|
+
total,
|
|
241
|
+
" \u2014 keep Shell pointed at the screen"
|
|
242
|
+
] })
|
|
243
|
+
] });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/components/SignModal.tsx
|
|
247
|
+
import { Fragment, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
248
|
+
function SignModal({ request, appName, onSign, onReject }) {
|
|
249
|
+
const [step, setStep] = useState4("display");
|
|
250
|
+
const parts = buildEthSignRequestURParts(
|
|
251
|
+
request.message,
|
|
252
|
+
request.address,
|
|
253
|
+
request.sourceFingerprint,
|
|
254
|
+
appName
|
|
255
|
+
);
|
|
256
|
+
const handleScan = useCallback4(
|
|
257
|
+
(data) => {
|
|
258
|
+
try {
|
|
259
|
+
const sig = parseEthSignature(data);
|
|
260
|
+
onSign(sig);
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[onSign]
|
|
266
|
+
);
|
|
267
|
+
return /* @__PURE__ */ jsxs5(
|
|
268
|
+
Modal,
|
|
269
|
+
{
|
|
270
|
+
title: step === "display" ? "Sign Request" : "Scan Signature",
|
|
271
|
+
onClose: onReject,
|
|
272
|
+
children: [
|
|
273
|
+
step === "display" && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
274
|
+
/* @__PURE__ */ jsx5("p", { className: "qrkit-step", children: "Point your hardware wallet camera at this QR code to approve the sign request." }),
|
|
275
|
+
/* @__PURE__ */ jsx5(QRDisplay, { parts }),
|
|
276
|
+
/* @__PURE__ */ jsx5("button", { className: "qrkit-btn qrkit-btn-primary", onClick: () => setStep("scan"), children: "Wallet signed \u2014 scan response" }),
|
|
277
|
+
/* @__PURE__ */ jsx5("button", { className: "qrkit-btn qrkit-btn-ghost", onClick: onReject, children: "Cancel" })
|
|
278
|
+
] }),
|
|
279
|
+
step === "scan" && /* @__PURE__ */ jsxs5(Fragment, { children: [
|
|
280
|
+
/* @__PURE__ */ jsx5("p", { className: "qrkit-step", children: "On your hardware wallet, show the signature QR and point it at this camera." }),
|
|
281
|
+
/* @__PURE__ */ jsx5(QRScanner, { onScan: handleScan, hint: "Scan the wallet's signature QR code" }),
|
|
282
|
+
/* @__PURE__ */ jsx5("button", { className: "qrkit-btn qrkit-btn-ghost", onClick: () => setStep("display"), children: "\u2190 Back" })
|
|
283
|
+
] })
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/context.tsx
|
|
290
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
291
|
+
var QRKitContext = createContext(null);
|
|
292
|
+
function buildThemeStyle(theme) {
|
|
293
|
+
const vars = {
|
|
294
|
+
"--qrkit-accent": theme.accent,
|
|
295
|
+
"--qrkit-bg": theme.background,
|
|
296
|
+
"--qrkit-backdrop": theme.backdrop,
|
|
297
|
+
"--qrkit-text": theme.text,
|
|
298
|
+
"--qrkit-text-muted": theme.textMuted,
|
|
299
|
+
"--qrkit-radius": theme.radius,
|
|
300
|
+
"--qrkit-font": theme.fontFamily
|
|
301
|
+
};
|
|
302
|
+
const declarations = Object.entries(vars).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}: ${v};`).join("\n");
|
|
303
|
+
return declarations ? `.qrkit {
|
|
304
|
+
${declarations}
|
|
305
|
+
}` : "";
|
|
306
|
+
}
|
|
307
|
+
function QRKitProvider({ children, theme = {}, appName = "qrkit" }) {
|
|
308
|
+
const [account, setAccount] = useState5(null);
|
|
309
|
+
const [connectOpen, setConnectOpen] = useState5(false);
|
|
310
|
+
const [pendingSign, setPendingSign] = useState5(null);
|
|
311
|
+
const pendingSignRef = useRef6(null);
|
|
312
|
+
const themeStyle = useMemo(() => buildThemeStyle(theme), [theme]);
|
|
313
|
+
useEffect5(() => {
|
|
314
|
+
if (!themeStyle) return;
|
|
315
|
+
const el = document.createElement("style");
|
|
316
|
+
el.setAttribute("data-qrkit-theme", "");
|
|
317
|
+
el.textContent = themeStyle;
|
|
318
|
+
document.head.appendChild(el);
|
|
319
|
+
return () => el.remove();
|
|
320
|
+
}, [themeStyle]);
|
|
321
|
+
const connect = useCallback5(() => setConnectOpen(true), []);
|
|
322
|
+
const disconnect = useCallback5(() => setAccount(null), []);
|
|
323
|
+
const handleConnect = useCallback5((acc) => {
|
|
324
|
+
setAccount(acc);
|
|
325
|
+
setConnectOpen(false);
|
|
326
|
+
}, []);
|
|
327
|
+
const sign = useCallback5((request) => {
|
|
328
|
+
return new Promise((resolve, reject) => {
|
|
329
|
+
const pending = { request, resolve, reject };
|
|
330
|
+
pendingSignRef.current = pending;
|
|
331
|
+
setPendingSign(pending);
|
|
332
|
+
});
|
|
333
|
+
}, []);
|
|
334
|
+
const handleSign = useCallback5((sig) => {
|
|
335
|
+
pendingSignRef.current?.resolve(sig);
|
|
336
|
+
pendingSignRef.current = null;
|
|
337
|
+
setPendingSign(null);
|
|
338
|
+
}, []);
|
|
339
|
+
const handleReject = useCallback5(() => {
|
|
340
|
+
pendingSignRef.current?.reject(new Error("User rejected the sign request"));
|
|
341
|
+
pendingSignRef.current = null;
|
|
342
|
+
setPendingSign(null);
|
|
343
|
+
}, []);
|
|
344
|
+
const value = useMemo(
|
|
345
|
+
() => ({ account, connect, disconnect, sign }),
|
|
346
|
+
[account, connect, disconnect, sign]
|
|
347
|
+
);
|
|
348
|
+
return /* @__PURE__ */ jsxs6(QRKitContext.Provider, { value, children: [
|
|
349
|
+
children,
|
|
350
|
+
connectOpen && createPortal(
|
|
351
|
+
/* @__PURE__ */ jsx6(ConnectModal, { onConnect: handleConnect, onClose: () => setConnectOpen(false) }),
|
|
352
|
+
document.body
|
|
353
|
+
),
|
|
354
|
+
pendingSign && createPortal(
|
|
355
|
+
/* @__PURE__ */ jsx6(
|
|
356
|
+
SignModal,
|
|
357
|
+
{
|
|
358
|
+
request: pendingSign.request,
|
|
359
|
+
appName,
|
|
360
|
+
onSign: handleSign,
|
|
361
|
+
onReject: handleReject
|
|
362
|
+
}
|
|
363
|
+
),
|
|
364
|
+
document.body
|
|
365
|
+
)
|
|
366
|
+
] });
|
|
367
|
+
}
|
|
368
|
+
function useQRKit() {
|
|
369
|
+
const ctx = useContext(QRKitContext);
|
|
370
|
+
if (!ctx) throw new Error("useQRKit must be used within a QRKitProvider");
|
|
371
|
+
return ctx;
|
|
372
|
+
}
|
|
373
|
+
export {
|
|
374
|
+
ConnectModal,
|
|
375
|
+
QRDisplay,
|
|
376
|
+
QRKitProvider,
|
|
377
|
+
QRScanner,
|
|
378
|
+
SignModal,
|
|
379
|
+
useQRDisplay,
|
|
380
|
+
useQRKit,
|
|
381
|
+
useQRParts,
|
|
382
|
+
useQRScanner,
|
|
383
|
+
useURDecoder
|
|
384
|
+
};
|
|
1
385
|
//# sourceMappingURL=index.js.map
|