@peers-app/peers-ui 0.20.6 → 0.21.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/dist/root-layout/root-layout-host.js +3 -0
- package/dist/screens/device-pairing/device-pairing-invite.d.ts +10 -0
- package/dist/screens/device-pairing/device-pairing-invite.js +50 -0
- package/dist/screens/device-pairing/device-pairing-invite.test.d.ts +1 -0
- package/dist/screens/device-pairing/device-pairing-invite.test.js +30 -0
- package/dist/screens/device-pairing/device-pairing.d.ts +10 -0
- package/dist/screens/device-pairing/device-pairing.js +237 -0
- package/dist/screens/device-pairing/device-pairing.test.d.ts +1 -0
- package/dist/screens/device-pairing/device-pairing.test.js +88 -0
- package/dist/screens/device-pairing/index.d.ts +3 -0
- package/dist/screens/device-pairing/index.js +19 -0
- package/dist/screens/device-pairing/use-device-pairing-state.d.ts +3 -0
- package/dist/screens/device-pairing/use-device-pairing-state.js +31 -0
- package/dist/screens/settings/settings-page.js +6 -1
- package/dist/screens/setup-user.js +24 -1
- package/package.json +3 -3
- package/src/root-layout/root-layout-host.tsx +3 -0
- package/src/screens/device-pairing/device-pairing-invite.test.ts +50 -0
- package/src/screens/device-pairing/device-pairing-invite.ts +55 -0
- package/src/screens/device-pairing/device-pairing.test.tsx +100 -0
- package/src/screens/device-pairing/device-pairing.tsx +390 -0
- package/src/screens/device-pairing/index.ts +3 -0
- package/src/screens/device-pairing/use-device-pairing-state.ts +38 -0
- package/src/screens/settings/settings-page.tsx +22 -0
- package/src/screens/setup-user.tsx +63 -1
|
@@ -21,6 +21,7 @@ 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
23
|
const user_connect_invite_1 = require("../screens/contacts/user-connect-invite");
|
|
24
|
+
const device_pairing_1 = require("../screens/device-pairing");
|
|
24
25
|
const color_mode_dropdown_1 = require("../screens/settings/color-mode-dropdown");
|
|
25
26
|
const setup_user_1 = require("../screens/setup-user");
|
|
26
27
|
const welcome_modal_1 = require("../screens/welcome-modal");
|
|
@@ -36,6 +37,7 @@ const root_layout_state_1 = require("./root-layout-state");
|
|
|
36
37
|
*/
|
|
37
38
|
function RootLayoutHost() {
|
|
38
39
|
(0, react_1.useState)(() => {
|
|
40
|
+
(0, device_pairing_1.captureDevicePairingInviteFromLocation)();
|
|
39
41
|
(0, user_connect_invite_1.captureUserConnectInviteFromLocation)();
|
|
40
42
|
return true;
|
|
41
43
|
});
|
|
@@ -48,6 +50,7 @@ function RootLayoutHost() {
|
|
|
48
50
|
(0, react_1.useEffect)(() => {
|
|
49
51
|
const captureInviteFromHashChange = () => {
|
|
50
52
|
const previousHash = window.location.hash;
|
|
53
|
+
(0, device_pairing_1.captureDevicePairingInviteFromLocation)();
|
|
51
54
|
(0, user_connect_invite_1.captureUserConnectInviteFromLocation)();
|
|
52
55
|
if (window.location.hash === previousHash) {
|
|
53
56
|
return;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Browser event emitted after a pairing invitation is captured and removed from the URL. */
|
|
2
|
+
export declare const DEVICE_PAIRING_INVITE_CAPTURED_EVENT = "device-pairing:invite-captured";
|
|
3
|
+
/** Captures a pairing fragment in memory and immediately removes it from browser history. */
|
|
4
|
+
export declare function captureDevicePairingInviteFromLocation(location?: Location): string | undefined;
|
|
5
|
+
/** Returns the in-memory invitation without consuming it. */
|
|
6
|
+
export declare function readPendingDevicePairingInvite(): string | undefined;
|
|
7
|
+
/** Returns and erases the in-memory invitation. */
|
|
8
|
+
export declare function consumePendingDevicePairingInvite(): string | undefined;
|
|
9
|
+
/** Erases any unconsumed invitation. */
|
|
10
|
+
export declare function clearPendingDevicePairingInvite(): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT = void 0;
|
|
4
|
+
exports.captureDevicePairingInviteFromLocation = captureDevicePairingInviteFromLocation;
|
|
5
|
+
exports.readPendingDevicePairingInvite = readPendingDevicePairingInvite;
|
|
6
|
+
exports.consumePendingDevicePairingInvite = consumePendingDevicePairingInvite;
|
|
7
|
+
exports.clearPendingDevicePairingInvite = clearPendingDevicePairingInvite;
|
|
8
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
9
|
+
let pendingInvitation;
|
|
10
|
+
/** Browser event emitted after a pairing invitation is captured and removed from the URL. */
|
|
11
|
+
exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT = "device-pairing:invite-captured";
|
|
12
|
+
/** Captures a pairing fragment in memory and immediately removes it from browser history. */
|
|
13
|
+
function captureDevicePairingInviteFromLocation(location = window.location) {
|
|
14
|
+
if (!location.hash.startsWith("#pair=") && !location.hash.startsWith("#pair?")) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
let capturedInvitation;
|
|
18
|
+
try {
|
|
19
|
+
capturedInvitation = (0, peers_sdk_1.encodeDevicePairingInvitation)((0, peers_sdk_1.parseDevicePairingInvitation)(location.href));
|
|
20
|
+
pendingInvitation = capturedInvitation;
|
|
21
|
+
return capturedInvitation;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
if (typeof window !== "undefined" && location === window.location) {
|
|
28
|
+
window.history.replaceState(window.history.state, document.title, `${location.pathname}${location.search}`);
|
|
29
|
+
if (capturedInvitation) {
|
|
30
|
+
window.dispatchEvent(new CustomEvent(exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT, {
|
|
31
|
+
detail: capturedInvitation,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Returns the in-memory invitation without consuming it. */
|
|
38
|
+
function readPendingDevicePairingInvite() {
|
|
39
|
+
return pendingInvitation;
|
|
40
|
+
}
|
|
41
|
+
/** Returns and erases the in-memory invitation. */
|
|
42
|
+
function consumePendingDevicePairingInvite() {
|
|
43
|
+
const invitation = pendingInvitation;
|
|
44
|
+
pendingInvitation = undefined;
|
|
45
|
+
return invitation;
|
|
46
|
+
}
|
|
47
|
+
/** Erases any unconsumed invitation. */
|
|
48
|
+
function clearPendingDevicePairingInvite() {
|
|
49
|
+
pendingInvitation = undefined;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
4
|
+
const device_pairing_invite_1 = require("./device-pairing-invite");
|
|
5
|
+
describe("device pairing invitation capture", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
(0, device_pairing_invite_1.clearPendingDevicePairingInvite)();
|
|
8
|
+
window.history.replaceState({}, "", "/?app");
|
|
9
|
+
});
|
|
10
|
+
it("captures the fragment only in memory and removes it from browser history", () => {
|
|
11
|
+
const invitation = (0, peers_sdk_1.createDevicePairingInvitation)({
|
|
12
|
+
serviceOrigin: "https://peers.app",
|
|
13
|
+
});
|
|
14
|
+
const inviteUrl = (0, peers_sdk_1.createDevicePairingInvitationUrl)(`${window.location.origin}/?app`, invitation);
|
|
15
|
+
window.history.replaceState({}, "", new URL(inviteUrl).pathname + new URL(inviteUrl).search + new URL(inviteUrl).hash);
|
|
16
|
+
const token = (0, device_pairing_invite_1.captureDevicePairingInviteFromLocation)();
|
|
17
|
+
expect(token).toBeDefined();
|
|
18
|
+
expect((0, peers_sdk_1.parseDevicePairingInvitation)(token ?? "")).toEqual(invitation);
|
|
19
|
+
expect(window.location.hash).toBe("");
|
|
20
|
+
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBe(token);
|
|
21
|
+
expect((0, device_pairing_invite_1.consumePendingDevicePairingInvite)()).toBe(token);
|
|
22
|
+
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
it("ignores invalid fragments without retaining them", () => {
|
|
25
|
+
window.history.replaceState({}, "", "/?app#pair=invalid");
|
|
26
|
+
expect((0, device_pairing_invite_1.captureDevicePairingInviteFromLocation)()).toBeUndefined();
|
|
27
|
+
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBeUndefined();
|
|
28
|
+
expect(window.location.hash).toBe("");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Signed-in source flow opened from Settings. */
|
|
2
|
+
export declare function SourceDevicePairing(props: {
|
|
3
|
+
onClose: () => void;
|
|
4
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
/** Signed-out destination flow opened by a QR link or manual invitation paste. */
|
|
6
|
+
export declare function DestinationDevicePairing(props: {
|
|
7
|
+
initialInvitation?: string;
|
|
8
|
+
onInvitationConsumed?: () => void;
|
|
9
|
+
onBack: () => void;
|
|
10
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SourceDevicePairing = SourceDevicePairing;
|
|
7
|
+
exports.DestinationDevicePairing = DestinationDevicePairing;
|
|
8
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
9
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
10
|
+
const react_1 = require("react");
|
|
11
|
+
const react_qr_code_1 = __importDefault(require("react-qr-code"));
|
|
12
|
+
const use_device_pairing_state_1 = require("./use-device-pairing-state");
|
|
13
|
+
function formatCountdown(expiresAt) {
|
|
14
|
+
if (!expiresAt) {
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
const seconds = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000));
|
|
18
|
+
return `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
19
|
+
}
|
|
20
|
+
function PairingProgress({ state }) {
|
|
21
|
+
const messages = {
|
|
22
|
+
inviting: "Waiting for the new device…",
|
|
23
|
+
connecting: "Establishing a secure connection…",
|
|
24
|
+
"waiting-for-approval": "Waiting for approval on the other device…",
|
|
25
|
+
transferring: "Installing encrypted account credentials…",
|
|
26
|
+
};
|
|
27
|
+
return ((0, jsx_runtime_1.jsxs)("output", { className: "text-center py-4 d-block", children: [(0, jsx_runtime_1.jsx)("div", { className: "spinner-border text-primary mb-3", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-0", children: messages[state.phase] ?? "Working…" })] }));
|
|
28
|
+
}
|
|
29
|
+
function PairingComparison(props) {
|
|
30
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 440, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("h4", { children: "Compare these numbers" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Confirm that both devices show the same number and the expected device before approving." }), (0, jsx_runtime_1.jsx)("div", { className: "display-4 font-monospace text-primary mb-3", style: { letterSpacing: "0.12em" }, children: props.state.sas }), (0, jsx_runtime_1.jsxs)("p", { className: "mb-4", children: [props.state.remoteDevice?.deviceName || "Unnamed device", props.state.remoteDevice?.deviceType ? ` (${props.state.remoteDevice.deviceType})` : ""] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", disabled: props.approving, onClick: props.onApprove, children: props.approving ? "Approving…" : "Numbers match — approve" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-danger", disabled: props.approving, onClick: props.onReject, children: "Reject" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", disabled: props.approving, onClick: props.onCancel, children: "Cancel" })] })] }));
|
|
31
|
+
}
|
|
32
|
+
function TerminalPanel(props) {
|
|
33
|
+
const complete = props.state.phase === "complete";
|
|
34
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 440, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("output", { className: `alert d-block ${complete ? "alert-success" : "alert-warning"}`, children: complete
|
|
35
|
+
? "Device added successfully."
|
|
36
|
+
: props.state.phase === "rejected"
|
|
37
|
+
? "Pairing was rejected. No credentials were installed."
|
|
38
|
+
: props.state.error || "Pairing could not be completed. No credentials were installed." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onClose, children: complete ? "Done" : "Back" })] }));
|
|
39
|
+
}
|
|
40
|
+
function ProgressWithCancel(props) {
|
|
41
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(PairingProgress, { state: props.state }), (0, jsx_runtime_1.jsx)("div", { className: "text-center", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onCancel, children: "Cancel" }) })] }));
|
|
42
|
+
}
|
|
43
|
+
function usePairingSessionLifecycle(state) {
|
|
44
|
+
const sessionId = (0, react_1.useRef)(undefined);
|
|
45
|
+
const phase = (0, react_1.useRef)("idle");
|
|
46
|
+
const mounted = (0, react_1.useRef)(true);
|
|
47
|
+
(0, react_1.useEffect)(() => {
|
|
48
|
+
if (state.sessionId && state.sessionId === sessionId.current) {
|
|
49
|
+
phase.current = state.phase;
|
|
50
|
+
}
|
|
51
|
+
}, [state.phase, state.sessionId]);
|
|
52
|
+
(0, react_1.useEffect)(() => {
|
|
53
|
+
mounted.current = true;
|
|
54
|
+
return () => {
|
|
55
|
+
mounted.current = false;
|
|
56
|
+
const activeSessionId = sessionId.current;
|
|
57
|
+
if (activeSessionId && !["idle", "complete", "rejected", "error"].includes(phase.current)) {
|
|
58
|
+
void peers_sdk_1.rpcServerCalls
|
|
59
|
+
.devicePairingCancel({ sessionId: activeSessionId })
|
|
60
|
+
.catch(() => undefined);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
return { sessionId, phase, mounted };
|
|
65
|
+
}
|
|
66
|
+
/** Signed-in source flow opened from Settings. */
|
|
67
|
+
function SourceDevicePairing(props) {
|
|
68
|
+
const state = (0, use_device_pairing_state_1.useDevicePairingState)();
|
|
69
|
+
const lifecycle = usePairingSessionLifecycle(state);
|
|
70
|
+
const started = (0, react_1.useRef)(false);
|
|
71
|
+
const [invitationUrl, setInvitationUrl] = (0, react_1.useState)("");
|
|
72
|
+
const [localError, setLocalError] = (0, react_1.useState)("");
|
|
73
|
+
const [approving, setApproving] = (0, react_1.useState)(false);
|
|
74
|
+
const [, setClock] = (0, react_1.useState)(0);
|
|
75
|
+
(0, react_1.useEffect)(() => {
|
|
76
|
+
if (started.current) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
started.current = true;
|
|
80
|
+
void peers_sdk_1.rpcServerCalls
|
|
81
|
+
.devicePairingStartSource()
|
|
82
|
+
.then(async (result) => {
|
|
83
|
+
lifecycle.sessionId.current = result.state.sessionId;
|
|
84
|
+
lifecycle.phase.current = result.state.phase;
|
|
85
|
+
if (!lifecycle.mounted.current && result.state.sessionId) {
|
|
86
|
+
await peers_sdk_1.rpcServerCalls
|
|
87
|
+
.devicePairingCancel({ sessionId: result.state.sessionId })
|
|
88
|
+
.catch(() => undefined);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setInvitationUrl(result.invitationUrl);
|
|
92
|
+
})
|
|
93
|
+
.catch(() => setLocalError("Could not create a pairing invitation."));
|
|
94
|
+
}, []);
|
|
95
|
+
(0, react_1.useEffect)(() => {
|
|
96
|
+
if (!state.expiresAt) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const timer = window.setInterval(() => setClock((value) => value + 1), 1000);
|
|
100
|
+
return () => window.clearInterval(timer);
|
|
101
|
+
}, [state.expiresAt]);
|
|
102
|
+
(0, react_1.useEffect)(() => {
|
|
103
|
+
if (state.phase !== "inviting") {
|
|
104
|
+
setInvitationUrl("");
|
|
105
|
+
}
|
|
106
|
+
}, [state.phase]);
|
|
107
|
+
const act = async (action) => {
|
|
108
|
+
const sessionId = lifecycle.sessionId.current;
|
|
109
|
+
if (!sessionId) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
setApproving(true);
|
|
113
|
+
try {
|
|
114
|
+
if (action === "approve") {
|
|
115
|
+
await peers_sdk_1.rpcServerCalls.devicePairingApprove({ sessionId });
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
await peers_sdk_1.rpcServerCalls.devicePairingReject({ sessionId });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
setLocalError("Could not update the pairing ceremony.");
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
setApproving(false);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const cancel = async () => {
|
|
129
|
+
const sessionId = lifecycle.sessionId.current;
|
|
130
|
+
lifecycle.phase.current = "idle";
|
|
131
|
+
if (sessionId) {
|
|
132
|
+
await peers_sdk_1.rpcServerCalls.devicePairingCancel({ sessionId }).catch(() => undefined);
|
|
133
|
+
}
|
|
134
|
+
props.onClose();
|
|
135
|
+
};
|
|
136
|
+
if (localError) {
|
|
137
|
+
return (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger", children: localError });
|
|
138
|
+
}
|
|
139
|
+
if (["complete", "rejected", "error"].includes(state.phase)) {
|
|
140
|
+
return (0, jsx_runtime_1.jsx)(TerminalPanel, { state: state, onClose: props.onClose });
|
|
141
|
+
}
|
|
142
|
+
if (state.phase === "comparing" && state.sas) {
|
|
143
|
+
return ((0, jsx_runtime_1.jsx)(PairingComparison, { state: state, approving: approving, onApprove: () => void act("approve"), onReject: () => void act("reject"), onCancel: () => void cancel() }));
|
|
144
|
+
}
|
|
145
|
+
if (state.phase === "waiting-for-approval" || state.phase === "transferring") {
|
|
146
|
+
return (0, jsx_runtime_1.jsx)(ProgressWithCancel, { state: state, onCancel: () => void cancel() });
|
|
147
|
+
}
|
|
148
|
+
if (invitationUrl) {
|
|
149
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 440, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Scan this on the signed-out device you want to add." }), (0, jsx_runtime_1.jsx)("div", { className: "d-inline-block bg-white rounded p-3 mb-3", children: (0, jsx_runtime_1.jsx)(react_qr_code_1.default, { value: invitationUrl, size: 220, "aria-label": "Device pairing QR code" }) }), (0, jsx_runtime_1.jsxs)("p", { className: "small text-muted", children: ["Expires in ", formatCountdown(state.expiresAt)] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-primary", onClick: () => void (0, peers_sdk_1.copyToClipboard)(invitationUrl), children: "Copy pairing link" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: () => void cancel(), children: "Cancel" })] })] }));
|
|
150
|
+
}
|
|
151
|
+
return (0, jsx_runtime_1.jsx)(PairingProgress, { state: state });
|
|
152
|
+
}
|
|
153
|
+
/** Signed-out destination flow opened by a QR link or manual invitation paste. */
|
|
154
|
+
function DestinationDevicePairing(props) {
|
|
155
|
+
const state = (0, use_device_pairing_state_1.useDevicePairingState)();
|
|
156
|
+
const lifecycle = usePairingSessionLifecycle(state);
|
|
157
|
+
const [invitation, setInvitation] = (0, react_1.useState)(props.initialInvitation ?? "");
|
|
158
|
+
const [localError, setLocalError] = (0, react_1.useState)("");
|
|
159
|
+
const [approving, setApproving] = (0, react_1.useState)(false);
|
|
160
|
+
const autoStarted = (0, react_1.useRef)(false);
|
|
161
|
+
const start = async (value = invitation) => {
|
|
162
|
+
setLocalError("");
|
|
163
|
+
try {
|
|
164
|
+
const result = await peers_sdk_1.rpcServerCalls.devicePairingStartDestination({
|
|
165
|
+
invitation: value.trim(),
|
|
166
|
+
deviceType: /Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "browser",
|
|
167
|
+
});
|
|
168
|
+
lifecycle.sessionId.current = result.sessionId;
|
|
169
|
+
lifecycle.phase.current = result.phase;
|
|
170
|
+
if (!lifecycle.mounted.current && result.sessionId) {
|
|
171
|
+
await peers_sdk_1.rpcServerCalls
|
|
172
|
+
.devicePairingCancel({ sessionId: result.sessionId })
|
|
173
|
+
.catch(() => undefined);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
setInvitation("");
|
|
177
|
+
props.onInvitationConsumed?.();
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
setLocalError("That pairing invitation is invalid, expired, or unavailable.");
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
(0, react_1.useEffect)(() => {
|
|
184
|
+
if (!props.initialInvitation || autoStarted.current) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
autoStarted.current = true;
|
|
188
|
+
void start(props.initialInvitation);
|
|
189
|
+
}, [props.initialInvitation]);
|
|
190
|
+
(0, react_1.useEffect)(() => {
|
|
191
|
+
if (state.phase !== "complete") {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
window.setTimeout(() => window.location.reload(), 750);
|
|
195
|
+
}, [state.phase]);
|
|
196
|
+
const act = async (action) => {
|
|
197
|
+
const sessionId = lifecycle.sessionId.current;
|
|
198
|
+
if (!sessionId) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
setApproving(true);
|
|
202
|
+
try {
|
|
203
|
+
if (action === "approve") {
|
|
204
|
+
await peers_sdk_1.rpcServerCalls.devicePairingApprove({ sessionId });
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
await peers_sdk_1.rpcServerCalls.devicePairingReject({ sessionId });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
setLocalError("Could not update the pairing ceremony.");
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
setApproving(false);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const cancel = async () => {
|
|
218
|
+
const sessionId = lifecycle.sessionId.current;
|
|
219
|
+
lifecycle.phase.current = "idle";
|
|
220
|
+
if (sessionId) {
|
|
221
|
+
await peers_sdk_1.rpcServerCalls.devicePairingCancel({ sessionId }).catch(() => undefined);
|
|
222
|
+
}
|
|
223
|
+
props.onBack();
|
|
224
|
+
};
|
|
225
|
+
if (["complete", "rejected", "error"].includes(state.phase)) {
|
|
226
|
+
return (0, jsx_runtime_1.jsx)(TerminalPanel, { state: state, onClose: props.onBack });
|
|
227
|
+
}
|
|
228
|
+
if (state.phase === "comparing" && state.sas) {
|
|
229
|
+
return ((0, jsx_runtime_1.jsx)(PairingComparison, { state: state, approving: approving, onApprove: () => void act("approve"), onReject: () => void act("reject"), onCancel: () => void cancel() }));
|
|
230
|
+
}
|
|
231
|
+
if (state.phase === "connecting" ||
|
|
232
|
+
state.phase === "waiting-for-approval" ||
|
|
233
|
+
state.phase === "transferring") {
|
|
234
|
+
return (0, jsx_runtime_1.jsx)(ProgressWithCancel, { state: state, onCancel: () => void cancel() });
|
|
235
|
+
}
|
|
236
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 500, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("h3", { children: "Sign in using another device" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "On a device already signed in to Peers, choose Add another device. Scan its QR code, or paste the complete pairing link or token below." }), (0, jsx_runtime_1.jsx)("textarea", { className: "form-control font-monospace mb-3", rows: 4, placeholder: "Paste pairing link or token", value: invitation, onChange: (event) => setInvitation(event.target.value), autoComplete: "off", "aria-label": "Device pairing invitation" }), localError ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger", children: localError }) : null, (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary btn-lg", disabled: !invitation.trim(), onClick: () => void start(), children: "Connect securely" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onBack, children: "Back" })] })] }));
|
|
237
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
const react_1 = require("@testing-library/react");
|
|
6
|
+
const device_pairing_1 = require("./device-pairing");
|
|
7
|
+
const mockRpc = {
|
|
8
|
+
startSource: jest.fn(),
|
|
9
|
+
startDestination: jest.fn(),
|
|
10
|
+
approve: jest.fn(),
|
|
11
|
+
reject: jest.fn(),
|
|
12
|
+
cancel: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
let mockPairingState = { phase: "idle" };
|
|
15
|
+
peers_sdk_1.rpcServerCalls.devicePairingStartSource =
|
|
16
|
+
mockRpc.startSource;
|
|
17
|
+
peers_sdk_1.rpcServerCalls.devicePairingStartDestination =
|
|
18
|
+
mockRpc.startDestination;
|
|
19
|
+
peers_sdk_1.rpcServerCalls.devicePairingApprove = mockRpc.approve;
|
|
20
|
+
peers_sdk_1.rpcServerCalls.devicePairingReject = mockRpc.reject;
|
|
21
|
+
peers_sdk_1.rpcServerCalls.devicePairingCancel = mockRpc.cancel;
|
|
22
|
+
jest.mock("./use-device-pairing-state", () => ({
|
|
23
|
+
useDevicePairingState: () => mockPairingState,
|
|
24
|
+
}));
|
|
25
|
+
describe("device pairing UI lifecycle", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.clearAllMocks();
|
|
28
|
+
mockPairingState = { phase: "idle" };
|
|
29
|
+
mockRpc.cancel.mockResolvedValue({ phase: "idle" });
|
|
30
|
+
});
|
|
31
|
+
it("uses the host-owned invitation URL and cancels a source that resolves after unmount", async () => {
|
|
32
|
+
let resolveStart;
|
|
33
|
+
mockRpc.startSource.mockReturnValue(new Promise((resolve) => {
|
|
34
|
+
resolveStart = resolve;
|
|
35
|
+
}));
|
|
36
|
+
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(device_pairing_1.SourceDevicePairing, { onClose: jest.fn() }));
|
|
37
|
+
expect(mockRpc.startSource).toHaveBeenCalledWith();
|
|
38
|
+
view.unmount();
|
|
39
|
+
resolveStart({
|
|
40
|
+
state: { sessionId: "00mh0wlipkdbeaw8tests0001", role: "source", phase: "inviting" },
|
|
41
|
+
invitationUrl: "https://peers.app/?app#pair=token",
|
|
42
|
+
});
|
|
43
|
+
await (0, react_1.waitFor)(() => {
|
|
44
|
+
expect(mockRpc.cancel).toHaveBeenCalledWith({
|
|
45
|
+
sessionId: "00mh0wlipkdbeaw8tests0001",
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it("keeps source cancellation available while waiting for remote approval", async () => {
|
|
50
|
+
const onClose = jest.fn();
|
|
51
|
+
mockPairingState = {
|
|
52
|
+
sessionId: "00mh0wlipkdbeaw8tests0002",
|
|
53
|
+
role: "source",
|
|
54
|
+
phase: "waiting-for-approval",
|
|
55
|
+
};
|
|
56
|
+
mockRpc.startSource.mockResolvedValue({
|
|
57
|
+
state: mockPairingState,
|
|
58
|
+
invitationUrl: "https://peers.app/?app#pair=token",
|
|
59
|
+
});
|
|
60
|
+
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(device_pairing_1.SourceDevicePairing, { onClose: onClose }));
|
|
61
|
+
await (0, react_1.waitFor)(() => expect(mockRpc.startSource).toHaveBeenCalled());
|
|
62
|
+
react_1.fireEvent.click(view.getByRole("button", { name: "Cancel" }));
|
|
63
|
+
await (0, react_1.waitFor)(() => {
|
|
64
|
+
expect(mockRpc.cancel).toHaveBeenCalledWith({
|
|
65
|
+
sessionId: "00mh0wlipkdbeaw8tests0002",
|
|
66
|
+
});
|
|
67
|
+
expect(onClose).toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it("does not clear the destination success reload when Done unmounts the flow", () => {
|
|
71
|
+
jest.useFakeTimers();
|
|
72
|
+
mockPairingState = {
|
|
73
|
+
sessionId: "00mh0wlipkdbeaw8tests0003",
|
|
74
|
+
role: "destination",
|
|
75
|
+
phase: "complete",
|
|
76
|
+
};
|
|
77
|
+
const setTimeoutSpy = jest.spyOn(window, "setTimeout");
|
|
78
|
+
const clearTimeoutSpy = jest.spyOn(window, "clearTimeout");
|
|
79
|
+
const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(device_pairing_1.DestinationDevicePairing, { onBack: jest.fn() }));
|
|
80
|
+
const reloadTimer = setTimeoutSpy.mock.calls.find(([, delay]) => delay === 750);
|
|
81
|
+
expect(reloadTimer).toBeDefined();
|
|
82
|
+
view.unmount();
|
|
83
|
+
expect(clearTimeoutSpy).not.toHaveBeenCalled();
|
|
84
|
+
setTimeoutSpy.mockRestore();
|
|
85
|
+
clearTimeoutSpy.mockRestore();
|
|
86
|
+
jest.useRealTimers();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
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 __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./device-pairing"), exports);
|
|
18
|
+
__exportStar(require("./device-pairing-invite"), exports);
|
|
19
|
+
__exportStar(require("./use-device-pairing-state"), exports);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useDevicePairingState = useDevicePairingState;
|
|
4
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
+
const react_1 = require("react");
|
|
6
|
+
const IDLE_PAIRING_STATE = { phase: "idle" };
|
|
7
|
+
/** Subscribes to renderer-safe host pairing state and loads its current snapshot. */
|
|
8
|
+
function useDevicePairingState() {
|
|
9
|
+
const [state, setState] = (0, react_1.useState)(IDLE_PAIRING_STATE);
|
|
10
|
+
(0, react_1.useEffect)(() => {
|
|
11
|
+
let active = true;
|
|
12
|
+
const subscription = (0, peers_sdk_1.subscribe)(peers_sdk_1.DEVICE_PAIRING_STATE_EVENT, (event) => {
|
|
13
|
+
if (active) {
|
|
14
|
+
setState(peers_sdk_1.devicePairingStateSchema.parse(event.data));
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
void peers_sdk_1.rpcServerCalls
|
|
18
|
+
.devicePairingGetState()
|
|
19
|
+
.then((value) => {
|
|
20
|
+
if (active) {
|
|
21
|
+
setState(peers_sdk_1.devicePairingStateSchema.parse(value));
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.catch(() => undefined);
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
subscription.unsubscribe();
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
return state;
|
|
31
|
+
}
|
|
@@ -43,6 +43,7 @@ const tooltip_1 = require("../../components/tooltip");
|
|
|
43
43
|
const globals = __importStar(require("../../globals"));
|
|
44
44
|
const hooks_1 = require("../../hooks");
|
|
45
45
|
const layout_picker_1 = require("../../root-layout/layout-picker");
|
|
46
|
+
const device_pairing_1 = require("../device-pairing");
|
|
46
47
|
const color_mode_dropdown_1 = require("./color-mode-dropdown");
|
|
47
48
|
const voice_settings_1 = require("./voice-settings");
|
|
48
49
|
function getAppVersion() {
|
|
@@ -72,7 +73,7 @@ const SettingsPage = () => {
|
|
|
72
73
|
};
|
|
73
74
|
exports.SettingsPage = SettingsPage;
|
|
74
75
|
const UserSettingsTab = () => {
|
|
75
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ProfileSection, {}), (0, jsx_runtime_1.jsx)(LogoutSection, {})] }));
|
|
76
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ProfileSection, {}), (0, jsx_runtime_1.jsx)(AddDeviceSection, {}), (0, jsx_runtime_1.jsx)(LogoutSection, {})] }));
|
|
76
77
|
};
|
|
77
78
|
const AppearanceSettingsTab = () => {
|
|
78
79
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-3", children: "Appearance" }), (0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsx)(color_mode_dropdown_1.ColorModeDropdown, {}) }), (0, jsx_runtime_1.jsx)("div", { className: "mb-3", children: (0, jsx_runtime_1.jsx)(layout_picker_1.LayoutPicker, {}) })] }));
|
|
@@ -148,6 +149,10 @@ const ProfileSection = () => {
|
|
|
148
149
|
const hasChanges = !!me.q() || deviceName() !== savedDeviceName();
|
|
149
150
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mb-3", children: [(0, jsx_runtime_1.jsx)("h5", { className: "mb-0 me-auto", children: "Settings" }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-primary btn-sm", onClick: handleSave, disabled: !hasChanges, children: "Save" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "form-label", children: "Display Name:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.qs.name, className: "form-control", maxLength: peers_sdk_1.DISPLAY_NAME_MAX_LENGTH, placeholder: "Enter your name" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device Name:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "Give this device a friendly name to identify it in your network." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceName, className: "form-control", maxLength: peers_sdk_1.DISPLAY_NAME_MAX_LENGTH, placeholder: "e.g., My Laptop" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["User ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies you to all other users." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.userId, className: "form-control", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "row", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is your public key that other users will use to verify your signatures." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicKey, className: "form-control form-control-sm", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-6 mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Public Box Key:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This is the public key that other users can use to encrypt data so that only you can open it." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: me.publicBoxKey, className: "form-control form-control-sm", disabled: true })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("small", { className: "form-label", children: ["Device ID:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: "This uniquely identifies this device on Peers networks." })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: deviceId || "", className: "form-control form-control-sm", disabled: true })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("small", { className: "form-label", children: "App Version:" }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: getAppVersion(), className: "form-control form-control-sm", disabled: true })] })] }));
|
|
150
151
|
};
|
|
152
|
+
const AddDeviceSection = () => {
|
|
153
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
154
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: "mt-4 pt-3 border-top", children: [(0, jsx_runtime_1.jsx)("h6", { children: "Add another device" }), (0, jsx_runtime_1.jsx)("p", { className: "small text-muted", children: "Create a five-minute invitation, compare the same number on both devices, and approve on each one before account access is transferred." }), open ? ((0, jsx_runtime_1.jsx)(device_pairing_1.SourceDevicePairing, { onClose: () => setOpen(false) })) : ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-primary", onClick: () => setOpen(true), children: "Add another device" }))] }));
|
|
155
|
+
};
|
|
151
156
|
const PackagesRootDirectory = () => {
|
|
152
157
|
return ((0, jsx_runtime_1.jsxs)("div", { className: "mt-3", children: [(0, jsx_runtime_1.jsxs)("small", { children: ["Packages Directory:", (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: `Changing this will not effect existing packages.` })] }), (0, jsx_runtime_1.jsx)(input_1.Input, { value: peers_sdk_1.packagesRootDir, className: "form-control" })] }));
|
|
153
158
|
};
|
|
@@ -5,10 +5,13 @@ 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
7
|
const hooks_1 = require("../hooks");
|
|
8
|
+
const device_pairing_1 = require("./device-pairing");
|
|
8
9
|
const color_mode_dropdown_1 = require("./settings/color-mode-dropdown");
|
|
9
10
|
const SetupUser = () => {
|
|
10
11
|
const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
|
|
11
12
|
const [step, _setStep] = (0, react_1.useState)("select-user-type");
|
|
13
|
+
const [pairingInvitation, setPairingInvitation] = (0, react_1.useState)(() => (0, device_pairing_1.consumePendingDevicePairingInvite)());
|
|
14
|
+
const [showPairing, setShowPairing] = (0, react_1.useState)(() => Boolean(pairingInvitation));
|
|
12
15
|
const [isExistingUser, setIsExistingUser] = (0, react_1.useState)(null);
|
|
13
16
|
const [userId, setUserId] = (0, react_1.useState)("");
|
|
14
17
|
const [secretKey, setSecretKey] = (0, react_1.useState)("");
|
|
@@ -16,6 +19,16 @@ const SetupUser = () => {
|
|
|
16
19
|
const [isValidating, setIsValidating] = (0, react_1.useState)(false);
|
|
17
20
|
document.getElementById("appLoadingDiv")?.remove();
|
|
18
21
|
const isDark = _colorMode === "dark";
|
|
22
|
+
(0, react_1.useEffect)(() => {
|
|
23
|
+
const capture = (event) => {
|
|
24
|
+
const invitation = event.detail;
|
|
25
|
+
setPairingInvitation(invitation);
|
|
26
|
+
(0, device_pairing_1.consumePendingDevicePairingInvite)();
|
|
27
|
+
setShowPairing(true);
|
|
28
|
+
};
|
|
29
|
+
window.addEventListener(device_pairing_1.DEVICE_PAIRING_INVITE_CAPTURED_EVENT, capture);
|
|
30
|
+
return () => window.removeEventListener(device_pairing_1.DEVICE_PAIRING_INVITE_CAPTURED_EVENT, capture);
|
|
31
|
+
}, []);
|
|
19
32
|
const handleNewUser = async () => {
|
|
20
33
|
setError("");
|
|
21
34
|
setIsValidating(true);
|
|
@@ -65,13 +78,23 @@ const SetupUser = () => {
|
|
|
65
78
|
setIsValidating(false);
|
|
66
79
|
}
|
|
67
80
|
};
|
|
81
|
+
if (showPairing) {
|
|
82
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: "20px", backgroundColor: isDark ? "#212529" : "#f8f9fa" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card shadow", style: {
|
|
83
|
+
maxWidth: "560px",
|
|
84
|
+
width: "100%",
|
|
85
|
+
backgroundColor: isDark ? "#2d3238" : "#ffffff",
|
|
86
|
+
}, children: (0, jsx_runtime_1.jsx)("div", { className: "card-body p-4 p-md-5", children: (0, jsx_runtime_1.jsx)(device_pairing_1.DestinationDevicePairing, { initialInvitation: pairingInvitation, onInvitationConsumed: () => setPairingInvitation(undefined), onBack: () => {
|
|
87
|
+
setPairingInvitation(undefined);
|
|
88
|
+
setShowPairing(false);
|
|
89
|
+
} }) }) }) }));
|
|
90
|
+
}
|
|
68
91
|
// Step 1: Select User Type
|
|
69
92
|
if (step === "select-user-type" && isExistingUser === null) {
|
|
70
93
|
return ((0, jsx_runtime_1.jsx)("div", { className: "container-fluid d-flex align-items-start justify-content-center", style: { paddingTop: "20px", backgroundColor: isDark ? "#212529" : "#f8f9fa" }, children: (0, jsx_runtime_1.jsx)("div", { className: "card shadow", style: {
|
|
71
94
|
maxWidth: "500px",
|
|
72
95
|
width: "100%",
|
|
73
96
|
backgroundColor: isDark ? "#2d3238" : "#ffffff",
|
|
74
|
-
}, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-4 p-md-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { className: `fw-bold mb-2 ${isDark ? "text-light" : ""}`, children: "Welcome to Peers" }), (0, jsx_runtime_1.jsx)("p", { className: isDark ? "text-light opacity-75" : "text-muted", children: "Let's get you set up" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-3", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-lg btn-primary py-3", onClick: () => setIsExistingUser(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-plus fs-4 me-2" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: "I'm a New User" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Create a new account" })] })] }) }), (0, jsx_runtime_1.jsx)("button", { className: `btn btn-lg py-3 ${isDark ? "btn-outline-light" : "btn-outline-primary"}`, onClick: () => setIsExistingUser(true), children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right fs-4 me-2" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: "I'm an Existing User" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Sign in with your credentials" })] })] }) })] })] }) }) }));
|
|
97
|
+
}, children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body p-4 p-md-5", children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-4", children: [(0, jsx_runtime_1.jsx)("h2", { className: `fw-bold mb-2 ${isDark ? "text-light" : ""}`, children: "Welcome to Peers" }), (0, jsx_runtime_1.jsx)("p", { className: isDark ? "text-light opacity-75" : "text-muted", children: "Let's get you set up" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-3", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-lg btn-primary py-3", onClick: () => setIsExistingUser(false), children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-plus fs-4 me-2" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: "I'm a New User" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Create a new account" })] })] }) }), (0, jsx_runtime_1.jsx)("button", { className: `btn btn-lg py-3 ${isDark ? "btn-outline-light" : "btn-outline-primary"}`, onClick: () => setIsExistingUser(true), children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right fs-4 me-2" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: "I'm an Existing User" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Sign in with your credentials" })] })] }) }), (0, jsx_runtime_1.jsx)("button", { className: `btn btn-lg py-3 ${isDark ? "btn-outline-light" : "btn-outline-primary"}`, onClick: () => setShowPairing(true), children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-phone fs-4 me-2" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: "Use Another Device" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Sign in with a device already on Peers" })] })] }) })] })] }) }) }));
|
|
75
98
|
}
|
|
76
99
|
// Step 2: New User Confirmation
|
|
77
100
|
if (isExistingUser === false) {
|