@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.
Files changed (93) hide show
  1. package/biome.json +2 -2
  2. package/dist/components/qr-scanner.d.ts +18 -0
  3. package/dist/components/qr-scanner.js +165 -0
  4. package/dist/components/router.d.ts +1 -1
  5. package/dist/components/router.js +1 -1
  6. package/dist/root-layout/root-layout-host.js +18 -19
  7. package/dist/root-layout/root-layout-host.test.js +14 -9
  8. package/dist/screens/account/account-screen.js +52 -1
  9. package/dist/screens/contacts/contact-list.js +28 -1
  10. package/dist/screens/contacts/index.d.ts +0 -2
  11. package/dist/screens/contacts/index.js +0 -2
  12. package/dist/screens/device-pairing/device-pairing.d.ts +2 -4
  13. package/dist/screens/device-pairing/device-pairing.js +127 -128
  14. package/dist/screens/device-pairing/device-pairing.test.js +51 -15
  15. package/dist/screens/device-pairing/index.d.ts +0 -1
  16. package/dist/screens/device-pairing/index.js +0 -1
  17. package/dist/screens/groups/group-details.js +2 -6
  18. package/dist/screens/groups/group-invite-panel.d.ts +14 -0
  19. package/dist/screens/groups/group-invite-panel.js +125 -0
  20. package/dist/screens/groups/group-members.js +9 -103
  21. package/dist/screens/invites/accept-invite.d.ts +11 -0
  22. package/dist/screens/invites/accept-invite.js +114 -0
  23. package/dist/screens/invites/index.d.ts +7 -0
  24. package/dist/screens/{join-group → invites}/index.js +8 -3
  25. package/dist/screens/invites/invite-capture.d.ts +34 -0
  26. package/dist/screens/invites/invite-capture.js +94 -0
  27. package/dist/screens/invites/invite-share.d.ts +15 -0
  28. package/dist/screens/invites/invite-share.js +40 -0
  29. package/dist/screens/invites/invites-ipc.d.ts +10 -0
  30. package/dist/screens/invites/invites-ipc.js +61 -0
  31. package/dist/screens/invites/invites-screen.d.ts +9 -0
  32. package/dist/screens/invites/invites-screen.js +36 -0
  33. package/dist/screens/invites/pending-invites-list.d.ts +29 -0
  34. package/dist/screens/invites/pending-invites-list.js +156 -0
  35. package/dist/screens/settings/settings-page.js +1 -1
  36. package/dist/screens/setup-user.js +4 -18
  37. package/dist/system-apps/index.d.ts +1 -1
  38. package/dist/system-apps/index.js +5 -5
  39. package/dist/system-apps/{join-group.app.d.ts → invites.app.d.ts} +1 -1
  40. package/dist/system-apps/invites.app.js +9 -0
  41. package/package.json +4 -3
  42. package/src/components/qr-scanner.tsx +180 -0
  43. package/src/components/router.tsx +1 -1
  44. package/src/root-layout/root-layout-host.test.tsx +18 -13
  45. package/src/root-layout/root-layout-host.tsx +23 -21
  46. package/src/screens/account/account-screen.tsx +89 -2
  47. package/src/screens/contacts/contact-list.tsx +64 -4
  48. package/src/screens/contacts/index.ts +0 -2
  49. package/src/screens/device-pairing/device-pairing.test.tsx +61 -16
  50. package/src/screens/device-pairing/device-pairing.tsx +283 -208
  51. package/src/screens/device-pairing/index.ts +0 -1
  52. package/src/screens/groups/group-details.tsx +1 -41
  53. package/src/screens/groups/group-invite-panel.tsx +316 -0
  54. package/src/screens/groups/group-members.tsx +15 -177
  55. package/src/screens/invites/accept-invite.tsx +271 -0
  56. package/src/screens/invites/index.ts +9 -0
  57. package/src/screens/invites/invite-capture.ts +107 -0
  58. package/src/screens/invites/invite-share.tsx +104 -0
  59. package/src/screens/invites/invites-ipc.ts +74 -0
  60. package/src/screens/invites/invites-screen.tsx +78 -0
  61. package/src/screens/invites/pending-invites-list.tsx +224 -0
  62. package/src/screens/settings/settings-page.tsx +2 -2
  63. package/src/screens/setup-user.tsx +29 -47
  64. package/src/system-apps/index.ts +3 -3
  65. package/src/system-apps/invites.app.ts +8 -0
  66. package/dist/screens/contacts/user-connect-invite.d.ts +0 -118
  67. package/dist/screens/contacts/user-connect-invite.js +0 -264
  68. package/dist/screens/contacts/user-connect-invite.test.d.ts +0 -1
  69. package/dist/screens/contacts/user-connect-invite.test.js +0 -215
  70. package/dist/screens/contacts/user-connect.d.ts +0 -8
  71. package/dist/screens/contacts/user-connect.js +0 -487
  72. package/dist/screens/contacts/user-connect.test.d.ts +0 -1
  73. package/dist/screens/contacts/user-connect.test.js +0 -291
  74. package/dist/screens/device-pairing/device-pairing-invite.d.ts +0 -10
  75. package/dist/screens/device-pairing/device-pairing-invite.js +0 -50
  76. package/dist/screens/device-pairing/device-pairing-invite.test.d.ts +0 -1
  77. package/dist/screens/device-pairing/device-pairing-invite.test.js +0 -30
  78. package/dist/screens/groups/group-invite-listener.d.ts +0 -14
  79. package/dist/screens/groups/group-invite-listener.js +0 -93
  80. package/dist/screens/join-group/index.d.ts +0 -2
  81. package/dist/screens/join-group/join-group.d.ts +0 -12
  82. package/dist/screens/join-group/join-group.js +0 -102
  83. package/dist/system-apps/join-group.app.js +0 -9
  84. package/src/screens/contacts/user-connect-invite.test.ts +0 -256
  85. package/src/screens/contacts/user-connect-invite.ts +0 -332
  86. package/src/screens/contacts/user-connect.test.tsx +0 -382
  87. package/src/screens/contacts/user-connect.tsx +0 -883
  88. package/src/screens/device-pairing/device-pairing-invite.test.ts +0 -50
  89. package/src/screens/device-pairing/device-pairing-invite.ts +0 -55
  90. package/src/screens/groups/group-invite-listener.tsx +0 -282
  91. package/src/screens/join-group/index.ts +0 -4
  92. package/src/screens/join-group/join-group.tsx +0 -296
  93. package/src/system-apps/join-group.app.ts +0 -8
