@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
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRoleLabel,
|
|
3
|
+
getUserContext,
|
|
4
|
+
type IInvite,
|
|
5
|
+
type InviteStatus,
|
|
6
|
+
Invites,
|
|
7
|
+
} from "@peers-app/peers-sdk";
|
|
8
|
+
import { useCallback, useEffect, useState } from "react";
|
|
9
|
+
import { inviteTools } from "./invites-ipc";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link PendingInvitesList}. */
|
|
12
|
+
export interface PendingInvitesListProps {
|
|
13
|
+
/** Only show rows of this type. */
|
|
14
|
+
type?: IInvite["type"];
|
|
15
|
+
/** Only show rows for this group. */
|
|
16
|
+
groupId?: string;
|
|
17
|
+
/** Statuses to include; defaults to pending only. */
|
|
18
|
+
statuses?: InviteStatus[];
|
|
19
|
+
/** Hide the section entirely when there is nothing to show. */
|
|
20
|
+
hideWhenEmpty?: boolean;
|
|
21
|
+
title?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Subscribe to the personal `Invites` table and return rows matching the filter, newest first. */
|
|
25
|
+
export function useInviteRows(filter: {
|
|
26
|
+
type?: IInvite["type"];
|
|
27
|
+
groupId?: string;
|
|
28
|
+
statuses?: InviteStatus[];
|
|
29
|
+
}): IInvite[] | undefined {
|
|
30
|
+
const [rows, setRows] = useState<IInvite[] | undefined>(undefined);
|
|
31
|
+
const statuses = filter.statuses ?? ["pending"];
|
|
32
|
+
const statusKey = statuses.join(",");
|
|
33
|
+
|
|
34
|
+
const refresh = useCallback(async () => {
|
|
35
|
+
const userContext = await getUserContext();
|
|
36
|
+
const table = Invites(userContext.userDataContext);
|
|
37
|
+
const all = await table.list({
|
|
38
|
+
...(filter.type ? { type: filter.type } : {}),
|
|
39
|
+
...(filter.groupId ? { groupId: filter.groupId } : {}),
|
|
40
|
+
});
|
|
41
|
+
const wanted = new Set(statusKey.split(","));
|
|
42
|
+
setRows(
|
|
43
|
+
all
|
|
44
|
+
.filter((row) => wanted.has(row.status))
|
|
45
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()),
|
|
46
|
+
);
|
|
47
|
+
}, [filter.type, filter.groupId, statusKey]);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
let disposed = false;
|
|
51
|
+
let dispose: (() => void) | undefined;
|
|
52
|
+
void refresh();
|
|
53
|
+
void getUserContext().then((userContext) => {
|
|
54
|
+
if (disposed) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const sub = Invites(userContext.userDataContext).dataChanged.subscribe(() => void refresh());
|
|
58
|
+
dispose = () => {
|
|
59
|
+
sub.unsubscribe();
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
return () => {
|
|
63
|
+
disposed = true;
|
|
64
|
+
dispose?.();
|
|
65
|
+
};
|
|
66
|
+
}, [refresh]);
|
|
67
|
+
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function counterpartLabel(row: IInvite): string {
|
|
72
|
+
return row.counterpart?.displayName || row.counterpart?.userId || "Someone";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Human summary of what a row is, from the local user's point of view. */
|
|
76
|
+
export function describeInviteRow(row: IInvite): { title: string; detail: string } {
|
|
77
|
+
const who = counterpartLabel(row);
|
|
78
|
+
const role = row.role !== undefined ? getRoleLabel(row.role) : undefined;
|
|
79
|
+
const group = row.groupName ?? "a group";
|
|
80
|
+
if (row.type === "contact") {
|
|
81
|
+
if (row.direction === "in") {
|
|
82
|
+
return { title: `${who} wants to connect`, detail: "Accept to add them as a contact." };
|
|
83
|
+
}
|
|
84
|
+
if (row.counterpart) {
|
|
85
|
+
return { title: `Contact invite to ${who}`, detail: "Waiting for them to accept." };
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
title: row.mode === "profile" ? "Your profile QR" : "Contact invite link",
|
|
89
|
+
detail: row.mode === "profile" ? "Reusable; regenerate to revoke." : "Not used yet.",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (row.direction === "in") {
|
|
93
|
+
if (row.mode === "direct") {
|
|
94
|
+
return {
|
|
95
|
+
title: `${who} invited you to ${group}`,
|
|
96
|
+
detail: role ? `You would join as ${role}.` : "",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return { title: `Joining ${group}`, detail: `Waiting for ${who} to approve.` };
|
|
100
|
+
}
|
|
101
|
+
if (row.mode === "direct") {
|
|
102
|
+
return { title: `Invited ${who} to ${group}`, detail: "Waiting for them to accept." };
|
|
103
|
+
}
|
|
104
|
+
if (row.counterpart) {
|
|
105
|
+
return {
|
|
106
|
+
title: `${who} wants to join ${group}`,
|
|
107
|
+
detail: role ? `Requested as ${role}. Approve or deny.` : "Approve or deny.",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
title: `${group} invite link`,
|
|
112
|
+
detail: `${row.mode === "open" ? "Open" : "Approval required"}${role ? ` · ${role}` : ""}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type RowAction = "accept" | "decline" | "approve" | "deny" | "revoke";
|
|
117
|
+
|
|
118
|
+
function actionsFor(row: IInvite): RowAction[] {
|
|
119
|
+
if (row.status !== "pending") {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
if (row.direction === "in") {
|
|
123
|
+
// Inbound rows I can act on: contact requests and direct group invites.
|
|
124
|
+
if (row.type === "contact" || row.mode === "direct") {
|
|
125
|
+
return ["accept", "decline"];
|
|
126
|
+
}
|
|
127
|
+
return []; // my own outstanding join request; nothing to do but wait
|
|
128
|
+
}
|
|
129
|
+
if (row.counterpart && row.type === "group" && row.mode !== "direct") {
|
|
130
|
+
return ["approve", "deny"];
|
|
131
|
+
}
|
|
132
|
+
return ["revoke"];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const ACTION_LABELS: Record<RowAction, { text: string; className: string }> = {
|
|
136
|
+
accept: { text: "Accept", className: "btn-primary" },
|
|
137
|
+
decline: { text: "Decline", className: "btn-outline-secondary" },
|
|
138
|
+
approve: { text: "Approve", className: "btn-primary" },
|
|
139
|
+
deny: { text: "Deny", className: "btn-outline-secondary" },
|
|
140
|
+
revoke: { text: "Revoke", className: "btn-outline-danger" },
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
function InviteRow({ row }: { row: IInvite }) {
|
|
144
|
+
const [busy, setBusy] = useState<RowAction | undefined>(undefined);
|
|
145
|
+
const [error, setError] = useState("");
|
|
146
|
+
const { title, detail } = describeInviteRow(row);
|
|
147
|
+
|
|
148
|
+
const run = async (action: RowAction) => {
|
|
149
|
+
setBusy(action);
|
|
150
|
+
setError("");
|
|
151
|
+
try {
|
|
152
|
+
const tools = await inviteTools();
|
|
153
|
+
const input = { inviteId: row.inviteId };
|
|
154
|
+
if (action === "accept") await tools.acceptInvite(input);
|
|
155
|
+
else if (action === "decline") await tools.declineInvite(input);
|
|
156
|
+
else if (action === "approve") await tools.approveJoinRequest(input);
|
|
157
|
+
else if (action === "deny") await tools.denyJoinRequest(input);
|
|
158
|
+
else await tools.revokeInvite(input);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
setError((err as Error).message || "Could not update the invite.");
|
|
161
|
+
} finally {
|
|
162
|
+
setBusy(undefined);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div className="list-group-item">
|
|
168
|
+
<div className="d-flex justify-content-between align-items-center gap-2 flex-wrap">
|
|
169
|
+
<div className="flex-grow-1">
|
|
170
|
+
<div className="fw-semibold">{title}</div>
|
|
171
|
+
{detail ? <small className="text-muted d-block">{detail}</small> : null}
|
|
172
|
+
{row.deliveryState === "queued" ? (
|
|
173
|
+
<small className="text-warning d-block">Queued until they are reachable</small>
|
|
174
|
+
) : null}
|
|
175
|
+
{row.status !== "pending" ? (
|
|
176
|
+
<small className="text-muted d-block text-capitalize">{row.status}</small>
|
|
177
|
+
) : null}
|
|
178
|
+
</div>
|
|
179
|
+
<div className="d-flex gap-1">
|
|
180
|
+
{actionsFor(row).map((action) => (
|
|
181
|
+
<button
|
|
182
|
+
key={action}
|
|
183
|
+
type="button"
|
|
184
|
+
className={`btn btn-sm ${ACTION_LABELS[action].className}`}
|
|
185
|
+
disabled={busy !== undefined}
|
|
186
|
+
onClick={() => void run(action)}
|
|
187
|
+
>
|
|
188
|
+
{busy === action ? "…" : ACTION_LABELS[action].text}
|
|
189
|
+
</button>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
{error ? <div className="alert alert-danger py-1 px-2 small mt-2 mb-0">{error}</div> : null}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Live list of invite rows with the actions each one supports. Subscribes to the `Invites`
|
|
200
|
+
* table directly, so it updates when replies arrive on any of the user's devices.
|
|
201
|
+
*/
|
|
202
|
+
export function PendingInvitesList(props: PendingInvitesListProps) {
|
|
203
|
+
const rows = useInviteRows(props);
|
|
204
|
+
if (!rows) {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
if (rows.length === 0 && props.hideWhenEmpty) {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return (
|
|
211
|
+
<section className="mb-3">
|
|
212
|
+
{props.title ? <h6 className="mb-2">{props.title}</h6> : null}
|
|
213
|
+
{rows.length === 0 ? (
|
|
214
|
+
<p className="text-muted small mb-0">No pending invites.</p>
|
|
215
|
+
) : (
|
|
216
|
+
<div className="list-group">
|
|
217
|
+
{rows.map((row) => (
|
|
218
|
+
<InviteRow key={row.inviteId} row={row} />
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -258,8 +258,8 @@ const AddDeviceSection: React.FC = () => {
|
|
|
258
258
|
<section className="mt-4 pt-3 border-top">
|
|
259
259
|
<h6>Add another device</h6>
|
|
260
260
|
<p className="small text-muted">
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
On the new device choose “Sign in from another device”, then type the code it shows here and
|
|
262
|
+
approve. Account access is transferred only after you approve.
|
|
263
263
|
</p>
|
|
264
264
|
{open ? (
|
|
265
265
|
<SourceDevicePairing onClose={() => setOpen(false)} />
|
|
@@ -7,13 +7,9 @@ import {
|
|
|
7
7
|
rpcServerCalls,
|
|
8
8
|
signMessageWithSecretKey,
|
|
9
9
|
} from "@peers-app/peers-sdk";
|
|
10
|
-
import {
|
|
10
|
+
import { useState } from "react";
|
|
11
11
|
import { useObservable } from "../hooks";
|
|
12
|
-
import {
|
|
13
|
-
consumePendingDevicePairingInvite,
|
|
14
|
-
DEVICE_PAIRING_INVITE_CAPTURED_EVENT,
|
|
15
|
-
DestinationDevicePairing,
|
|
16
|
-
} from "./device-pairing";
|
|
12
|
+
import { DestinationDevicePairing } from "./device-pairing";
|
|
17
13
|
import { colorMode } from "./settings/color-mode-dropdown";
|
|
18
14
|
|
|
19
15
|
type SetupStep = "select-user-type" | "complete";
|
|
@@ -21,10 +17,7 @@ type SetupStep = "select-user-type" | "complete";
|
|
|
21
17
|
export const SetupUser = () => {
|
|
22
18
|
const [_colorMode] = useObservable(colorMode);
|
|
23
19
|
const [step, _setStep] = useState<SetupStep>("select-user-type");
|
|
24
|
-
const [
|
|
25
|
-
consumePendingDevicePairingInvite(),
|
|
26
|
-
);
|
|
27
|
-
const [showPairing, setShowPairing] = useState(() => Boolean(pairingInvitation));
|
|
20
|
+
const [showPairing, setShowPairing] = useState(false);
|
|
28
21
|
const [isExistingUser, setIsExistingUser] = useState<boolean | null>(null);
|
|
29
22
|
const [userId, setUserId] = useState("");
|
|
30
23
|
const [secretKey, setSecretKey] = useState("");
|
|
@@ -35,17 +28,6 @@ export const SetupUser = () => {
|
|
|
35
28
|
|
|
36
29
|
const isDark = _colorMode === "dark";
|
|
37
30
|
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
const capture = (event: Event) => {
|
|
40
|
-
const invitation = (event as CustomEvent<string>).detail;
|
|
41
|
-
setPairingInvitation(invitation);
|
|
42
|
-
consumePendingDevicePairingInvite();
|
|
43
|
-
setShowPairing(true);
|
|
44
|
-
};
|
|
45
|
-
window.addEventListener(DEVICE_PAIRING_INVITE_CAPTURED_EVENT, capture);
|
|
46
|
-
return () => window.removeEventListener(DEVICE_PAIRING_INVITE_CAPTURED_EVENT, capture);
|
|
47
|
-
}, []);
|
|
48
|
-
|
|
49
31
|
const handleNewUser = async () => {
|
|
50
32
|
setError("");
|
|
51
33
|
setIsValidating(true);
|
|
@@ -120,14 +102,7 @@ export const SetupUser = () => {
|
|
|
120
102
|
}}
|
|
121
103
|
>
|
|
122
104
|
<div className="card-body p-4 p-md-5">
|
|
123
|
-
<DestinationDevicePairing
|
|
124
|
-
initialInvitation={pairingInvitation}
|
|
125
|
-
onInvitationConsumed={() => setPairingInvitation(undefined)}
|
|
126
|
-
onBack={() => {
|
|
127
|
-
setPairingInvitation(undefined);
|
|
128
|
-
setShowPairing(false);
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
105
|
+
<DestinationDevicePairing onBack={() => setShowPairing(false)} />
|
|
131
106
|
</div>
|
|
132
107
|
</div>
|
|
133
108
|
</div>
|
|
@@ -165,21 +140,8 @@ export const SetupUser = () => {
|
|
|
165
140
|
<div className="d-flex align-items-center justify-content-center">
|
|
166
141
|
<i className="bi bi-person-plus fs-4 me-2" />
|
|
167
142
|
<div className="text-start">
|
|
168
|
-
<div className="fw-semibold">
|
|
169
|
-
<small className="opacity-75">Create a new account</small>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
</button>
|
|
173
|
-
|
|
174
|
-
<button
|
|
175
|
-
className={`btn btn-lg py-3 ${isDark ? "btn-outline-light" : "btn-outline-primary"}`}
|
|
176
|
-
onClick={() => setIsExistingUser(true)}
|
|
177
|
-
>
|
|
178
|
-
<div className="d-flex align-items-center justify-content-center">
|
|
179
|
-
<i className="bi bi-box-arrow-in-right fs-4 me-2" />
|
|
180
|
-
<div className="text-start">
|
|
181
|
-
<div className="fw-semibold">I'm an Existing User</div>
|
|
182
|
-
<small className="opacity-75">Sign in with your credentials</small>
|
|
143
|
+
<div className="fw-semibold">New account</div>
|
|
144
|
+
<small className="opacity-75">Create a new account on this device</small>
|
|
183
145
|
</div>
|
|
184
146
|
</div>
|
|
185
147
|
</button>
|
|
@@ -191,11 +153,30 @@ export const SetupUser = () => {
|
|
|
191
153
|
<div className="d-flex align-items-center justify-content-center">
|
|
192
154
|
<i className="bi bi-phone fs-4 me-2" />
|
|
193
155
|
<div className="text-start">
|
|
194
|
-
<div className="fw-semibold">
|
|
195
|
-
<small className="opacity-75">
|
|
156
|
+
<div className="fw-semibold">Sign in from another device</div>
|
|
157
|
+
<small className="opacity-75">
|
|
158
|
+
Show a code to a device that is already signed in
|
|
159
|
+
</small>
|
|
196
160
|
</div>
|
|
197
161
|
</div>
|
|
198
162
|
</button>
|
|
163
|
+
|
|
164
|
+
<details className="text-center mt-2">
|
|
165
|
+
<summary
|
|
166
|
+
className={`small ${isDark ? "text-light opacity-75" : "text-muted"}`}
|
|
167
|
+
style={{ cursor: "pointer" }}
|
|
168
|
+
>
|
|
169
|
+
Advanced
|
|
170
|
+
</summary>
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
className={`btn btn-sm mt-2 ${isDark ? "btn-outline-light" : "btn-outline-secondary"}`}
|
|
174
|
+
onClick={() => setIsExistingUser(true)}
|
|
175
|
+
>
|
|
176
|
+
<i className="bi bi-key me-1" />
|
|
177
|
+
Enter a user ID and secret key
|
|
178
|
+
</button>
|
|
179
|
+
</details>
|
|
199
180
|
</div>
|
|
200
181
|
</div>
|
|
201
182
|
</div>
|
|
@@ -291,7 +272,8 @@ export const SetupUser = () => {
|
|
|
291
272
|
<i className="bi bi-box-arrow-in-right text-primary" />
|
|
292
273
|
</h3>
|
|
293
274
|
<p className={isDark ? "text-light opacity-75" : "text-muted"}>
|
|
294
|
-
|
|
275
|
+
Paste your user ID and secret key. If you have another signed-in device, “Sign in
|
|
276
|
+
from another device” is easier and safer.
|
|
295
277
|
</p>
|
|
296
278
|
</div>
|
|
297
279
|
|
package/src/system-apps/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { consoleLogsApp } from "./console-logs.app";
|
|
|
7
7
|
export { contactsApp } from "./contacts.app";
|
|
8
8
|
export { dataExplorerApp } from "./data-explorer.app";
|
|
9
9
|
export { groupsApp } from "./groups.app";
|
|
10
|
-
export {
|
|
10
|
+
export { invitesApp } from "./invites.app";
|
|
11
11
|
export { mobileSettingsApp } from "./mobile-settings.app";
|
|
12
12
|
export { networkViewerApp } from "./network-viewer.app";
|
|
13
13
|
export { packagesApp } from "./packages.app";
|
|
@@ -26,7 +26,7 @@ import { consoleLogsApp } from "./console-logs.app";
|
|
|
26
26
|
import { contactsApp } from "./contacts.app";
|
|
27
27
|
import { dataExplorerApp } from "./data-explorer.app";
|
|
28
28
|
import { groupsApp } from "./groups.app";
|
|
29
|
-
import {
|
|
29
|
+
import { invitesApp } from "./invites.app";
|
|
30
30
|
import { mobileSettingsApp } from "./mobile-settings.app";
|
|
31
31
|
import { networkViewerApp } from "./network-viewer.app";
|
|
32
32
|
import { packagesApp } from "./packages.app";
|
|
@@ -64,7 +64,7 @@ export const systemApps: IAppNav[] = [
|
|
|
64
64
|
packagesApp,
|
|
65
65
|
contactsApp,
|
|
66
66
|
groupsApp,
|
|
67
|
-
|
|
67
|
+
invitesApp,
|
|
68
68
|
|
|
69
69
|
// User & Settings Apps
|
|
70
70
|
accountApp,
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Invite URL and pending-intent helpers for the user-connect QR/link flow.
|
|
3
|
-
*
|
|
4
|
-
* Invite codes stay in the URL fragment (never the HTTP query) and are moved
|
|
5
|
-
* into tab-scoped sessionStorage before startup route restoration can persist
|
|
6
|
-
* them into `mainContentPath`.
|
|
7
|
-
*/
|
|
8
|
-
/** sessionStorage key for a captured inbound invite. */
|
|
9
|
-
export declare const USER_CONNECT_INVITE_STORAGE_KEY = "peers.userConnectInvite";
|
|
10
|
-
/** Default production launch origin used when building shareable invite links. */
|
|
11
|
-
export declare const DEFAULT_USER_CONNECT_BASE_URL = "https://peers.app";
|
|
12
|
-
/**
|
|
13
|
-
* Serializable pending invite captured from a deep link or stored for post-setup resume.
|
|
14
|
-
*/
|
|
15
|
-
export interface UserConnectPendingIntent {
|
|
16
|
-
/** Normalized 12-character Crockford code (no dashes). */
|
|
17
|
-
code: string;
|
|
18
|
-
/** Epoch ms when the intent was captured. */
|
|
19
|
-
capturedAt: number;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Result of attempting to capture or read a pending invite.
|
|
23
|
-
*/
|
|
24
|
-
export type UserConnectInviteCaptureResult = {
|
|
25
|
-
ok: true;
|
|
26
|
-
intent: UserConnectPendingIntent;
|
|
27
|
-
} | {
|
|
28
|
-
ok: false;
|
|
29
|
-
reason: "none" | "invalid" | "expired";
|
|
30
|
-
};
|
|
31
|
-
/**
|
|
32
|
-
* Build the canonical shareable invite URL.
|
|
33
|
-
* Keeps the bearer code in the fragment and uses `?app` so signed-out PWA
|
|
34
|
-
* recipients receive the app shell instead of the marketing landing page.
|
|
35
|
-
* @param code Connection code (formatted or raw)
|
|
36
|
-
* @param baseUrl Launch origin (no trailing slash); defaults to peers.app
|
|
37
|
-
* @returns Absolute invite URL
|
|
38
|
-
*/
|
|
39
|
-
export declare function buildUserConnectInviteUrl(code: string, baseUrl?: string): string;
|
|
40
|
-
/**
|
|
41
|
-
* Extract a connection code from a `contacts/connect` hash fragment.
|
|
42
|
-
* Accepts `#contacts/connect?code=…` or `contacts/connect?code=…`.
|
|
43
|
-
* @param hashOrPath Hash or path fragment from `location.hash` / a full URL
|
|
44
|
-
* @returns Raw code string from the query, or null when the route is not an invite
|
|
45
|
-
*/
|
|
46
|
-
export declare function extractUserConnectCodeFromFragment(hashOrPath: string): string | null;
|
|
47
|
-
/**
|
|
48
|
-
* Validate and normalize a connection code to 12 Crockford characters.
|
|
49
|
-
* @param code Candidate code (formatted or raw)
|
|
50
|
-
* @returns Normalized code, or null when invalid
|
|
51
|
-
*/
|
|
52
|
-
export declare function normalizeUserConnectCode(code: string): string | null;
|
|
53
|
-
/**
|
|
54
|
-
* Whether a pending intent is still within the local invite TTL.
|
|
55
|
-
* @param intent Captured intent
|
|
56
|
-
* @param nowMs Current epoch ms (injectable for tests)
|
|
57
|
-
* @returns True when the intent has not expired
|
|
58
|
-
*/
|
|
59
|
-
export declare function isUserConnectIntentFresh(intent: Pick<UserConnectPendingIntent, "capturedAt">, nowMs?: number): boolean;
|
|
60
|
-
/**
|
|
61
|
-
* Replace the visible URL so the bearer code is no longer in the address bar.
|
|
62
|
-
* Leaves `?app` and a non-sensitive `#contacts/connect` route shell when present.
|
|
63
|
-
* @param locationLike Browser location (defaults to `window.location`)
|
|
64
|
-
* @param historyLike History API (defaults to `window.history`)
|
|
65
|
-
*/
|
|
66
|
-
export declare function stripUserConnectCodeFromUrl(locationLike?: Pick<Location, "pathname" | "search" | "hash">, historyLike?: Pick<History, "replaceState" | "state">): void;
|
|
67
|
-
/**
|
|
68
|
-
* Persist a pending invite in tab-scoped sessionStorage.
|
|
69
|
-
* @param intent Valid intent to store
|
|
70
|
-
* @param storage Storage backend (defaults to sessionStorage)
|
|
71
|
-
*/
|
|
72
|
-
export declare function writeUserConnectPendingIntent(intent: UserConnectPendingIntent, storage?: Storage): void;
|
|
73
|
-
/**
|
|
74
|
-
* Read a pending invite without consuming it.
|
|
75
|
-
* Expired entries are cleared. Invalid markers remain until consumed/cleared
|
|
76
|
-
* so the connection screen can show recovery UI.
|
|
77
|
-
* @param storage Storage backend
|
|
78
|
-
* @param nowMs Current epoch ms
|
|
79
|
-
* @returns Capture result
|
|
80
|
-
*/
|
|
81
|
-
export declare function readUserConnectPendingIntent(storage?: Storage, nowMs?: number): UserConnectInviteCaptureResult;
|
|
82
|
-
/**
|
|
83
|
-
* Read and remove a pending invite in one step (one-time consumption).
|
|
84
|
-
* @param storage Storage backend
|
|
85
|
-
* @param nowMs Current epoch ms
|
|
86
|
-
* @returns Capture result for the consumed intent
|
|
87
|
-
*/
|
|
88
|
-
export declare function consumeUserConnectPendingIntent(storage?: Storage, nowMs?: number): UserConnectInviteCaptureResult;
|
|
89
|
-
/**
|
|
90
|
-
* Remove any pending invite from sessionStorage.
|
|
91
|
-
* @param storage Storage backend
|
|
92
|
-
*/
|
|
93
|
-
export declare function clearUserConnectPendingIntent(storage?: Storage): void;
|
|
94
|
-
/**
|
|
95
|
-
* Capture an inbound invite from the current location into sessionStorage and
|
|
96
|
-
* strip the code from the address bar. Safe to call multiple times; a second
|
|
97
|
-
* call with no code in the URL leaves existing storage alone.
|
|
98
|
-
* @param options Optional location/history/storage/clock overrides for tests
|
|
99
|
-
* @returns Capture result
|
|
100
|
-
*/
|
|
101
|
-
export declare function captureUserConnectInviteFromLocation(options?: {
|
|
102
|
-
locationLike?: Pick<Location, "pathname" | "search" | "hash">;
|
|
103
|
-
historyLike?: Pick<History, "replaceState" | "state">;
|
|
104
|
-
storage?: Storage;
|
|
105
|
-
nowMs?: number;
|
|
106
|
-
}): UserConnectInviteCaptureResult;
|
|
107
|
-
/**
|
|
108
|
-
* Whether sessionStorage currently holds a fresh, valid pending invite.
|
|
109
|
-
* @param storage Storage backend
|
|
110
|
-
* @param nowMs Current epoch ms
|
|
111
|
-
*/
|
|
112
|
-
export declare function hasPendingUserConnectInvite(storage?: Storage, nowMs?: number): boolean;
|
|
113
|
-
/**
|
|
114
|
-
* Share text for the Web Share API / clipboard companion copy.
|
|
115
|
-
* @param inviteUrl Canonical invite URL
|
|
116
|
-
* @returns Human-readable share body
|
|
117
|
-
*/
|
|
118
|
-
export declare function buildUserConnectShareText(inviteUrl: string): string;
|