@peers-app/peers-ui 0.23.13 → 0.24.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/biome.json +2 -2
- package/dist/components/qr-scanner.d.ts +18 -0
- package/dist/components/qr-scanner.js +165 -0
- package/dist/components/router.d.ts +1 -1
- package/dist/components/router.js +1 -1
- package/dist/root-layout/root-layout-host.js +18 -19
- package/dist/root-layout/root-layout-host.test.js +14 -9
- package/dist/screens/account/account-screen.js +52 -1
- package/dist/screens/contacts/contact-list.js +28 -1
- package/dist/screens/contacts/index.d.ts +0 -2
- package/dist/screens/contacts/index.js +0 -2
- package/dist/screens/device-pairing/device-pairing.d.ts +2 -4
- package/dist/screens/device-pairing/device-pairing.js +127 -128
- package/dist/screens/device-pairing/device-pairing.test.js +51 -15
- package/dist/screens/device-pairing/index.d.ts +0 -1
- package/dist/screens/device-pairing/index.js +0 -1
- package/dist/screens/groups/group-details.js +2 -6
- package/dist/screens/groups/group-invite-panel.d.ts +14 -0
- package/dist/screens/groups/group-invite-panel.js +125 -0
- package/dist/screens/groups/group-members.js +9 -103
- package/dist/screens/invites/accept-invite.d.ts +11 -0
- package/dist/screens/invites/accept-invite.js +114 -0
- package/dist/screens/invites/index.d.ts +7 -0
- package/dist/screens/{join-group → invites}/index.js +8 -3
- package/dist/screens/invites/invite-capture.d.ts +34 -0
- package/dist/screens/invites/invite-capture.js +94 -0
- package/dist/screens/invites/invite-share.d.ts +15 -0
- package/dist/screens/invites/invite-share.js +40 -0
- package/dist/screens/invites/invites-ipc.d.ts +10 -0
- package/dist/screens/invites/invites-ipc.js +61 -0
- package/dist/screens/invites/invites-screen.d.ts +9 -0
- package/dist/screens/invites/invites-screen.js +36 -0
- package/dist/screens/invites/pending-invites-list.d.ts +29 -0
- package/dist/screens/invites/pending-invites-list.js +156 -0
- package/dist/screens/settings/settings-page.js +1 -1
- package/dist/screens/setup-user.js +4 -18
- package/dist/system-apps/index.d.ts +1 -1
- package/dist/system-apps/index.js +5 -5
- package/dist/system-apps/{join-group.app.d.ts → invites.app.d.ts} +1 -1
- package/dist/system-apps/invites.app.js +9 -0
- package/package.json +4 -3
- package/src/components/qr-scanner.tsx +180 -0
- package/src/components/router.tsx +1 -1
- package/src/root-layout/root-layout-host.test.tsx +18 -13
- package/src/root-layout/root-layout-host.tsx +23 -21
- package/src/screens/account/account-screen.tsx +89 -2
- package/src/screens/contacts/contact-list.tsx +64 -4
- package/src/screens/contacts/index.ts +0 -2
- package/src/screens/device-pairing/device-pairing.test.tsx +61 -16
- package/src/screens/device-pairing/device-pairing.tsx +283 -208
- package/src/screens/device-pairing/index.ts +0 -1
- package/src/screens/groups/group-details.tsx +1 -41
- package/src/screens/groups/group-invite-panel.tsx +316 -0
- package/src/screens/groups/group-members.tsx +15 -177
- package/src/screens/invites/accept-invite.tsx +271 -0
- package/src/screens/invites/index.ts +9 -0
- package/src/screens/invites/invite-capture.ts +107 -0
- package/src/screens/invites/invite-share.tsx +104 -0
- package/src/screens/invites/invites-ipc.ts +74 -0
- package/src/screens/invites/invites-screen.tsx +78 -0
- package/src/screens/invites/pending-invites-list.tsx +224 -0
- package/src/screens/settings/settings-page.tsx +2 -2
- package/src/screens/setup-user.tsx +29 -47
- package/src/system-apps/index.ts +3 -3
- package/src/system-apps/invites.app.ts +8 -0
- package/dist/screens/contacts/user-connect-invite.d.ts +0 -118
- package/dist/screens/contacts/user-connect-invite.js +0 -264
- package/dist/screens/contacts/user-connect-invite.test.d.ts +0 -1
- package/dist/screens/contacts/user-connect-invite.test.js +0 -215
- package/dist/screens/contacts/user-connect.d.ts +0 -8
- package/dist/screens/contacts/user-connect.js +0 -487
- package/dist/screens/contacts/user-connect.test.d.ts +0 -1
- package/dist/screens/contacts/user-connect.test.js +0 -291
- package/dist/screens/device-pairing/device-pairing-invite.d.ts +0 -10
- package/dist/screens/device-pairing/device-pairing-invite.js +0 -50
- package/dist/screens/device-pairing/device-pairing-invite.test.d.ts +0 -1
- package/dist/screens/device-pairing/device-pairing-invite.test.js +0 -30
- package/dist/screens/groups/group-invite-listener.d.ts +0 -14
- package/dist/screens/groups/group-invite-listener.js +0 -93
- package/dist/screens/join-group/index.d.ts +0 -2
- package/dist/screens/join-group/join-group.d.ts +0 -12
- package/dist/screens/join-group/join-group.js +0 -102
- package/dist/system-apps/join-group.app.js +0 -9
- package/src/screens/contacts/user-connect-invite.test.ts +0 -256
- package/src/screens/contacts/user-connect-invite.ts +0 -332
- package/src/screens/contacts/user-connect.test.tsx +0 -382
- package/src/screens/contacts/user-connect.tsx +0 -883
- package/src/screens/device-pairing/device-pairing-invite.test.ts +0 -50
- package/src/screens/device-pairing/device-pairing-invite.ts +0 -55
- package/src/screens/groups/group-invite-listener.tsx +0 -282
- package/src/screens/join-group/index.ts +0 -4
- package/src/screens/join-group/join-group.tsx +0 -296
- package/src/system-apps/join-group.app.ts +0 -8
package/biome.json
CHANGED
|
@@ -141,8 +141,8 @@
|
|
|
141
141
|
"includes": [
|
|
142
142
|
"src/screens/contacts/**/*.tsx",
|
|
143
143
|
"src/screens/contacts/**/*.ts",
|
|
144
|
-
"src/screens/
|
|
145
|
-
"src/screens/
|
|
144
|
+
"src/screens/invites/**/*.tsx",
|
|
145
|
+
"src/screens/invites/**/*.ts",
|
|
146
146
|
"src/screens/peer-types/**/*.tsx",
|
|
147
147
|
"src/screens/peer-types/**/*.ts",
|
|
148
148
|
"src/screens/variables/**/*.tsx",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Props for {@link QrScanner}. */
|
|
2
|
+
export interface QrScannerProps {
|
|
3
|
+
/** Called once with the decoded text of the first QR code seen; scanning stops after this. */
|
|
4
|
+
onScan: (text: string) => void;
|
|
5
|
+
/** Called when the camera cannot be opened. */
|
|
6
|
+
onError?: (message: string) => void;
|
|
7
|
+
/** Optional filter; return `false` to keep scanning past a code that is not ours. */
|
|
8
|
+
accept?: (text: string) => boolean;
|
|
9
|
+
/** Square edge length in CSS pixels. */
|
|
10
|
+
size?: number;
|
|
11
|
+
}
|
|
12
|
+
/** Whether this host can plausibly scan (secure context with a camera API). */
|
|
13
|
+
export declare function canScanQr(): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Live camera QR scanner. Uses the platform `BarcodeDetector` when available and falls
|
|
16
|
+
* back to `jsqr` on a canvas. Stops the camera on unmount or after the first accepted code.
|
|
17
|
+
*/
|
|
18
|
+
export declare function QrScanner(props: QrScannerProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.canScanQr = canScanQr;
|
|
37
|
+
exports.QrScanner = QrScanner;
|
|
38
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
39
|
+
const react_1 = require("react");
|
|
40
|
+
function nativeDetector() {
|
|
41
|
+
const ctor = globalThis.BarcodeDetector;
|
|
42
|
+
if (!ctor) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return new ctor({ formats: ["qr_code"] });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Whether this host can plausibly scan (secure context with a camera API). */
|
|
53
|
+
function canScanQr() {
|
|
54
|
+
return (typeof navigator !== "undefined" &&
|
|
55
|
+
Boolean(navigator.mediaDevices?.getUserMedia) &&
|
|
56
|
+
(typeof window === "undefined" || window.isSecureContext !== false));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Live camera QR scanner. Uses the platform `BarcodeDetector` when available and falls
|
|
60
|
+
* back to `jsqr` on a canvas. Stops the camera on unmount or after the first accepted code.
|
|
61
|
+
*/
|
|
62
|
+
function QrScanner(props) {
|
|
63
|
+
const { onScan, onError, accept, size = 280 } = props;
|
|
64
|
+
const videoRef = (0, react_1.useRef)(null);
|
|
65
|
+
const [status, setStatus] = (0, react_1.useState)("starting");
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
let stream;
|
|
69
|
+
let frame = 0;
|
|
70
|
+
const canvas = document.createElement("canvas");
|
|
71
|
+
const detector = nativeDetector();
|
|
72
|
+
let jsqr;
|
|
73
|
+
const stop = () => {
|
|
74
|
+
cancelled = true;
|
|
75
|
+
if (frame) {
|
|
76
|
+
cancelAnimationFrame(frame);
|
|
77
|
+
}
|
|
78
|
+
for (const track of stream?.getTracks() ?? []) {
|
|
79
|
+
track.stop();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const deliver = (text) => {
|
|
83
|
+
if (cancelled || (accept && !accept(text))) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
stop();
|
|
87
|
+
onScan(text);
|
|
88
|
+
return true;
|
|
89
|
+
};
|
|
90
|
+
const tick = async () => {
|
|
91
|
+
const video = videoRef.current;
|
|
92
|
+
if (cancelled || !video || video.readyState < 2) {
|
|
93
|
+
frame = requestAnimationFrame(() => void tick());
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (detector) {
|
|
98
|
+
const codes = await detector.detect(video);
|
|
99
|
+
for (const code of codes) {
|
|
100
|
+
if (deliver(code.rawValue)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (jsqr) {
|
|
106
|
+
const width = video.videoWidth;
|
|
107
|
+
const height = video.videoHeight;
|
|
108
|
+
if (width && height) {
|
|
109
|
+
canvas.width = width;
|
|
110
|
+
canvas.height = height;
|
|
111
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
112
|
+
if (ctx) {
|
|
113
|
+
ctx.drawImage(video, 0, 0, width, height);
|
|
114
|
+
const image = ctx.getImageData(0, 0, width, height);
|
|
115
|
+
const result = jsqr(image.data, width, height, { inversionAttempts: "dontInvert" });
|
|
116
|
+
if (result?.data && deliver(result.data)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// A single bad frame is not fatal; keep scanning.
|
|
125
|
+
}
|
|
126
|
+
if (!cancelled) {
|
|
127
|
+
frame = requestAnimationFrame(() => void tick());
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
(async () => {
|
|
131
|
+
try {
|
|
132
|
+
if (!detector) {
|
|
133
|
+
jsqr = (await Promise.resolve().then(() => __importStar(require("jsqr")))).default;
|
|
134
|
+
}
|
|
135
|
+
stream = await navigator.mediaDevices.getUserMedia({
|
|
136
|
+
video: { facingMode: { ideal: "environment" } },
|
|
137
|
+
audio: false,
|
|
138
|
+
});
|
|
139
|
+
if (cancelled) {
|
|
140
|
+
stop();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const video = videoRef.current;
|
|
144
|
+
if (!video) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
video.srcObject = stream;
|
|
148
|
+
await video.play();
|
|
149
|
+
setStatus("scanning");
|
|
150
|
+
frame = requestAnimationFrame(() => void tick());
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (cancelled) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
setStatus("error");
|
|
157
|
+
onError?.(error instanceof DOMException && error.name === "NotAllowedError"
|
|
158
|
+
? "Camera access was denied."
|
|
159
|
+
: "Could not open the camera.");
|
|
160
|
+
}
|
|
161
|
+
})();
|
|
162
|
+
return stop;
|
|
163
|
+
}, [onScan, onError, accept]);
|
|
164
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "position-relative bg-black rounded overflow-hidden mx-auto", style: { width: size, height: size }, children: [(0, jsx_runtime_1.jsx)("video", { ref: videoRef, muted: true, playsInline: true, style: { width: "100%", height: "100%", objectFit: "cover" }, "aria-label": "Camera preview" }), status === "starting" ? ((0, jsx_runtime_1.jsx)("output", { className: "position-absolute top-50 start-50 translate-middle text-light small", children: "Starting camera\u2026" })) : null, status === "scanning" ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": "true", className: "position-absolute top-50 start-50 translate-middle border border-2 border-light rounded", style: { width: "60%", height: "60%", opacity: 0.7 } })) : null] }));
|
|
165
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "../screens/console-logs/console-logs-list";
|
|
2
2
|
import "../screens/groups";
|
|
3
3
|
import "../screens/contacts";
|
|
4
|
-
import "../screens/
|
|
4
|
+
import "../screens/invites";
|
|
5
5
|
import "../screens/network-viewer";
|
|
6
6
|
import "../screens/data-explorer";
|
|
7
7
|
export declare function Router({ path: providedPath }?: {
|
|
@@ -62,7 +62,7 @@ const thread_message_list_1 = require("./messages/thread-message-list");
|
|
|
62
62
|
require("../screens/console-logs/console-logs-list");
|
|
63
63
|
require("../screens/groups");
|
|
64
64
|
require("../screens/contacts");
|
|
65
|
-
require("../screens/
|
|
65
|
+
require("../screens/invites");
|
|
66
66
|
require("../screens/network-viewer");
|
|
67
67
|
require("../screens/data-explorer");
|
|
68
68
|
function Router({ path: providedPath } = {}) {
|
|
@@ -20,8 +20,7 @@ const react_1 = require("react");
|
|
|
20
20
|
const command_palette_ui_1 = require("../command-palette/command-palette-ui");
|
|
21
21
|
const globals_1 = require("../globals");
|
|
22
22
|
const hooks_1 = require("../hooks");
|
|
23
|
-
const
|
|
24
|
-
const device_pairing_1 = require("../screens/device-pairing");
|
|
23
|
+
const invite_capture_1 = require("../screens/invites/invite-capture");
|
|
25
24
|
const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown");
|
|
26
25
|
const setup_user_1 = require("../screens/setup-user");
|
|
27
26
|
const welcome_modal_1 = require("../screens/welcome-modal");
|
|
@@ -37,8 +36,7 @@ const root_layout_state_1 = require("./root-layout-state");
|
|
|
37
36
|
*/
|
|
38
37
|
function RootLayoutHost() {
|
|
39
38
|
(0, react_1.useState)(() => {
|
|
40
|
-
(0,
|
|
41
|
-
(0, user_connect_invite_1.captureUserConnectInviteFromLocation)();
|
|
39
|
+
(0, invite_capture_1.captureInviteFromLocation)();
|
|
42
40
|
return true;
|
|
43
41
|
});
|
|
44
42
|
const userIdRef = (0, react_1.useRef)(undefined);
|
|
@@ -48,20 +46,22 @@ function RootLayoutHost() {
|
|
|
48
46
|
});
|
|
49
47
|
userIdRef.current = userId;
|
|
50
48
|
(0, react_1.useEffect)(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
(0, globals_1.mainContentPath)("contacts/connect");
|
|
49
|
+
// Tokens arriving after startup: a hash change in the browser/PWA, or a peers:// open
|
|
50
|
+
// forwarded by the Electron main process. Signed-out captures stay stashed until setup
|
|
51
|
+
// finishes; signed-in captures route straight to the Accept screen.
|
|
52
|
+
const onHashChange = () => (0, invite_capture_1.captureInviteFromLocation)();
|
|
53
|
+
window.addEventListener("hashchange", onHashChange);
|
|
54
|
+
const stopOpenUrl = (0, invite_capture_1.listenForOpenUrlInvites)();
|
|
55
|
+
const sub = invite_capture_1.inviteCaptured.subscribe(() => {
|
|
56
|
+
if (userIdRef.current && (0, invite_capture_1.peekPendingInvite)()) {
|
|
57
|
+
(0, globals_1.mainContentPath)(invite_capture_1.ACCEPT_INVITE_PATH);
|
|
61
58
|
}
|
|
59
|
+
});
|
|
60
|
+
return () => {
|
|
61
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
62
|
+
stopOpenUrl();
|
|
63
|
+
sub.dispose();
|
|
62
64
|
};
|
|
63
|
-
window.addEventListener("hashchange", captureInviteFromHashChange);
|
|
64
|
-
return () => window.removeEventListener("hashchange", captureInviteFromHashChange);
|
|
65
65
|
}, []);
|
|
66
66
|
if (userId === undefined) {
|
|
67
67
|
return;
|
|
@@ -92,9 +92,8 @@ function RootLayoutBootstrap(props) {
|
|
|
92
92
|
tabs_state_1.recentlyUsedApps.loadingPromise,
|
|
93
93
|
peers_sdk_1.hasShownWelcomeModal.loadingPromise,
|
|
94
94
|
]);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
(0, globals_1.mainContentPath)("contacts/connect");
|
|
95
|
+
if ((0, invite_capture_1.peekPendingInvite)()) {
|
|
96
|
+
(0, globals_1.mainContentPath)(invite_capture_1.ACCEPT_INVITE_PATH);
|
|
98
97
|
}
|
|
99
98
|
return true;
|
|
100
99
|
});
|
|
@@ -3,8 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
4
|
const react_1 = require("@testing-library/react");
|
|
5
5
|
const globals_1 = require("../globals");
|
|
6
|
-
const
|
|
6
|
+
const invite_capture_1 = require("../screens/invites/invite-capture");
|
|
7
7
|
const root_layout_host_1 = require("./root-layout-host");
|
|
8
|
+
const TOKEN = "eyJhbGciOiJub25lIn0-_abcdefghijklmnopqrstuvwxyz0123456789";
|
|
8
9
|
jest.mock("@peers-app/peers-sdk", () => {
|
|
9
10
|
const actual = jest.requireActual("@peers-app/peers-sdk");
|
|
10
11
|
return {
|
|
@@ -71,14 +72,18 @@ describe("RootLayoutHost", () => {
|
|
|
71
72
|
});
|
|
72
73
|
it("captures and sanitizes invite links opened after startup", () => {
|
|
73
74
|
(0, react_1.render)((0, jsx_runtime_1.jsx)(root_layout_host_1.RootLayoutHost, {}));
|
|
74
|
-
window.history.replaceState(null, "", `/?app#
|
|
75
|
+
window.history.replaceState(null, "", `/?app#invite=${TOKEN}`);
|
|
75
76
|
(0, react_1.fireEvent)(window, new HashChangeEvent("hashchange"));
|
|
76
|
-
expect(window.location.hash).toBe("
|
|
77
|
-
expect((0,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
expect(window.location.hash).toBe("");
|
|
78
|
+
expect((0, invite_capture_1.peekPendingInvite)()).toBe(TOKEN);
|
|
79
|
+
expect(globals_1.mainContentPath).toHaveBeenCalledWith(invite_capture_1.ACCEPT_INVITE_PATH);
|
|
80
|
+
expect(globals_1.mainContentPath).not.toHaveBeenCalledWith(expect.stringContaining(TOKEN));
|
|
81
|
+
});
|
|
82
|
+
it("ignores ordinary hash routes", () => {
|
|
83
|
+
(0, react_1.render)((0, jsx_runtime_1.jsx)(root_layout_host_1.RootLayoutHost, {}));
|
|
84
|
+
window.history.replaceState(null, "", "/?app#groups");
|
|
85
|
+
(0, react_1.fireEvent)(window, new HashChangeEvent("hashchange"));
|
|
86
|
+
expect(window.location.hash).toBe("#groups");
|
|
87
|
+
expect((0, invite_capture_1.peekPendingInvite)()).toBeUndefined();
|
|
83
88
|
});
|
|
84
89
|
});
|
|
@@ -4,9 +4,60 @@ exports.AccountScreen = AccountScreen;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
6
6
|
const react_1 = require("react");
|
|
7
|
+
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
7
8
|
const tooltip_1 = require("../../components/tooltip");
|
|
8
9
|
const hooks_1 = require("../../hooks");
|
|
9
10
|
const peers_services_registration_1 = require("../../peers-services-registration");
|
|
11
|
+
const invite_share_1 = require("../invites/invite-share");
|
|
12
|
+
const invites_ipc_1 = require("../invites/invites-ipc");
|
|
13
|
+
const PROFILE_QR_INFO = `### Your profile QR
|
|
14
|
+
|
|
15
|
+
Anyone who scans this (or opens the link) can send you a contact request. Requests show up under **Contacts → Pending** and nothing happens until you confirm.
|
|
16
|
+
|
|
17
|
+
### Regenerate
|
|
18
|
+
|
|
19
|
+
Regenerating invalidates this QR **and every other invite link you have issued**. Use it if the QR leaked somewhere you did not intend.`;
|
|
20
|
+
/** Reusable profile invite with a regenerate control that bumps the invite epoch. */
|
|
21
|
+
function ProfileInviteSection() {
|
|
22
|
+
const [invite, setInvite] = (0, react_1.useState)(undefined);
|
|
23
|
+
const [busy, setBusy] = (0, react_1.useState)(false);
|
|
24
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
25
|
+
(0, react_1.useEffect)(() => {
|
|
26
|
+
let cancelled = false;
|
|
27
|
+
void (0, invites_ipc_1.inviteTools)()
|
|
28
|
+
.then((tools) => tools.createContactInvite({ profile: true }))
|
|
29
|
+
.then(({ invite: created }) => {
|
|
30
|
+
if (!cancelled)
|
|
31
|
+
setInvite(created);
|
|
32
|
+
})
|
|
33
|
+
.catch((err) => {
|
|
34
|
+
if (!cancelled)
|
|
35
|
+
setError(err.message || "Could not load your profile QR.");
|
|
36
|
+
});
|
|
37
|
+
return () => {
|
|
38
|
+
cancelled = true;
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
const regenerate = async () => {
|
|
42
|
+
if (!confirm("Regenerate your profile QR? Every invite link you have issued so far will stop working.")) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
setBusy(true);
|
|
46
|
+
setError(null);
|
|
47
|
+
try {
|
|
48
|
+
const tools = await (0, invites_ipc_1.inviteTools)();
|
|
49
|
+
const { invite: created } = await tools.regenerateProfileInvite();
|
|
50
|
+
setInvite(created);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
setError(err.message || "Could not regenerate.");
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
setBusy(false);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("h5", { className: "card-title d-flex align-items-center gap-2", children: ["Profile QR", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: PROFILE_QR_INFO })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }) : null, invite ? ((0, jsx_runtime_1.jsx)(invite_share_1.InviteShare, { invite: invite, description: "Let people connect with you by scanning this or opening the link. You confirm each request.", shareText: "Connect with me on Peers:" })) : !error ? ((0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {})) : null, (0, jsx_runtime_1.jsx)("div", { className: "text-center mt-3", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-danger btn-sm", onClick: () => void regenerate(), disabled: busy || !invite, children: busy ? "Regenerating…" : "Regenerate (revokes all invites)" }) })] }) }));
|
|
60
|
+
}
|
|
10
61
|
const INFO_TEXT = `### What does registering do?
|
|
11
62
|
|
|
12
63
|
Registering your account links your **public key** with the Peers cloud service. This enables cloud-powered features like:
|
|
@@ -45,7 +96,7 @@ function AccountScreen() {
|
|
|
45
96
|
setIsRegistering(false);
|
|
46
97
|
}
|
|
47
98
|
};
|
|
48
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", style: { maxWidth: 600 }, children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-3 d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-circle" }), "Account"] }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-2" }), error, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setError(null), "aria-label": "Close" })] })), success && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-success alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle me-2" }), success, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setSuccess(null), "aria-label": "Close" })] })), (0, jsx_runtime_1.jsx)("div", { className: "card", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("h5", { className: "card-title d-flex align-items-center gap-2", children: ["Peers Services Registration", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: INFO_TEXT })] }), isRegistered ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2 mb-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle-fill text-success", style: { fontSize: "1.2em" } }), (0, jsx_runtime_1.jsx)("span", { children: "Registered" })] }), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted d-block mb-3", children: ["Registered on", " ", new Date(registrationStatus).toLocaleDateString(undefined, {
|
|
99
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", style: { maxWidth: 600 }, children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-3 d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-circle" }), "Account"] }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-2" }), error, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setError(null), "aria-label": "Close" })] })), success && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-success alert-dismissible", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle me-2" }), success, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn-close", onClick: () => setSuccess(null), "aria-label": "Close" })] })), (0, jsx_runtime_1.jsx)(ProfileInviteSection, {}), (0, jsx_runtime_1.jsx)("div", { className: "card", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("h5", { className: "card-title d-flex align-items-center gap-2", children: ["Peers Services Registration", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: INFO_TEXT })] }), isRegistered ? ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2 mb-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle-fill text-success", style: { fontSize: "1.2em" } }), (0, jsx_runtime_1.jsx)("span", { children: "Registered" })] }), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted d-block mb-3", children: ["Registered on", " ", new Date(registrationStatus).toLocaleDateString(undefined, {
|
|
49
100
|
year: "numeric",
|
|
50
101
|
month: "long",
|
|
51
102
|
day: "numeric",
|
|
@@ -11,7 +11,34 @@ const trust_level_badge_1 = require("../../components/trust-level-badge");
|
|
|
11
11
|
const globals_1 = require("../../globals");
|
|
12
12
|
const hooks_1 = require("../../hooks");
|
|
13
13
|
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
14
|
+
const invite_capture_1 = require("../invites/invite-capture");
|
|
15
|
+
const invite_share_1 = require("../invites/invite-share");
|
|
16
|
+
const invites_ipc_1 = require("../invites/invites-ipc");
|
|
17
|
+
const pending_invites_list_1 = require("../invites/pending-invites-list");
|
|
18
|
+
/** Issues a one-off contact invite and shows it as QR/link until dismissed. */
|
|
19
|
+
function InviteContactPanel({ onClose }) {
|
|
20
|
+
const [invite, setInvite] = (0, react_1.useState)(undefined);
|
|
21
|
+
const [error, setError] = (0, react_1.useState)("");
|
|
22
|
+
(0, react_1.useEffect)(() => {
|
|
23
|
+
let cancelled = false;
|
|
24
|
+
void (0, invites_ipc_1.inviteTools)()
|
|
25
|
+
.then((tools) => tools.createContactInvite({}))
|
|
26
|
+
.then(({ invite: created }) => {
|
|
27
|
+
if (!cancelled)
|
|
28
|
+
setInvite(created);
|
|
29
|
+
})
|
|
30
|
+
.catch((err) => {
|
|
31
|
+
if (!cancelled)
|
|
32
|
+
setError(err.message || "Could not create an invite.");
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
cancelled = true;
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title", children: "Invite a contact" }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }) : null, invite ? ((0, jsx_runtime_1.jsx)(invite_share_1.InviteShare, { invite: invite, description: "Send this link or show the QR. When they accept, you are connected \u2014 no need to be online at the same time.", shareText: "Connect with me on Peers:", onDone: onClose })) : !error ? ((0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {})) : null] }) }));
|
|
39
|
+
}
|
|
14
40
|
function ContactList() {
|
|
41
|
+
const [inviting, setInviting] = (0, react_1.useState)(false);
|
|
15
42
|
const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
|
|
16
43
|
const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
|
|
17
44
|
const [cursorObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)());
|
|
@@ -98,7 +125,7 @@ function ContactList() {
|
|
|
98
125
|
});
|
|
99
126
|
return elements;
|
|
100
127
|
}
|
|
101
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("h4", { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-fill-check me-2" }), "Contacts"] }), (0, jsx_runtime_1.jsxs)("a", { href: "
|
|
128
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("h4", { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-fill-check me-2" }), "Contacts"] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsxs)("a", { href: `#${invite_capture_1.ACCEPT_INVITE_PATH}`, className: "btn btn-outline-primary btn-sm", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-qr-code-scan me-1" }), "Accept invite"] }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-primary btn-sm", onClick: () => setInviting(true), disabled: inviting, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-plus me-1" }), "Invite"] })] })] }), inviting ? (0, jsx_runtime_1.jsx)(InviteContactPanel, { onClose: () => setInviting(false) }) : null, (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { type: "contact", hideWhenEmpty: true, title: "Pending contact invites" }), (0, jsx_runtime_1.jsx)("div", { className: "input-group mb-3", children: (0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search or create contacts", autoFocus: true, onKeyUp: handleSearchSubmit }) }), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: searchText, loadMore: loadMore, scrollThreshold: 0.6, renderItems: renderItems, loadingIndicator: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 }, children: (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) }), endOfList: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 } }) }) })] }));
|
|
102
129
|
}
|
|
103
130
|
const ContactCard = ({ contact }) => {
|
|
104
131
|
const getSourceLabel = (source) => {
|
|
@@ -17,7 +17,5 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
17
17
|
// Import all contact screen components to ensure they register their routes
|
|
18
18
|
require("./contact-list");
|
|
19
19
|
require("./contact-details");
|
|
20
|
-
require("./user-connect");
|
|
21
20
|
__exportStar(require("./contact-details"), exports);
|
|
22
21
|
__exportStar(require("./contact-list"), exports);
|
|
23
|
-
__exportStar(require("./user-connect"), exports);
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
/** Signed-in source flow opened from Settings. */
|
|
1
|
+
/** Signed-in source flow opened from Settings: type the code shown on the new device, then approve. */
|
|
2
2
|
export declare function SourceDevicePairing(props: {
|
|
3
3
|
onClose: () => void;
|
|
4
4
|
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
-
/** Signed-out destination flow
|
|
5
|
+
/** Signed-out destination flow: shows a code and QR, then installs credentials after approval. */
|
|
6
6
|
export declare function DestinationDevicePairing(props: {
|
|
7
|
-
initialInvitation?: string;
|
|
8
|
-
onInvitationConsumed?: () => void;
|
|
9
7
|
onBack: () => void;
|
|
10
8
|
}): import("react/jsx-runtime").JSX.Element;
|