@@ -0,0 +1,40 @@
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.InviteShare = InviteShare;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
9
+ const react_1 = require("react");
10
+ const react_qr_code_1 = __importDefault(require("react-qr-code"));
11
+ function canNativeShare() {
12
+ return typeof navigator !== "undefined" && typeof navigator.share === "function";
13
+ }
14
+ /**
15
+ * Presents an issued invite as a QR code plus copyable link and token. The QR encodes the
16
+ * landing-page URL so phone cameras, the in-app scanner, and the desktop handler all work.
17
+ */
18
+ function InviteShare(props) {
19
+ const { invite } = props;
20
+ const [copied, setCopied] = (0, react_1.useState)(undefined);
21
+ const expires = new Date(invite.token.expiresAt);
22
+ const copy = async (what) => {
23
+ await (0, peers_sdk_1.copyToClipboard)(what === "link" ? invite.url : invite.encoded);
24
+ setCopied(what);
25
+ window.setTimeout(() => setCopied(undefined), 1500);
26
+ };
27
+ const share = async () => {
28
+ try {
29
+ await navigator.share({
30
+ title: "Peers invite",
31
+ text: props.shareText ?? "Join me on Peers:",
32
+ url: invite.url,
33
+ });
34
+ }
35
+ catch {
36
+ // User dismissed the share sheet; nothing to do.
37
+ }
38
+ };
39
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 440, margin: "0 auto" }, children: [props.description ? (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: props.description }) : null, (0, jsx_runtime_1.jsx)("div", { className: "d-inline-block bg-white rounded p-3 mb-2", children: (0, jsx_runtime_1.jsx)(react_qr_code_1.default, { value: invite.url, size: 200, "aria-label": "Invite QR code" }) }), (0, jsx_runtime_1.jsxs)("p", { className: "small text-muted mb-3", children: ["Expires ", expires.toLocaleString(), invite.token.uses === "unlimited" ? " · reusable" : " · single use"] }), (0, jsx_runtime_1.jsxs)("div", { className: "input-group input-group-sm mb-2", children: [(0, jsx_runtime_1.jsx)("input", { className: "form-control font-monospace", readOnly: true, value: invite.url, "aria-label": "Invite link", onFocus: (event) => event.currentTarget.select() }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-primary", onClick: () => void copy("link"), children: copied === "link" ? "Copied" : "Copy link" })] }), (0, jsx_runtime_1.jsxs)("details", { className: "text-start small mb-3", children: [(0, jsx_runtime_1.jsx)("summary", { className: "text-muted", style: { cursor: "pointer" }, children: "No camera or link? Share the code text instead" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-group input-group-sm mt-2", children: [(0, jsx_runtime_1.jsx)("input", { className: "form-control font-monospace", readOnly: true, value: invite.encoded, "aria-label": "Invite code", onFocus: (event) => event.currentTarget.select() }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: () => void copy("token"), children: copied === "token" ? "Copied" : "Copy" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center", children: [canNativeShare() ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-primary", onClick: () => void share(), children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-share me-1" }), "Share\u2026"] })) : null, props.onDone ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onDone, children: "Done" })) : null] })] }));
40
+ }
@@ -0,0 +1,10 @@
1
+ import { type IInvitesConsumer } from "@peers-app/peers-sdk";
2
+ /**
3
+ * Return the typed local-host consumer for the System Invites contract.
4
+ *
5
+ * The consumer is cached per host transport and rebuilt if the transport is replaced
6
+ * (e.g. after a socket reconnect).
7
+ */
8
+ export declare function getInvitesConsumer(): Promise<IInvitesConsumer>;
9
+ /** Shorthand for `(await getInvitesConsumer()).tools`. */
10
+ export declare function inviteTools(): Promise<IInvitesConsumer["tools"]>;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getInvitesConsumer = getInvitesConsumer;
4
+ exports.inviteTools = inviteTools;
5
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
+ const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
7
+ let cached;
8
+ let pending;
9
+ (0, peers_sdk_1.subscribeHostContractTransport)((transport) => {
10
+ if (cached && cached.transport !== transport) {
11
+ cached.consumer.dispose();
12
+ cached = undefined;
13
+ pending = undefined;
14
+ }
15
+ }, { emitCurrent: false });
16
+ async function load(transport) {
17
+ const userContext = await (0, peers_sdk_1.getUserContext)();
18
+ const consumer = (0, peers_sdk_1.createContractConsumer)((0, peers_sdk_1.getInvitesContractDefinition)(), transport, {
19
+ dataContextId: userContext.userId,
20
+ });
21
+ try {
22
+ await consumer.loadingPromise;
23
+ if ((0, peers_sdk_1.getHostContractTransport)() !== transport) {
24
+ consumer.dispose();
25
+ return getInvitesConsumer();
26
+ }
27
+ cached = { transport, consumer };
28
+ return consumer;
29
+ }
30
+ catch (error) {
31
+ consumer.dispose();
32
+ throw error;
33
+ }
34
+ }
35
+ /**
36
+ * Return the typed local-host consumer for the System Invites contract.
37
+ *
38
+ * The consumer is cached per host transport and rebuilt if the transport is replaced
39
+ * (e.g. after a socket reconnect).
40
+ */
41
+ async function getInvitesConsumer() {
42
+ const transport = (0, peers_sdk_1.getHostContractTransport)() ??
43
+ (await (0, peers_sdk_1.waitForHostContractTransport)({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
44
+ if (cached?.transport === transport) {
45
+ return cached.consumer;
46
+ }
47
+ if (pending) {
48
+ return pending;
49
+ }
50
+ pending = load(transport);
51
+ try {
52
+ return await pending;
53
+ }
54
+ finally {
55
+ pending = undefined;
56
+ }
57
+ }
58
+ /** Shorthand for `(await getInvitesConsumer()).tools`. */
59
+ async function inviteTools() {
60
+ return (await getInvitesConsumer()).tools;
61
+ }
@@ -0,0 +1,9 @@
1
+ /** Screen props supplied by the router. */
2
+ export interface InvitesScreenProps {
3
+ /** When `true`, the Accept panel starts open (the `invites/accept` route). */
4
+ accept?: boolean;
5
+ }
6
+ /**
7
+ * Invites hub: accept a pasted/scanned/deep-linked invite and review everything pending.
8
+ */
9
+ export declare function InvitesScreen(props: InvitesScreenProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvitesScreen = InvitesScreen;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const ui_loader_1 = require("../../ui-router/ui-loader");
7
+ const accept_invite_1 = require("./accept-invite");
8
+ const invite_capture_1 = require("./invite-capture");
9
+ const pending_invites_list_1 = require("./pending-invites-list");
10
+ /**
11
+ * Invites hub: accept a pasted/scanned/deep-linked invite and review everything pending.
12
+ */
13
+ function InvitesScreen(props) {
14
+ const [accepting, setAccepting] = (0, react_1.useState)(Boolean(props.accept || (0, invite_capture_1.peekPendingInvite)()));
15
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", style: { maxWidth: 720 }, 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", { className: "mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-envelope-paper me-2" }), "Invites"] }), !accepting ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-primary btn-sm", onClick: () => setAccepting(true), children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" }), "Accept an invite"] })) : null] }), accepting ? ((0, jsx_runtime_1.jsx)("div", { className: "card mb-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title", children: "Accept an invite" }), (0, jsx_runtime_1.jsx)(accept_invite_1.AcceptInvite, { onDone: () => setAccepting(false) })] }) })) : null, (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { title: "Waiting on you or others" }), (0, jsx_runtime_1.jsxs)("details", { className: "mt-3", children: [(0, jsx_runtime_1.jsx)("summary", { className: "small text-muted", style: { cursor: "pointer" }, children: "Recent history" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-2", children: (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { statuses: ["accepted", "declined", "expired", "revoked"] }) })] })] }));
16
+ }
17
+ (0, ui_loader_1.registerInternalPeersUI)({
18
+ peersUIId: "00invites0screen000000001",
19
+ component: InvitesScreen,
20
+ routes: [
21
+ {
22
+ isMatch: (props, context) => {
23
+ if (context.path === "invites") {
24
+ return true;
25
+ }
26
+ if (context.path === invite_capture_1.ACCEPT_INVITE_PATH) {
27
+ props.accept = true;
28
+ return true;
29
+ }
30
+ return false;
31
+ },
32
+ uiCategory: "screen",
33
+ priority: 2,
34
+ },
35
+ ],
36
+ });
@@ -0,0 +1,29 @@
1
+ import { type IInvite, type InviteStatus } from "@peers-app/peers-sdk";
2
+ /** Props for {@link PendingInvitesList}. */
3
+ export interface PendingInvitesListProps {
4
+ /** Only show rows of this type. */
5
+ type?: IInvite["type"];
6
+ /** Only show rows for this group. */
7
+ groupId?: string;
8
+ /** Statuses to include; defaults to pending only. */
9
+ statuses?: InviteStatus[];
10
+ /** Hide the section entirely when there is nothing to show. */
11
+ hideWhenEmpty?: boolean;
12
+ title?: string;
13
+ }
14
+ /** Subscribe to the personal `Invites` table and return rows matching the filter, newest first. */
15
+ export declare function useInviteRows(filter: {
16
+ type?: IInvite["type"];
17
+ groupId?: string;
18
+ statuses?: InviteStatus[];
19
+ }): IInvite[] | undefined;
20
+ /** Human summary of what a row is, from the local user's point of view. */
21
+ export declare function describeInviteRow(row: IInvite): {
22
+ title: string;
23
+ detail: string;
24
+ };
25
+ /**
26
+ * Live list of invite rows with the actions each one supports. Subscribes to the `Invites`
27
+ * table directly, so it updates when replies arrive on any of the user's devices.
28
+ */
29
+ export declare function PendingInvitesList(props: PendingInvitesListProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.useInviteRows = useInviteRows;
4
+ exports.describeInviteRow = describeInviteRow;
5
+ exports.PendingInvitesList = PendingInvitesList;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
8
+ const react_1 = require("react");
9
+ const invites_ipc_1 = require("./invites-ipc");
10
+ /** Subscribe to the personal `Invites` table and return rows matching the filter, newest first. */
11
+ function useInviteRows(filter) {
12
+ const [rows, setRows] = (0, react_1.useState)(undefined);
13
+ const statuses = filter.statuses ?? ["pending"];
14
+ const statusKey = statuses.join(",");
15
+ const refresh = (0, react_1.useCallback)(async () => {
16
+ const userContext = await (0, peers_sdk_1.getUserContext)();
17
+ const table = (0, peers_sdk_1.Invites)(userContext.userDataContext);
18
+ const all = await table.list({
19
+ ...(filter.type ? { type: filter.type } : {}),
20
+ ...(filter.groupId ? { groupId: filter.groupId } : {}),
21
+ });
22
+ const wanted = new Set(statusKey.split(","));
23
+ setRows(all
24
+ .filter((row) => wanted.has(row.status))
25
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()));
26
+ }, [filter.type, filter.groupId, statusKey]);
27
+ (0, react_1.useEffect)(() => {
28
+ let disposed = false;
29
+ let dispose;
30
+ void refresh();
31
+ void (0, peers_sdk_1.getUserContext)().then((userContext) => {
32
+ if (disposed) {
33
+ return;
34
+ }
35
+ const sub = (0, peers_sdk_1.Invites)(userContext.userDataContext).dataChanged.subscribe(() => void refresh());
36
+ dispose = () => {
37
+ sub.unsubscribe();
38
+ };
39
+ });
40
+ return () => {
41
+ disposed = true;
42
+ dispose?.();
43
+ };
44
+ }, [refresh]);
45
+ return rows;
46
+ }
47
+ function counterpartLabel(row) {
48
+ return row.counterpart?.displayName || row.counterpart?.userId || "Someone";
49
+ }
50
+ /** Human summary of what a row is, from the local user's point of view. */
51
+ function describeInviteRow(row) {
52
+ const who = counterpartLabel(row);
53
+ const role = row.role !== undefined ? (0, peers_sdk_1.getRoleLabel)(row.role) : undefined;
54
+ const group = row.groupName ?? "a group";
55
+ if (row.type === "contact") {
56
+ if (row.direction === "in") {
57
+ return { title: `${who} wants to connect`, detail: "Accept to add them as a contact." };
58
+ }
59
+ if (row.counterpart) {
60
+ return { title: `Contact invite to ${who}`, detail: "Waiting for them to accept." };
61
+ }
62
+ return {
63
+ title: row.mode === "profile" ? "Your profile QR" : "Contact invite link",
64
+ detail: row.mode === "profile" ? "Reusable; regenerate to revoke." : "Not used yet.",
65
+ };
66
+ }
67
+ if (row.direction === "in") {
68
+ if (row.mode === "direct") {
69
+ return {
70
+ title: `${who} invited you to ${group}`,
71
+ detail: role ? `You would join as ${role}.` : "",
72
+ };
73
+ }
74
+ return { title: `Joining ${group}`, detail: `Waiting for ${who} to approve.` };
75
+ }
76
+ if (row.mode === "direct") {
77
+ return { title: `Invited ${who} to ${group}`, detail: "Waiting for them to accept." };
78
+ }
79
+ if (row.counterpart) {
80
+ return {
81
+ title: `${who} wants to join ${group}`,
82
+ detail: role ? `Requested as ${role}. Approve or deny.` : "Approve or deny.",
83
+ };
84
+ }
85
+ return {
86
+ title: `${group} invite link`,
87
+ detail: `${row.mode === "open" ? "Open" : "Approval required"}${role ? ` · ${role}` : ""}`,
88
+ };
89
+ }
90
+ function actionsFor(row) {
91
+ if (row.status !== "pending") {
92
+ return [];
93
+ }
94
+ if (row.direction === "in") {
95
+ // Inbound rows I can act on: contact requests and direct group invites.
96
+ if (row.type === "contact" || row.mode === "direct") {
97
+ return ["accept", "decline"];
98
+ }
99
+ return []; // my own outstanding join request; nothing to do but wait
100
+ }
101
+ if (row.counterpart && row.type === "group" && row.mode !== "direct") {
102
+ return ["approve", "deny"];
103
+ }
104
+ return ["revoke"];
105
+ }
106
+ const ACTION_LABELS = {
107
+ accept: { text: "Accept", className: "btn-primary" },
108
+ decline: { text: "Decline", className: "btn-outline-secondary" },
109
+ approve: { text: "Approve", className: "btn-primary" },
110
+ deny: { text: "Deny", className: "btn-outline-secondary" },
111
+ revoke: { text: "Revoke", className: "btn-outline-danger" },
112
+ };
113
+ function InviteRow({ row }) {
114
+ const [busy, setBusy] = (0, react_1.useState)(undefined);
115
+ const [error, setError] = (0, react_1.useState)("");
116
+ const { title, detail } = describeInviteRow(row);
117
+ const run = async (action) => {
118
+ setBusy(action);
119
+ setError("");
120
+ try {
121
+ const tools = await (0, invites_ipc_1.inviteTools)();
122
+ const input = { inviteId: row.inviteId };
123
+ if (action === "accept")
124
+ await tools.acceptInvite(input);
125
+ else if (action === "decline")
126
+ await tools.declineInvite(input);
127
+ else if (action === "approve")
128
+ await tools.approveJoinRequest(input);
129
+ else if (action === "deny")
130
+ await tools.denyJoinRequest(input);
131
+ else
132
+ await tools.revokeInvite(input);
133
+ }
134
+ catch (err) {
135
+ setError(err.message || "Could not update the invite.");
136
+ }
137
+ finally {
138
+ setBusy(undefined);
139
+ }
140
+ };
141
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "list-group-item", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: title }), detail ? (0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: detail }) : null, row.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("small", { className: "text-warning d-block", children: "Queued until they are reachable" })) : null, row.status !== "pending" ? ((0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block text-capitalize", children: row.status })) : null] }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex gap-1", children: actionsFor(row).map((action) => ((0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn btn-sm ${ACTION_LABELS[action].className}`, disabled: busy !== undefined, onClick: () => void run(action), children: busy === action ? "…" : ACTION_LABELS[action].text }, action))) })] }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-1 px-2 small mt-2 mb-0", children: error }) : null] }));
142
+ }
143
+ /**
144
+ * Live list of invite rows with the actions each one supports. Subscribes to the `Invites`
145
+ * table directly, so it updates when replies arrive on any of the user's devices.
146
+ */
147
+ function PendingInvitesList(props) {
148
+ const rows = useInviteRows(props);
149
+ if (!rows) {
150
+ return null;
151
+ }
152
+ if (rows.length === 0 && props.hideWhenEmpty) {
153
+ return null;
154
+ }
155
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "mb-3", children: [props.title ? (0, jsx_runtime_1.jsx)("h6", { className: "mb-2", children: props.title }) : null, rows.length === 0 ? ((0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-0", children: "No pending invites." })) : ((0, jsx_runtime_1.jsx)("div", { className: "list-group", children: rows.map((row) => ((0, jsx_runtime_1.jsx)(InviteRow, { row: row }, row.inviteId))) }))] }));
156
+ }
@@ -149,7 +149,7 @@ const ProfileSection = () => {
149
149
  };
150
150
  const AddDeviceSection = () => {
151
151
  const [open, setOpen] = (0, react_1.useState)(false);
152
- 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" }))] }));
152
+ 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: "On the new device choose \u201CSign in from another device\u201D, then type the code it shows here and approve. Account access is transferred only after you approve." }), 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" }))] }));
153
153
  };
154
154
  const PackagesRootDirectory = () => {
155
155
  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" })] }));
@@ -10,8 +10,7 @@ const color_mode_dropdown_1 = require("./settings/color-mode-dropdown");
10
10
  const SetupUser = () => {
11
11
  const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
12
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));
13
+ const [showPairing, setShowPairing] = (0, react_1.useState)(false);
15
14
  const [isExistingUser, setIsExistingUser] = (0, react_1.useState)(null);
16
15
  const [userId, setUserId] = (0, react_1.useState)("");
17
16
  const [secretKey, setSecretKey] = (0, react_1.useState)("");
@@ -19,16 +18,6 @@ const SetupUser = () => {
19
18
  const [isValidating, setIsValidating] = (0, react_1.useState)(false);
20
19
  document.getElementById("appLoadingDiv")?.remove();
21
20
  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
- }, []);
32
21
  const handleNewUser = async () => {
33
22
  setError("");
34
23
  setIsValidating(true);
@@ -83,10 +72,7 @@ const SetupUser = () => {
83
72
  maxWidth: "560px",
84
73
  width: "100%",
85
74
  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
- } }) }) }) }));
75
+ }, 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, { onBack: () => setShowPairing(false) }) }) }) }));
90
76
  }
91
77
  // Step 1: Select User Type
92
78
  if (step === "select-user-type" && isExistingUser === null) {
@@ -94,7 +80,7 @@ const SetupUser = () => {
94
80
  maxWidth: "500px",
95
81
  width: "100%",
96
82
  backgroundColor: isDark ? "#2d3238" : "#ffffff",
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" })] })] }) })] })] }) }) }));
83
+ }, 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: "New account" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Create a new account on this device" })] })] }) }), (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: "Sign in from another device" }), (0, jsx_runtime_1.jsx)("small", { className: "opacity-75", children: "Show a code to a device that is already signed in" })] })] }) }), (0, jsx_runtime_1.jsxs)("details", { className: "text-center mt-2", children: [(0, jsx_runtime_1.jsx)("summary", { className: `small ${isDark ? "text-light opacity-75" : "text-muted"}`, style: { cursor: "pointer" }, children: "Advanced" }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: `btn btn-sm mt-2 ${isDark ? "btn-outline-light" : "btn-outline-secondary"}`, onClick: () => setIsExistingUser(true), children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-key me-1" }), "Enter a user ID and secret key"] })] })] })] }) }) }));
98
84
  }
99
85
  // Step 2: New User Confirmation
100
86
  if (isExistingUser === false) {
@@ -110,7 +96,7 @@ const SetupUser = () => {
110
96
  maxWidth: "500px",
111
97
  width: "100%",
112
98
  backgroundColor: isDark ? "#2d3238" : "#ffffff",
113
- }, 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-2", children: [(0, jsx_runtime_1.jsxs)("h3", { className: `fw-bold mb-2 ${isDark ? "text-light" : ""}`, children: [(0, jsx_runtime_1.jsx)("span", { children: "Sign In" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right text-primary" })] }), (0, jsx_runtime_1.jsx)("p", { className: isDark ? "text-light opacity-75" : "text-muted", children: "Enter your existing credentials" })] }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-2" }), error] })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: (e) => {
99
+ }, 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-2", children: [(0, jsx_runtime_1.jsxs)("h3", { className: `fw-bold mb-2 ${isDark ? "text-light" : ""}`, children: [(0, jsx_runtime_1.jsx)("span", { children: "Sign In" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right text-primary" })] }), (0, jsx_runtime_1.jsx)("p", { className: isDark ? "text-light opacity-75" : "text-muted", children: "Paste your user ID and secret key. If you have another signed-in device, \u201CSign in from another device\u201D is easier and safer." })] }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger", role: "alert", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-exclamation-triangle me-2" }), error] })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: (e) => {
114
100
  e.preventDefault();
115
101
  handleExistingUser();
116
102
  }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "userId", className: "form-label fw-semibold", children: "User ID" }), (0, jsx_runtime_1.jsx)("input", { id: "userId", type: "text", className: "form-control form-control-lg", placeholder: "Enter your user ID", value: userId, onChange: (e) => setUserId(e.target.value), disabled: isValidating })] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-4", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "secretKey", className: "form-label fw-semibold", children: "Secret Key" }), (0, jsx_runtime_1.jsx)("input", { id: "secretKey", type: "password", className: "form-control form-control-lg", placeholder: "Enter your secret key", value: secretKey, onChange: (e) => setSecretKey(e.target.value), disabled: isValidating })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "submit", className: "btn btn-lg btn-primary", disabled: isValidating, children: isValidating ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2" }), "Signing In..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-check-circle me-2" }), "Sign In"] })) }), (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-outline-secondary", onClick: () => setIsExistingUser(null), disabled: isValidating, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-left me-2" }), "Back"] })] })] })] }) }) }));
@@ -6,7 +6,7 @@ export { consoleLogsApp } from "./console-logs.app";
6
6
  export { contactsApp } from "./contacts.app";
7
7
  export { dataExplorerApp } from "./data-explorer.app";
8
8
  export { groupsApp } from "./groups.app";
9
- export { joinGroupApp } from "./join-group.app";
9
+ export { invitesApp } from "./invites.app";
10
10
  export { mobileSettingsApp } from "./mobile-settings.app";
11
11
  export { networkViewerApp } from "./network-viewer.app";
12
12
  export { packagesApp } from "./packages.app";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.systemPackage = exports.systemApps = exports.workflowsApp = exports.variablesApp = exports.typesApp = exports.toolsApp = exports.threadsApp = exports.settingsApp = exports.searchApp = exports.packagesApp = exports.networkViewerApp = exports.mobileSettingsApp = exports.joinGroupApp = exports.groupsApp = exports.dataExplorerApp = exports.contactsApp = exports.consoleLogsApp = exports.assistantsApp = exports.accountApp = void 0;
3
+ exports.systemPackage = exports.systemApps = exports.workflowsApp = exports.variablesApp = exports.typesApp = exports.toolsApp = exports.threadsApp = exports.settingsApp = exports.searchApp = exports.packagesApp = exports.networkViewerApp = exports.mobileSettingsApp = exports.invitesApp = exports.groupsApp = exports.dataExplorerApp = exports.contactsApp = exports.consoleLogsApp = exports.assistantsApp = exports.accountApp = void 0;
4
4
  var account_app_1 = require("./account.app");
5
5
  Object.defineProperty(exports, "accountApp", { enumerable: true, get: function () { return account_app_1.accountApp; } });
6
6
  var assistants_app_1 = require("./assistants.app");
@@ -13,8 +13,8 @@ var data_explorer_app_1 = require("./data-explorer.app");
13
13
  Object.defineProperty(exports, "dataExplorerApp", { enumerable: true, get: function () { return data_explorer_app_1.dataExplorerApp; } });
14
14
  var groups_app_1 = require("./groups.app");
15
15
  Object.defineProperty(exports, "groupsApp", { enumerable: true, get: function () { return groups_app_1.groupsApp; } });
16
- var join_group_app_1 = require("./join-group.app");
17
- Object.defineProperty(exports, "joinGroupApp", { enumerable: true, get: function () { return join_group_app_1.joinGroupApp; } });
16
+ var invites_app_1 = require("./invites.app");
17
+ Object.defineProperty(exports, "invitesApp", { enumerable: true, get: function () { return invites_app_1.invitesApp; } });
18
18
  var mobile_settings_app_1 = require("./mobile-settings.app");
19
19
  Object.defineProperty(exports, "mobileSettingsApp", { enumerable: true, get: function () { return mobile_settings_app_1.mobileSettingsApp; } });
20
20
  var network_viewer_app_1 = require("./network-viewer.app");
@@ -42,7 +42,7 @@ const console_logs_app_2 = require("./console-logs.app");
42
42
  const contacts_app_2 = require("./contacts.app");
43
43
  const data_explorer_app_2 = require("./data-explorer.app");
44
44
  const groups_app_2 = require("./groups.app");
45
- const join_group_app_2 = require("./join-group.app");
45
+ const invites_app_2 = require("./invites.app");
46
46
  const mobile_settings_app_2 = require("./mobile-settings.app");
47
47
  const network_viewer_app_2 = require("./network-viewer.app");
48
48
  const packages_app_2 = require("./packages.app");
@@ -76,7 +76,7 @@ exports.systemApps = [
76
76
  packages_app_2.packagesApp,
77
77
  contacts_app_2.contactsApp,
78
78
  groups_app_2.groupsApp,
79
- join_group_app_2.joinGroupApp,
79
+ invites_app_2.invitesApp,
80
80
  // User & Settings Apps
81
81
  account_app_2.accountApp,
82
82
  settings_app_2.settingsApp,
@@ -1,2 +1,2 @@
1
1
  import type { IAppNav } from "@peers-app/peers-sdk";
2
- export declare const joinGroupApp: IAppNav;
2
+ export declare const invitesApp: IAppNav;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.invitesApp = void 0;
4
+ exports.invitesApp = {
5
+ name: "Invites",
6
+ displayName: "Invites",
7
+ iconClassName: "bi-envelope-paper",
8
+ navigationPath: "invites",
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.23.13",
3
+ "version": "0.24.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -28,7 +28,7 @@
28
28
  "lint:fix": "biome check --write ."
29
29
  },
30
30
  "peerDependencies": {
31
- "@peers-app/peers-sdk": "^0.23.13",
31
+ "@peers-app/peers-sdk": "^0.24.0",
32
32
  "bootstrap": "^5.3.3",
33
33
  "react": "^18.0.0",
34
34
  "react-dom": "^18.0.0"
@@ -39,7 +39,7 @@
39
39
  "@babel/preset-react": "^7.24.1",
40
40
  "@babel/preset-typescript": "^7.27.1",
41
41
  "@biomejs/biome": "2.3.11",
42
- "@peers-app/peers-sdk": "0.23.13",
42
+ "@peers-app/peers-sdk": "0.24.0",
43
43
  "@testing-library/dom": "^10.4.0",
44
44
  "@testing-library/jest-dom": "^6.6.3",
45
45
  "@testing-library/react": "^16.3.0",
@@ -89,6 +89,7 @@
89
89
  "dotenv": "^16.4.5",
90
90
  "fast-json-stable-stringify": "^2.1.0",
91
91
  "js-sha256": "^0.11.0",
92
+ "jsqr": "^1.4.0",
92
93
  "keytar": "^7.9.0",
93
94
  "langchain": "^0.1.36",
94
95
  "lexical": "^0.41.0",