@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,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GroupInvitePanel = GroupInvitePanel;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
+ const react_1 = require("react");
7
+ const typeahead_1 = require("../../components/typeahead");
8
+ const invite_share_1 = require("../invites/invite-share");
9
+ const invites_ipc_1 = require("../invites/invites-ipc");
10
+ const pending_invites_list_1 = require("../invites/pending-invites-list");
11
+ const ROLE_OPTIONS = [
12
+ { value: peers_sdk_1.GroupMemberRole.Reader, label: "Reader" },
13
+ { value: peers_sdk_1.GroupMemberRole.Writer, label: "Writer" },
14
+ { value: peers_sdk_1.GroupMemberRole.Admin, label: "Admin" },
15
+ { value: peers_sdk_1.GroupMemberRole.Owner, label: "Owner" },
16
+ ];
17
+ const EXPIRY_OPTIONS = [
18
+ { ms: 60 * 60_000, label: "1 hour" },
19
+ { ms: 24 * 60 * 60_000, label: "1 day" },
20
+ { ms: 7 * 24 * 60 * 60_000, label: "7 days" },
21
+ { ms: 30 * 24 * 60 * 60_000, label: "30 days" },
22
+ ];
23
+ function RoleSelect(props) {
24
+ return ((0, jsx_runtime_1.jsx)("select", { className: "form-select", value: props.value, disabled: props.disabled, onChange: (event) => props.onChange(Number(event.target.value)), "aria-label": "Role", children: ROLE_OPTIONS.filter((option) => option.value <= props.max).map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.value, children: option.label }, option.value))) }));
25
+ }
26
+ function ContactPicker(props) {
27
+ const search = async (query) => {
28
+ const cursor = await (0, peers_sdk_1.usersCursor)({ name: { $matchWords: query } }, { includeTrustLevel: true });
29
+ const results = [];
30
+ for await (const contact of cursor) {
31
+ if (!props.excludeUserIds.includes(contact.userId)) {
32
+ results.push(contact);
33
+ }
34
+ if (results.length >= 10)
35
+ break;
36
+ }
37
+ return results.map((contact) => ({ ...contact, id: contact.userId }));
38
+ };
39
+ return ((0, jsx_runtime_1.jsx)(typeahead_1.Typeahead, { placeholder: "Search contacts\u2026", searchFn: search, onSelectionChange: (items) => props.onChange(items), renderItem: (contact) => ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-circle me-2", style: { fontSize: 20 } }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: contact.name }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: contact.userId })] })] })), renderBadge: (contact) => (0, jsx_runtime_1.jsx)("span", { children: contact.name }), selectedItems: props.selected.map((contact) => ({ ...contact, id: contact.userId })), multiSelect: true, disabled: props.disabled }));
40
+ }
41
+ /** Invite known contacts directly (they get a ready-to-accept invite; no link needed). */
42
+ function InviteContactsCard(props) {
43
+ const [selected, setSelected] = (0, react_1.useState)([]);
44
+ const [role, setRole] = (0, react_1.useState)(peers_sdk_1.GroupMemberRole.Writer);
45
+ const [busy, setBusy] = (0, react_1.useState)(false);
46
+ const [message, setMessage] = (0, react_1.useState)(undefined);
47
+ const send = async () => {
48
+ setBusy(true);
49
+ setMessage(undefined);
50
+ try {
51
+ const tools = await (0, invites_ipc_1.inviteTools)();
52
+ let queued = 0;
53
+ for (const contact of selected) {
54
+ const { result } = await tools.inviteContactToGroup({
55
+ groupId: props.groupId,
56
+ userId: contact.userId,
57
+ role,
58
+ });
59
+ if (result.deliveryState === "queued")
60
+ queued++;
61
+ }
62
+ setMessage({
63
+ ok: true,
64
+ text: queued > 0
65
+ ? `Invited ${selected.length}. ${queued} will be delivered when they are reachable.`
66
+ : `Invited ${selected.length}. They will appear once they accept.`,
67
+ });
68
+ setSelected([]);
69
+ }
70
+ catch (err) {
71
+ setMessage({ ok: false, text: err.message || "Could not send the invite." });
72
+ }
73
+ finally {
74
+ setBusy(false);
75
+ }
76
+ };
77
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "row g-2 align-items-start", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-6", children: (0, jsx_runtime_1.jsx)(ContactPicker, { excludeUserIds: props.excludeUserIds, selected: selected, disabled: busy, onChange: setSelected }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-3", children: (0, jsx_runtime_1.jsx)(RoleSelect, { value: role, max: props.myRole, disabled: busy, onChange: setRole }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-3", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary w-100", disabled: busy || selected.length === 0, onClick: () => void send(), children: busy
78
+ ? "Sending…"
79
+ : selected.length > 1
80
+ ? `Invite ${selected.length} contacts`
81
+ : "Invite contact" }) }), message ? ((0, jsx_runtime_1.jsx)("div", { className: "col-12", children: (0, jsx_runtime_1.jsx)("div", { className: `alert py-2 mb-0 ${message.ok ? "alert-success" : "alert-danger"}`, children: message.text }) })) : null] }));
82
+ }
83
+ /** Issue a shareable link for people who are not contacts yet. */
84
+ function ShareLinkCard(props) {
85
+ const [role, setRole] = (0, react_1.useState)(peers_sdk_1.GroupMemberRole.Reader);
86
+ const [mode, setMode] = (0, react_1.useState)("approve");
87
+ const [expiresInMs, setExpiresInMs] = (0, react_1.useState)(EXPIRY_OPTIONS[1].ms);
88
+ const [busy, setBusy] = (0, react_1.useState)(false);
89
+ const [error, setError] = (0, react_1.useState)("");
90
+ const [invite, setInvite] = (0, react_1.useState)(undefined);
91
+ const create = async () => {
92
+ setBusy(true);
93
+ setError("");
94
+ try {
95
+ const tools = await (0, invites_ipc_1.inviteTools)();
96
+ const { invite: created } = await tools.createGroupInvite({
97
+ groupId: props.groupId,
98
+ role,
99
+ mode,
100
+ expiresInMs,
101
+ });
102
+ setInvite(created);
103
+ }
104
+ catch (err) {
105
+ setError(err.message || "Could not create the link.");
106
+ }
107
+ finally {
108
+ setBusy(false);
109
+ }
110
+ };
111
+ if (invite) {
112
+ return ((0, jsx_runtime_1.jsx)(invite_share_1.InviteShare, { invite: invite, description: mode === "open"
113
+ ? "Anyone with this link joins immediately. Revoke it from the pending list when you are done."
114
+ : "People who use this link appear below as join requests for you to approve.", shareText: `Join ${invite.token.group?.name ?? "my group"} on Peers:`, onDone: () => setInvite(undefined) }));
115
+ }
116
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "row g-2 align-items-end", children: [(0, jsx_runtime_1.jsxs)("div", { className: "col-md-3", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "link-role", className: "form-label small text-muted mb-1", children: "Role" }), (0, jsx_runtime_1.jsx)(RoleSelect, { value: role, max: props.myRole, disabled: busy, onChange: setRole })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-3", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "link-mode", className: "form-label small text-muted mb-1", children: "Approval" }), (0, jsx_runtime_1.jsxs)("select", { id: "link-mode", className: "form-select", value: mode, disabled: busy, onChange: (event) => setMode(event.target.value), children: [(0, jsx_runtime_1.jsx)("option", { value: "approve", children: "I approve each join" }), (0, jsx_runtime_1.jsx)("option", { value: "open", children: "Anyone with the link joins" })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "col-md-3", children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "link-expiry", className: "form-label small text-muted mb-1", children: "Expires in" }), (0, jsx_runtime_1.jsx)("select", { id: "link-expiry", className: "form-select", value: expiresInMs, disabled: busy, onChange: (event) => setExpiresInMs(Number(event.target.value)), children: EXPIRY_OPTIONS.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.ms, children: option.label }, option.ms))) })] }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-3", children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-primary w-100", disabled: busy, onClick: () => void create(), children: busy ? "Creating…" : "Create link" }) }), error ? ((0, jsx_runtime_1.jsx)("div", { className: "col-12", children: (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2 mb-0", children: error }) })) : null] }));
117
+ }
118
+ /**
119
+ * Admin controls for growing a group: invite existing contacts first, share a link second,
120
+ * and act on pending join requests. Membership changes land when the other side accepts.
121
+ */
122
+ function GroupInvitePanel(props) {
123
+ const [tab, setTab] = (0, react_1.useState)("contacts");
124
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: "card mb-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "card-header d-flex align-items-center gap-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "mb-0", children: "Invite people" }), (0, jsx_runtime_1.jsxs)("div", { className: "btn-group btn-group-sm ms-auto", role: "group", "aria-label": "Invite method", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn ${tab === "contacts" ? "btn-primary" : "btn-outline-primary"}`, onClick: () => setTab("contacts"), children: "Contacts" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: `btn ${tab === "link" ? "btn-primary" : "btn-outline-primary"}`, onClick: () => setTab("link"), children: "Share a link" })] })] }), (0, jsx_runtime_1.jsx)("div", { className: "card-body", children: tab === "contacts" ? (0, jsx_runtime_1.jsx)(InviteContactsCard, { ...props }) : (0, jsx_runtime_1.jsx)(ShareLinkCard, { ...props }) })] }), (0, jsx_runtime_1.jsx)(pending_invites_list_1.PendingInvitesList, { type: "group", groupId: props.groupId, hideWhenEmpty: true, title: "Pending invites and join requests" })] }));
125
+ }
@@ -9,9 +9,8 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
9
9
  const react_1 = __importDefault(require("react"));
10
10
  const loading_indicator_1 = require("../../components/loading-indicator");
11
11
  const trust_level_badge_1 = require("../../components/trust-level-badge");
12
- const typeahead_1 = require("../../components/typeahead");
13
12
  const hooks_1 = require("../../hooks");
14
- const group_invite_listener_1 = require("./group-invite-listener");
13
+ const group_invite_panel_1 = require("./group-invite-panel");
15
14
  const GroupMembersUI = (props) => {
16
15
  const { groupId, userContext } = props;
17
16
  // Get the group's data context and tables
@@ -99,97 +98,13 @@ const GroupMembersUI = (props) => {
99
98
  founderUserId: group.founderUserId,
100
99
  };
101
100
  }, undefined, [groupId, refreshKey]);
102
- const [newMemberRole, setNewMemberRole] = react_1.default.useState(peers_sdk_1.GroupMemberRole.Reader);
103
- const [addingMember, setAddingMember] = react_1.default.useState(false);
104
- const [selectedContacts, setSelectedContacts] = react_1.default.useState([]);
105
- // Search function for typeahead
106
- const searchContacts = async (query) => {
107
- try {
108
- const cursor = await (0, peers_sdk_1.usersCursor)({ name: { $matchWords: query } }, { includeTrustLevel: true });
109
- const results = [];
110
- // Get first 10 results
111
- for await (const contact of cursor) {
112
- results.push(contact);
113
- if (results.length >= 10)
114
- break;
115
- }
116
- // Filter out users who are already members
117
- const currentMembers = membersData?.map((m) => m.member.userId) || [];
118
- const filteredResults = results.filter((contact) => !currentMembers.includes(contact.userId));
119
- return filteredResults.map((contact) => ({
120
- ...contact,
121
- id: contact.userId,
122
- }));
123
- }
124
- catch (error) {
125
- console.error("Error searching contacts:", error);
126
- return [];
127
- }
128
- };
129
- // Render function for typeahead results
130
- const renderContactItem = (contact, _isSelected) => ((0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-circle me-2", style: { fontSize: "20px" } }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "fw-semibold", children: contact.name }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: contact.userId })] }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsxs)("small", { className: "text-info", children: [(0, jsx_runtime_1.jsx)("i", { className: `bi-${getSourceIcon(contact.source)} me-1` }), getSourceLabel(contact.source)] }) })] }));
131
- // Helper functions for contact sources
132
- const getSourceLabel = (source) => {
133
- switch (source) {
134
- case "userDataContext":
135
- return "Personal";
136
- case "currentDataContext":
137
- return "Current";
138
- case "otherDataContexts":
139
- return "Groups";
140
- default:
141
- return source;
142
- }
143
- };
144
- const getSourceIcon = (source) => {
145
- switch (source) {
146
- case "userDataContext":
147
- return "person-fill";
148
- case "currentDataContext":
149
- return "house-fill";
150
- case "otherDataContexts":
151
- return "people-fill";
152
- default:
153
- return "question-circle";
154
- }
155
- };
156
- const addMember = async () => {
157
- if (selectedContacts.length === 0)
158
- return;
159
- setAddingMember(true);
160
- try {
161
- // Add all selected contacts with the chosen role
162
- for (const contact of selectedContacts) {
163
- let member = await groupMembersTable.findOne({ userId: contact.userId, groupId });
164
- if (member) {
165
- member.role = newMemberRole;
166
- member.deleted = false;
167
- }
168
- else {
169
- member = await groupMembersTable.initRecord({
170
- groupId: groupId,
171
- userId: contact.userId,
172
- role: newMemberRole,
173
- });
174
- }
175
- await groupMembersTable.signAndSave(member);
176
- }
177
- // Reset form
178
- setSelectedContacts([]);
179
- setNewMemberRole(peers_sdk_1.GroupMemberRole.Reader);
180
- // Refresh the members list
181
- setRefreshKey((prev) => prev + 1);
182
- }
183
- catch (err) {
184
- console.error("Failed to add member:", err);
185
- alert("Failed to add member. They may already be in the group.");
186
- }
187
- finally {
188
- setAddingMember(false);
189
- }
190
- };
191
- // Render function for contact badges
192
- const renderContactBadge = (contact) => (0, jsx_runtime_1.jsx)("span", { children: contact.name });
101
+ // Refresh the member list when an accepted invite adds a membership row.
102
+ react_1.default.useEffect(() => {
103
+ const sub = groupMembersTable.dataChanged.subscribe(() => setRefreshKey((prev) => prev + 1));
104
+ return () => {
105
+ sub.unsubscribe();
106
+ };
107
+ }, [groupMembersTable]);
193
108
  const updateRole = async (memberId, newRole) => {
194
109
  try {
195
110
  const member = await groupMembersTable.get(memberId);
@@ -220,16 +135,7 @@ const GroupMembersUI = (props) => {
220
135
  return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
221
136
  }
222
137
  const { canEdit, userRole, currentUserId } = userPermissions;
223
- return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "mb-0", children: "Group Members" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: `alert alert-${userRole ? "primary" : "warning"} py-2 px-3 mb-0 d-flex align-items-center`, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${userRole ? "bi-person-badge" : "bi-exclamation-triangle"} me-2` }), (0, jsx_runtime_1.jsxs)("strong", { children: ["Your role: ", (0, peers_sdk_1.getRoleLabel)(userRole)] })] }), founderData && founderData.founderUserId !== currentUserId && ((0, jsx_runtime_1.jsxs)("a", { href: `#contacts/${founderData.founderUserId}`, className: "alert alert-warning py-2 px-3 mb-0 d-flex align-items-center text-decoration-none", style: { cursor: "pointer" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-crown-fill me-2" }), (0, jsx_runtime_1.jsxs)("strong", { children: ["Founder: ", founderData.founderUser?.name || founderData.founderUserId] })] }))] })] }), canEdit && (0, jsx_runtime_1.jsx)(group_invite_listener_1.GroupInviteListener, { groupId: groupId, userContext: userContext }), canEdit && ((0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h6", { className: "card-title", children: "Add Existing Contact as Member" }), (0, jsx_runtime_1.jsxs)("div", { className: "row g-2", children: [(0, jsx_runtime_1.jsx)("div", { className: "col-md-6", children: (0, jsx_runtime_1.jsx)(typeahead_1.Typeahead, { placeholder: "Search contacts to add...", searchFn: searchContacts, onSelectionChange: (items) => setSelectedContacts(items), renderItem: renderContactItem, renderBadge: renderContactBadge, selectedItems: selectedContacts.map((contact) => ({
224
- ...contact,
225
- id: contact.userId,
226
- })), multiSelect: true, disabled: addingMember }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-3", children: (0, jsx_runtime_1.jsxs)("select", { className: "form-select", value: newMemberRole, onChange: (e) => setNewMemberRole(Number(e.target.value) || 0), disabled: addingMember, children: [(0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.None, children: "None" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Reader, children: "Reader" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Writer, children: "Writer" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Admin, children: "Admin" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Owner, children: "Owner" })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "col-md-3", children: (0, jsx_runtime_1.jsx)("button", { className: "btn btn-primary w-100", onClick: addMember, disabled: addingMember || selectedContacts.length === 0, children: addingMember
227
- ? "Adding..."
228
- : selectedContacts.length === 1
229
- ? "Add Member"
230
- : selectedContacts.length > 1
231
- ? `Add ${selectedContacts.length} Members`
232
- : "Add" }) })] })] }) })), !canEdit && userRole && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-info", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-info-circle me-2" }), "You can view group members, but only admins and owners can manage membership."] })), membersData.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-info", children: "No members in this group yet. Add the first member above." })) : ((0, jsx_runtime_1.jsx)("div", { className: "list-group", children: membersData.map(({ member, user }) => ((0, jsx_runtime_1.jsx)("div", { className: "list-group-item", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-fill me-2" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("strong", { children: [user?.name || member.userId, member.userId === currentUserId && " (You)"] }), user?.source === "userDataContext" && ((0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info ms-2", style: { fontSize: "0.7rem" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person me-1" }), "Personal"] })), user?.source === "otherDataContexts" && ((0, jsx_runtime_1.jsxs)("span", { className: "badge bg-secondary ms-2", style: { fontSize: "0.7rem" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-people me-1" }), "Other Groups"] }))] }), user && user.userId !== user.name && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: member.userId }))] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "me-2", children: (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: user?.trustLevel || peers_sdk_1.TrustLevel.Unknown }) }), (0, jsx_runtime_1.jsxs)("a", { href: `#contacts/${member.userId}`, className: "btn btn-outline-primary btn-sm me-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-eye me-1" }), "View"] }), canEdit ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm me-2", style: { width: "120px" }, value: member.role, onChange: (e) => updateRole(member.groupMemberId, Number(e.target.value) || 0), children: [(0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.None, children: "None" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Reader, children: "Reader" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Writer, children: "Writer" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Admin, children: "Admin" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Owner, children: "Owner" })] }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-danger btn-sm", onClick: () => removeMember(member.groupMemberId), title: "Remove member", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" }) })] })) : (
138
+ return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsx)("h6", { className: "mb-0", children: "Group Members" }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: `alert alert-${userRole ? "primary" : "warning"} py-2 px-3 mb-0 d-flex align-items-center`, children: [(0, jsx_runtime_1.jsx)("i", { className: `bi ${userRole ? "bi-person-badge" : "bi-exclamation-triangle"} me-2` }), (0, jsx_runtime_1.jsxs)("strong", { children: ["Your role: ", (0, peers_sdk_1.getRoleLabel)(userRole)] })] }), founderData && founderData.founderUserId !== currentUserId && ((0, jsx_runtime_1.jsxs)("a", { href: `#contacts/${founderData.founderUserId}`, className: "alert alert-warning py-2 px-3 mb-0 d-flex align-items-center text-decoration-none", style: { cursor: "pointer" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-crown-fill me-2" }), (0, jsx_runtime_1.jsxs)("strong", { children: ["Founder: ", founderData.founderUser?.name || founderData.founderUserId] })] }))] })] }), canEdit && ((0, jsx_runtime_1.jsx)(group_invite_panel_1.GroupInvitePanel, { groupId: groupId, excludeUserIds: membersData.map((m) => m.member.userId), myRole: userRole })), !canEdit && userRole && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-info", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-info-circle me-2" }), "You can view group members, but only admins and owners can manage membership."] })), membersData.length === 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-info", children: "No members in this group yet. Invite someone above." })) : ((0, jsx_runtime_1.jsx)("div", { className: "list-group", children: membersData.map(({ member, user }) => ((0, jsx_runtime_1.jsx)("div", { className: "list-group-item", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "flex-grow-1", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person-fill me-2" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("strong", { children: [user?.name || member.userId, member.userId === currentUserId && " (You)"] }), user?.source === "userDataContext" && ((0, jsx_runtime_1.jsxs)("span", { className: "badge bg-info ms-2", style: { fontSize: "0.7rem" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-person me-1" }), "Personal"] })), user?.source === "otherDataContexts" && ((0, jsx_runtime_1.jsxs)("span", { className: "badge bg-secondary ms-2", style: { fontSize: "0.7rem" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-people me-1" }), "Other Groups"] }))] }), user && user.userId !== user.name && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: member.userId }))] })] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "me-2", children: (0, jsx_runtime_1.jsx)(trust_level_badge_1.TrustLevelBadge, { level: user?.trustLevel || peers_sdk_1.TrustLevel.Unknown }) }), (0, jsx_runtime_1.jsxs)("a", { href: `#contacts/${member.userId}`, className: "btn btn-outline-primary btn-sm me-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-eye me-1" }), "View"] }), canEdit ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm me-2", style: { width: "120px" }, value: member.role, onChange: (e) => updateRole(member.groupMemberId, Number(e.target.value) || 0), children: [(0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.None, children: "None" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Reader, children: "Reader" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Writer, children: "Writer" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Admin, children: "Admin" }), (0, jsx_runtime_1.jsx)("option", { value: peers_sdk_1.GroupMemberRole.Owner, children: "Owner" })] }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-danger btn-sm", onClick: () => removeMember(member.groupMemberId), title: "Remove member", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" }) })] })) : (
233
139
  /* Read-only role display for non-admins */
234
140
  (0, jsx_runtime_1.jsx)("span", { className: "badge bg-secondary text-capitalize", children: (0, peers_sdk_1.getRoleLabel)(member.role) }))] })] }) }, member.groupMemberId))) })), (0, jsx_runtime_1.jsx)("div", { className: "mt-4", children: (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [(0, jsx_runtime_1.jsx)("strong", { children: "Roles:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("strong", { children: "Reader:" }), " Can view group content", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("strong", { children: "Writer:" }), " Can create and edit content", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("strong", { children: "Admin:" }), " Can manage members and settings", (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("strong", { children: "Owner:" }), " Full control including group deletion"] }) })] }));
235
141
  };
@@ -0,0 +1,11 @@
1
+ /** Props for {@link AcceptInvite}. */
2
+ export interface AcceptInviteProps {
3
+ /** Token to preview immediately (e.g. from a deep link); otherwise the paste box is shown. */
4
+ initialToken?: string;
5
+ onDone?: () => void;
6
+ }
7
+ /**
8
+ * Paste or scan an invite, review what it grants, then accept it. Also consumes a token
9
+ * captured from a link or `peers://` open before this screen mounted.
10
+ */
11
+ export declare function AcceptInvite(props: AcceptInviteProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AcceptInvite = AcceptInvite;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
+ const react_1 = require("react");
7
+ const qr_scanner_1 = require("../../components/qr-scanner");
8
+ const globals_1 = require("../../globals");
9
+ const invite_capture_1 = require("./invite-capture");
10
+ const invites_ipc_1 = require("./invites-ipc");
11
+ const TRUST_OPTIONS = [
12
+ { value: peers_sdk_1.TrustLevel.NewUser, label: "New user (default)" },
13
+ { value: peers_sdk_1.TrustLevel.Known, label: "Known" },
14
+ { value: peers_sdk_1.TrustLevel.Trusted, label: "Trusted" },
15
+ ];
16
+ function PreviewCard(props) {
17
+ const { preview } = props;
18
+ const inviter = preview.inviter.displayName || preview.inviter.userId;
19
+ return ((0, jsx_runtime_1.jsx)("div", { className: "card mb-3", children: (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsx)("h5", { className: "card-title mb-2", children: preview.type === "group" ? `Join ${preview.group?.name}` : `Connect with ${inviter}` }), (0, jsx_runtime_1.jsxs)("div", { className: "small text-muted", children: ["From ", (0, jsx_runtime_1.jsx)("strong", { children: inviter }), preview.knownContact ? " (already a contact)" : ""] }), preview.group ? ((0, jsx_runtime_1.jsxs)("div", { className: "small text-muted", children: ["You would join as ", (0, jsx_runtime_1.jsx)("strong", { children: (0, peers_sdk_1.getRoleLabel)(preview.group.role) }), preview.group.mode === "open" ? " · joins immediately" : " · the inviter must approve", preview.group.alreadyMember ? " · you are already a member" : ""] })) : null, (0, jsx_runtime_1.jsxs)("div", { className: `small ${preview.expired ? "text-danger" : "text-muted"}`, children: [preview.expired ? "Expired " : "Expires ", new Date(preview.expiresAt).toLocaleString()] })] }) }));
20
+ }
21
+ function ResultPanel(props) {
22
+ const { result, preview } = props;
23
+ const accepted = result.status === "accepted";
24
+ const message = accepted
25
+ ? preview.type === "group"
26
+ ? `You joined ${preview.group?.name}.`
27
+ : "Contact added."
28
+ : preview.type === "group"
29
+ ? "Join request sent. You will be added once it is approved."
30
+ : "Request sent. They will see it and confirm.";
31
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("output", { className: `alert d-block ${accepted ? "alert-success" : "alert-info"}`, children: [message, result.deliveryState === "queued" ? ((0, jsx_runtime_1.jsx)("div", { className: "small mt-1", children: "Queued until the other person is reachable." })) : null] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center", children: [accepted && result.groupId ? ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: () => (0, globals_1.mainContentPath)(`groups/${result.groupId}`), children: "Open group" })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onDone, children: "Done" })] })] }));
32
+ }
33
+ /**
34
+ * Paste or scan an invite, review what it grants, then accept it. Also consumes a token
35
+ * captured from a link or `peers://` open before this screen mounted.
36
+ */
37
+ function AcceptInvite(props) {
38
+ const [text, setText] = (0, react_1.useState)(props.initialToken ?? "");
39
+ const [scanning, setScanning] = (0, react_1.useState)(false);
40
+ const [preview, setPreview] = (0, react_1.useState)(undefined);
41
+ const [result, setResult] = (0, react_1.useState)(undefined);
42
+ const [trustLevel, setTrustLevel] = (0, react_1.useState)(peers_sdk_1.TrustLevel.NewUser);
43
+ const [busy, setBusy] = (0, react_1.useState)(false);
44
+ const [error, setError] = (0, react_1.useState)("");
45
+ const runPreview = (0, react_1.useCallback)(async (token) => {
46
+ setError("");
47
+ setBusy(true);
48
+ try {
49
+ const tools = await (0, invites_ipc_1.inviteTools)();
50
+ const response = await tools.previewInvite({ token });
51
+ setPreview(response.preview);
52
+ setText(token);
53
+ }
54
+ catch (err) {
55
+ setPreview(undefined);
56
+ setError(err.message || "That is not a valid invite.");
57
+ }
58
+ finally {
59
+ setBusy(false);
60
+ }
61
+ }, []);
62
+ // Deep-linked tokens: one stashed before mount, or one arriving while mounted.
63
+ (0, react_1.useEffect)(() => {
64
+ const stashed = props.initialToken ?? (0, invite_capture_1.consumePendingInvite)();
65
+ if (stashed) {
66
+ void runPreview(stashed);
67
+ }
68
+ const sub = invite_capture_1.inviteCaptured.subscribe(() => {
69
+ const token = (0, invite_capture_1.consumePendingInvite)();
70
+ if (token) {
71
+ void runPreview(token);
72
+ }
73
+ });
74
+ return () => sub.dispose();
75
+ }, [props.initialToken, runPreview]);
76
+ const accept = async () => {
77
+ setError("");
78
+ setBusy(true);
79
+ try {
80
+ const tools = await (0, invites_ipc_1.inviteTools)();
81
+ const response = await tools.acceptInvite({ token: text, trustLevel });
82
+ setResult(response.result);
83
+ }
84
+ catch (err) {
85
+ setError(err.message || "Could not accept the invite.");
86
+ }
87
+ finally {
88
+ setBusy(false);
89
+ }
90
+ };
91
+ const reset = () => {
92
+ setText("");
93
+ setPreview(undefined);
94
+ setResult(undefined);
95
+ setError("");
96
+ };
97
+ const done = props.onDone ?? reset;
98
+ if (result && preview) {
99
+ return (0, jsx_runtime_1.jsx)(ResultPanel, { result: result, preview: preview, onDone: done });
100
+ }
101
+ if (scanning) {
102
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)(qr_scanner_1.QrScanner, { onScan: (scanned) => {
103
+ setScanning(false);
104
+ void runPreview(scanned);
105
+ }, onError: (message) => {
106
+ setScanning(false);
107
+ setError(message);
108
+ } }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary mt-3", onClick: () => setScanning(false), children: "Cancel" })] }));
109
+ }
110
+ return ((0, jsx_runtime_1.jsx)("div", { style: { maxWidth: 520, margin: "0 auto" }, children: preview ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(PreviewCard, { preview: preview }), !preview.knownContact ? ((0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsxs)("label", { htmlFor: "invite-trust", className: "form-label small text-muted mb-1", children: ["Trust level for ", preview.inviter.displayName || "this contact"] }), (0, jsx_runtime_1.jsx)("select", { id: "invite-trust", className: "form-select form-select-sm", value: trustLevel, onChange: (event) => setTrustLevel(Number(event.target.value)), disabled: busy, children: TRUST_OPTIONS.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option.value, children: option.label }, option.value))) })] })) : null, error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }) : null, (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", disabled: busy || preview.expired || preview.group?.alreadyMember, onClick: () => void accept(), children: busy ? "Working…" : preview.type === "group" ? "Join" : "Accept" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: reset, children: "Back" })] })] })) : ((0, jsx_runtime_1.jsxs)("form", { onSubmit: (event) => {
111
+ event.preventDefault();
112
+ void runPreview(text);
113
+ }, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "invite-text", className: "form-label", children: "Paste an invite link or code" }), (0, jsx_runtime_1.jsx)("textarea", { id: "invite-text", className: "form-control font-monospace mb-2", rows: 3, value: text, onChange: (event) => setText(event.target.value), placeholder: "https://peers.app/i#\u2026", disabled: busy, spellCheck: false }), error ? (0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger py-2", children: error }) : null, (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "submit", className: "btn btn-primary", disabled: busy || !text.trim(), children: busy ? "Checking…" : "Continue" }), (0, qr_scanner_1.canScanQr)() ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-outline-primary", disabled: busy, onClick: () => setScanning(true), children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-qr-code-scan me-1" }), "Scan QR"] })) : null] })] })) }));
114
+ }
@@ -0,0 +1,7 @@
1
+ import "./invites-screen";
2
+ export * from "./accept-invite";
3
+ export * from "./invite-capture";
4
+ export * from "./invite-share";
5
+ export * from "./invites-ipc";
6
+ export * from "./invites-screen";
7
+ export * from "./pending-invites-list";
@@ -14,6 +14,11 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- // Import to ensure route registration
18
- require("./join-group");
19
- __exportStar(require("./join-group"), exports);
17
+ // Import the screen so it registers its routes.
18
+ require("./invites-screen");
19
+ __exportStar(require("./accept-invite"), exports);
20
+ __exportStar(require("./invite-capture"), exports);
21
+ __exportStar(require("./invite-share"), exports);
22
+ __exportStar(require("./invites-ipc"), exports);
23
+ __exportStar(require("./invites-screen"), exports);
24
+ __exportStar(require("./pending-invites-list"), exports);
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Captures an inbound invite token from the page URL or a desktop `peers://` open-url event
3
+ * and holds it (tab-scoped) until the signed-in UI can route to the Accept screen.
4
+ *
5
+ * Tokens travel only in URL fragments and are stripped from the address bar immediately so
6
+ * startup route restoration never persists them.
7
+ */
8
+ /** sessionStorage key for a captured token awaiting the Accept screen. */
9
+ export declare const PENDING_INVITE_STORAGE_KEY = "peers.pendingInviteToken";
10
+ /** Route of the Accept-invite screen. */
11
+ export declare const ACCEPT_INVITE_PATH = "invites/accept";
12
+ /** Fires whenever a new token is captured, so a mounted app can navigate immediately. */
13
+ export declare const inviteCaptured: import("@peers-app/peers-sdk").Observable<string | undefined>;
14
+ /**
15
+ * Pull a token out of a URL fragment or deep link if one is present.
16
+ * Accepts `#invite=<token>`, `https://peers.app/i#<token>`, and `peers://i#<token>`.
17
+ */
18
+ export declare function inviteTokenFromUrl(url: string): string | undefined;
19
+ /** Store a token for the Accept screen and notify listeners. */
20
+ export declare function stashPendingInvite(token: string, storage?: Storage): void;
21
+ /** Read the stored token without consuming it. */
22
+ export declare function peekPendingInvite(storage?: Storage): string | undefined;
23
+ /** Read and clear the stored token. */
24
+ export declare function consumePendingInvite(storage?: Storage): string | undefined;
25
+ /**
26
+ * Capture `#invite=<token>` from the current location, strip it from the URL, and stash it.
27
+ * Safe to call repeatedly; returns the captured token when one was present.
28
+ */
29
+ export declare function captureInviteFromLocation(locationLike?: Pick<Location, "href" | "pathname" | "search">, historyLike?: Pick<History, "replaceState" | "state">, storage?: Storage): string | undefined;
30
+ /**
31
+ * Listen for `peers://` links forwarded by the Electron main process (`peers-open-url`).
32
+ * No-op outside Electron. Returns an unsubscribe function.
33
+ */
34
+ export declare function listenForOpenUrlInvites(): () => void;
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ /**
3
+ * Captures an inbound invite token from the page URL or a desktop `peers://` open-url event
4
+ * and holds it (tab-scoped) until the signed-in UI can route to the Accept screen.
5
+ *
6
+ * Tokens travel only in URL fragments and are stripped from the address bar immediately so
7
+ * startup route restoration never persists them.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.inviteCaptured = exports.ACCEPT_INVITE_PATH = exports.PENDING_INVITE_STORAGE_KEY = void 0;
11
+ exports.inviteTokenFromUrl = inviteTokenFromUrl;
12
+ exports.stashPendingInvite = stashPendingInvite;
13
+ exports.peekPendingInvite = peekPendingInvite;
14
+ exports.consumePendingInvite = consumePendingInvite;
15
+ exports.captureInviteFromLocation = captureInviteFromLocation;
16
+ exports.listenForOpenUrlInvites = listenForOpenUrlInvites;
17
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
18
+ /** sessionStorage key for a captured token awaiting the Accept screen. */
19
+ exports.PENDING_INVITE_STORAGE_KEY = "peers.pendingInviteToken";
20
+ /** Route of the Accept-invite screen. */
21
+ exports.ACCEPT_INVITE_PATH = "invites/accept";
22
+ /** Fires whenever a new token is captured, so a mounted app can navigate immediately. */
23
+ exports.inviteCaptured = (0, peers_sdk_1.observable)(undefined);
24
+ /**
25
+ * Pull a token out of a URL fragment or deep link if one is present.
26
+ * Accepts `#invite=<token>`, `https://peers.app/i#<token>`, and `peers://i#<token>`.
27
+ */
28
+ function inviteTokenFromUrl(url) {
29
+ const hash = url.indexOf("#");
30
+ if (hash < 0) {
31
+ return undefined;
32
+ }
33
+ const before = url.slice(0, hash);
34
+ const fragment = url.slice(hash + 1);
35
+ const isInviteFragment = fragment.startsWith("invite=");
36
+ const isLandingPage = /^peers:\/\/i\/?$/.test(before) || /^https?:\/\/[^/]+\/i\/?$/.test(before);
37
+ if (!isInviteFragment && !isLandingPage) {
38
+ return undefined;
39
+ }
40
+ try {
41
+ return (0, peers_sdk_1.extractInviteTokenText)(url);
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** Store a token for the Accept screen and notify listeners. */
48
+ function stashPendingInvite(token, storage = sessionStorage) {
49
+ storage.setItem(exports.PENDING_INVITE_STORAGE_KEY, token);
50
+ (0, exports.inviteCaptured)(token);
51
+ }
52
+ /** Read the stored token without consuming it. */
53
+ function peekPendingInvite(storage = sessionStorage) {
54
+ return storage.getItem(exports.PENDING_INVITE_STORAGE_KEY) ?? undefined;
55
+ }
56
+ /** Read and clear the stored token. */
57
+ function consumePendingInvite(storage = sessionStorage) {
58
+ const token = peekPendingInvite(storage);
59
+ storage.removeItem(exports.PENDING_INVITE_STORAGE_KEY);
60
+ return token;
61
+ }
62
+ /**
63
+ * Capture `#invite=<token>` from the current location, strip it from the URL, and stash it.
64
+ * Safe to call repeatedly; returns the captured token when one was present.
65
+ */
66
+ function captureInviteFromLocation(locationLike = window.location, historyLike = window.history, storage = sessionStorage) {
67
+ const token = inviteTokenFromUrl(locationLike.href);
68
+ if (!token) {
69
+ return undefined;
70
+ }
71
+ historyLike.replaceState(historyLike.state ?? null, "", `${locationLike.pathname}${locationLike.search}`);
72
+ stashPendingInvite(token, storage);
73
+ return token;
74
+ }
75
+ /**
76
+ * Listen for `peers://` links forwarded by the Electron main process (`peers-open-url`).
77
+ * No-op outside Electron. Returns an unsubscribe function.
78
+ */
79
+ function listenForOpenUrlInvites() {
80
+ const ipc = window.ipcRenderer;
81
+ if (!ipc) {
82
+ return () => { };
83
+ }
84
+ ipc.on("peers-open-url", (_event, url) => {
85
+ if (typeof url !== "string") {
86
+ return;
87
+ }
88
+ const token = inviteTokenFromUrl(url);
89
+ if (token) {
90
+ stashPendingInvite(token);
91
+ }
92
+ });
93
+ return () => ipc.removeAllListeners("peers-open-url");
94
+ }
@@ -0,0 +1,15 @@
1
+ import { type ICreatedInvite } from "@peers-app/peers-sdk";
2
+ /** Props for {@link InviteShare}. */
3
+ export interface InviteShareProps {
4
+ invite: ICreatedInvite;
5
+ /** Short line under the heading explaining what accepting grants. */
6
+ description?: string;
7
+ /** Body text used by the native share sheet; defaults to a generic line. */
8
+ shareText?: string;
9
+ onDone?: () => void;
10
+ }
11
+ /**
12
+ * Presents an issued invite as a QR code plus copyable link and token. The QR encodes the
13
+ * landing-page URL so phone cameras, the in-app scanner, and the desktop handler all work.
14
+ */
15
+ export declare function InviteShare(props: InviteShareProps): import("react/jsx-runtime").JSX.Element;