@peers-app/peers-ui 0.23.14 → 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
@@ -1,487 +0,0 @@
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.UserConnect = UserConnect;
7
- const jsx_runtime_1 = require("react/jsx-runtime");
8
- /**
9
- * Connect-to-user screen: QR invite, link share, manual code entry, and trust confirmation.
10
- */
11
- const peers_sdk_1 = require("@peers-app/peers-sdk");
12
- const react_1 = require("react");
13
- const react_qr_code_1 = __importDefault(require("react-qr-code"));
14
- const globals_1 = require("../../globals");
15
- const hooks_1 = require("../../hooks");
16
- const ui_loader_1 = require("../../ui-router/ui-loader");
17
- const user_connect_invite_1 = require("./user-connect-invite");
18
- const ERROR_COPY = {
19
- "invalid-code": {
20
- title: "That invite isn’t valid",
21
- body: "Check the link or code and try again, or create a new invite.",
22
- },
23
- "no-route": {
24
- title: "This device isn’t connected yet",
25
- body: "Connect this device to the internet or another Peers device, then try again.",
26
- },
27
- "not-found-or-expired": {
28
- title: "Invite not found or expired",
29
- body: "Ask the other person to create a new invite, or enter a fresh code.",
30
- },
31
- "remote-not-waiting": {
32
- title: "The other device stopped waiting",
33
- body: "Ask them to open Connect again and create a new invite.",
34
- },
35
- canceled: {
36
- title: "Connection canceled",
37
- body: "You can create a new invite or enter another code whenever you’re ready.",
38
- },
39
- "protocol-error": {
40
- title: "Something went wrong",
41
- body: "Try again in a moment. If it keeps failing, create a new invite.",
42
- },
43
- };
44
- function errorMessage(error) {
45
- const base = ERROR_COPY[error.code];
46
- if (error.code === "protocol-error" && error.detail) {
47
- return { title: base.title, body: base.body };
48
- }
49
- return base;
50
- }
51
- function formatCountdown(remainingMs) {
52
- const clamped = Math.max(0, remainingMs);
53
- const totalSeconds = Math.ceil(clamped / 1000);
54
- const minutes = Math.floor(totalSeconds / 60);
55
- const seconds = totalSeconds % 60;
56
- return `${minutes}:${seconds.toString().padStart(2, "0")}`;
57
- }
58
- function canShare() {
59
- return typeof navigator !== "undefined" && typeof navigator.share === "function";
60
- }
61
- /**
62
- * Polished Connect to user screen. Creating an invite is the default path;
63
- * inbound links open a review state before connecting.
64
- */
65
- function UserConnect() {
66
- const [phase, setPhase] = (0, react_1.useState)("offer");
67
- const [connectionCode, setConnectionCode] = (0, react_1.useState)("");
68
- const [inviteUrl, setInviteUrl] = (0, react_1.useState)("");
69
- const [expiresAt, setExpiresAt] = (0, react_1.useState)(null);
70
- const [remainingMs, setRemainingMs] = (0, react_1.useState)(peers_sdk_1.USER_CONNECT_TTL_MS);
71
- const [inputCode, setInputCode] = (0, react_1.useState)("");
72
- const [reviewCode, setReviewCode] = (0, react_1.useState)("");
73
- const [result, setResult] = (0, react_1.useState)(null);
74
- const [localError, setLocalError] = (0, react_1.useState)(null);
75
- const [feedback, setFeedback] = (0, react_1.useState)("");
76
- const [submitting, setSubmitting] = (0, react_1.useState)(false);
77
- const [hasRoute, setHasRoute] = (0, react_1.useState)(null);
78
- const [connectStatus] = (0, hooks_1.useObservable)(peers_sdk_1.userConnectStatus);
79
- const [answerCode] = (0, hooks_1.useObservable)(peers_sdk_1.userConnectCodeAnswer);
80
- const completedRef = (0, react_1.useRef)(false);
81
- const offerStartedRef = (0, react_1.useRef)(false);
82
- const answerWasActiveRef = (0, react_1.useRef)(false);
83
- const protocolActiveRef = (0, react_1.useRef)(false);
84
- const ceremonyGenerationRef = (0, react_1.useRef)(0);
85
- const clearCeremony = (0, react_1.useCallback)((options) => {
86
- ceremonyGenerationRef.current += 1;
87
- completedRef.current = false;
88
- answerWasActiveRef.current = false;
89
- protocolActiveRef.current = false;
90
- (0, peers_sdk_1.userConnectCodeOffer)("");
91
- (0, peers_sdk_1.userConnectCodeAnswer)("");
92
- (0, peers_sdk_1.userConnectStatus)(peers_sdk_1.USER_CONNECT_IDLE_STATUS);
93
- if (!options?.preservePendingIntent) {
94
- (0, user_connect_invite_1.clearUserConnectPendingIntent)();
95
- }
96
- }, []);
97
- const startOffer = (0, react_1.useCallback)(() => {
98
- completedRef.current = false;
99
- clearCeremony();
100
- const code = (0, peers_sdk_1.generateConnectionCode)();
101
- const formatted = (0, peers_sdk_1.formatConnectionCode)(code.code);
102
- setConnectionCode(formatted);
103
- setInviteUrl((0, user_connect_invite_1.buildUserConnectInviteUrl)(code.code));
104
- setExpiresAt(Date.now() + peers_sdk_1.USER_CONNECT_TTL_MS);
105
- setRemainingMs(peers_sdk_1.USER_CONNECT_TTL_MS);
106
- setLocalError(null);
107
- setResult(null);
108
- setFeedback("");
109
- setSubmitting(false);
110
- setPhase("offer");
111
- (0, peers_sdk_1.userConnectCodeOffer)(code.code);
112
- }, [clearCeremony]);
113
- const showError = (0, react_1.useCallback)((error) => {
114
- answerWasActiveRef.current = false;
115
- protocolActiveRef.current = false;
116
- setLocalError(error);
117
- setSubmitting(false);
118
- setPhase("error");
119
- }, []);
120
- const applyPendingInvite = (0, react_1.useCallback)(() => {
121
- const pending = (0, user_connect_invite_1.readUserConnectPendingIntent)();
122
- if (!pending.ok && pending.reason === "none") {
123
- return false;
124
- }
125
- clearCeremony({ preservePendingIntent: true });
126
- offerStartedRef.current = true;
127
- setConnectionCode("");
128
- setInviteUrl("");
129
- setExpiresAt(null);
130
- setLocalError(null);
131
- setResult(null);
132
- setFeedback("");
133
- setSubmitting(false);
134
- if (!pending.ok && pending.reason === "invalid") {
135
- setReviewCode("");
136
- setInputCode("");
137
- setLocalError({ code: "invalid-code" });
138
- setPhase("error");
139
- return true;
140
- }
141
- if (!pending.ok && pending.reason === "expired") {
142
- setReviewCode("");
143
- setInputCode("");
144
- setPhase("expired");
145
- return true;
146
- }
147
- if (pending.ok) {
148
- const formatted = (0, peers_sdk_1.formatConnectionCode)(pending.intent.code);
149
- setReviewCode(formatted);
150
- setInputCode(formatted);
151
- setPhase("review");
152
- }
153
- return true;
154
- }, [clearCeremony]);
155
- const refreshRouteStatus = (0, react_1.useCallback)(async () => {
156
- try {
157
- const devices = await peers_sdk_1.rpcServerCalls.listConnectedDevices();
158
- setHasRoute(devices.length > 0);
159
- }
160
- catch {
161
- // If the RPC is unavailable, do not block entry — route may appear shortly.
162
- setHasRoute(null);
163
- }
164
- }, []);
165
- // Capture inbound intent once on mount; otherwise auto-start an offer.
166
- (0, react_1.useEffect)(() => {
167
- if (applyPendingInvite()) {
168
- return;
169
- }
170
- if (!offerStartedRef.current) {
171
- offerStartedRef.current = true;
172
- startOffer();
173
- }
174
- }, [applyPendingInvite, startOffer]);
175
- // A second invite can arrive while this screen is already mounted.
176
- (0, react_1.useEffect)(() => {
177
- const captureInboundInvite = () => {
178
- const path = window.location.hash.slice(1).split("?")[0];
179
- if (path !== "contacts/connect") {
180
- return;
181
- }
182
- const captured = (0, user_connect_invite_1.captureUserConnectInviteFromLocation)();
183
- if (captured.ok || captured.reason !== "none") {
184
- applyPendingInvite();
185
- }
186
- };
187
- window.addEventListener("hashchange", captureInboundInvite);
188
- return () => window.removeEventListener("hashchange", captureInboundInvite);
189
- }, [applyPendingInvite]);
190
- (0, react_1.useEffect)(() => {
191
- void refreshRouteStatus();
192
- }, [refreshRouteStatus]);
193
- // Countdown while an offer is active.
194
- (0, react_1.useEffect)(() => {
195
- if (phase !== "offer" || expiresAt === null) {
196
- return;
197
- }
198
- const tick = () => {
199
- const remaining = expiresAt - Date.now();
200
- setRemainingMs(remaining);
201
- if (remaining <= 0) {
202
- clearCeremony();
203
- setPhase("expired");
204
- }
205
- };
206
- tick();
207
- const id = window.setInterval(tick, 1000);
208
- return () => window.clearInterval(id);
209
- }, [phase, expiresAt, clearCeremony]);
210
- const handleProtocolStatus = (0, react_1.useCallback)(async (status) => {
211
- if (!status || completedRef.current) {
212
- return;
213
- }
214
- if (status.kind === "connecting") {
215
- protocolActiveRef.current = true;
216
- return;
217
- }
218
- if (status.kind === "connected") {
219
- const generation = ceremonyGenerationRef.current;
220
- completedRef.current = true;
221
- try {
222
- const userContext = await (0, peers_sdk_1.getUserContext)();
223
- const me = await (0, peers_sdk_1.getMe)();
224
- const remoteUser = await (0, peers_sdk_1.Users)(userContext.userDataContext).get(status.remoteUserId);
225
- if (generation !== ceremonyGenerationRef.current) {
226
- return;
227
- }
228
- if (!remoteUser) {
229
- showError({ code: "protocol-error", detail: "Could not find connected user" });
230
- return;
231
- }
232
- const remoteDevice = await (0, peers_sdk_1.Devices)(userContext.userDataContext).get(status.remoteDeviceId);
233
- if (generation !== ceremonyGenerationRef.current) {
234
- return;
235
- }
236
- if (!remoteDevice) {
237
- showError({
238
- code: "protocol-error",
239
- detail: "Could not find connected device",
240
- });
241
- return;
242
- }
243
- const myInfo = {
244
- userId: me.userId,
245
- publicKey: me.publicKey,
246
- publicBoxKey: me.publicBoxKey,
247
- deviceId: userContext.deviceId(),
248
- };
249
- const remoteInfo = {
250
- userId: remoteUser.userId,
251
- publicKey: remoteUser.publicKey,
252
- publicBoxKey: remoteUser.publicBoxKey,
253
- deviceId: remoteDevice.deviceId,
254
- };
255
- setResult({
256
- remoteUser,
257
- confirmationHash: (0, peers_sdk_1.generateConfirmationHash)(myInfo, remoteInfo),
258
- });
259
- (0, user_connect_invite_1.clearUserConnectPendingIntent)();
260
- (0, peers_sdk_1.userConnectCodeOffer)("");
261
- (0, peers_sdk_1.userConnectCodeAnswer)("");
262
- setSubmitting(false);
263
- setPhase("success");
264
- }
265
- catch (err) {
266
- if (generation !== ceremonyGenerationRef.current) {
267
- return;
268
- }
269
- showError({
270
- code: "protocol-error",
271
- detail: err instanceof Error ? err.message : String(err),
272
- });
273
- }
274
- return;
275
- }
276
- if (status.kind === "error") {
277
- showError({ code: status.code, detail: status.detail });
278
- }
279
- }, [showError]);
280
- (0, react_1.useEffect)(() => {
281
- void handleProtocolStatus(connectStatus);
282
- }, [connectStatus, handleProtocolStatus]);
283
- (0, react_1.useEffect)(() => {
284
- if (phase !== "connecting" ||
285
- !connectStatus ||
286
- connectStatus.kind !== "idle" ||
287
- (answerCode && !protocolActiveRef.current) ||
288
- (!answerCode && !answerWasActiveRef.current)) {
289
- return;
290
- }
291
- // Terminal handlers publish error/connected immediately after clearing the
292
- // answer. Defer idle recovery one turn so those statuses win.
293
- const timeout = window.setTimeout(() => {
294
- const latestStatus = (0, peers_sdk_1.userConnectStatus)();
295
- const latestAnswer = (0, peers_sdk_1.userConnectCodeAnswer)();
296
- if (latestStatus.kind !== "idle") {
297
- return;
298
- }
299
- if (latestAnswer) {
300
- if (protocolActiveRef.current) {
301
- clearCeremony();
302
- showError({ code: "canceled" });
303
- }
304
- return;
305
- }
306
- if (answerWasActiveRef.current) {
307
- clearCeremony();
308
- setSubmitting(false);
309
- setPhase("expired");
310
- }
311
- }, 0);
312
- return () => window.clearTimeout(timeout);
313
- }, [answerCode, clearCeremony, connectStatus, phase, showError]);
314
- (0, react_1.useEffect)(() => {
315
- return () => {
316
- // Leaving the screen cancels an in-flight offer/answer.
317
- (0, peers_sdk_1.userConnectCodeOffer)("");
318
- (0, peers_sdk_1.userConnectCodeAnswer)("");
319
- (0, peers_sdk_1.userConnectStatus)(peers_sdk_1.USER_CONNECT_IDLE_STATUS);
320
- const path = window.location.hash.slice(1).split("?")[0];
321
- if (path !== "contacts/connect") {
322
- (0, user_connect_invite_1.clearUserConnectPendingIntent)();
323
- }
324
- };
325
- }, []);
326
- const handleConfirmReview = (0, react_1.useCallback)(async () => {
327
- if (submitting)
328
- return;
329
- const pending = (0, user_connect_invite_1.readUserConnectPendingIntent)();
330
- if (!pending.ok) {
331
- if (pending.reason === "invalid" || pending.reason === "expired") {
332
- applyPendingInvite();
333
- }
334
- else {
335
- showError({ code: "canceled" });
336
- }
337
- return;
338
- }
339
- const normalized = (0, user_connect_invite_1.normalizeUserConnectCode)(reviewCode);
340
- if (!normalized || normalized !== pending.intent.code) {
341
- showError({ code: "invalid-code" });
342
- return;
343
- }
344
- ceremonyGenerationRef.current += 1;
345
- completedRef.current = false;
346
- answerWasActiveRef.current = true;
347
- protocolActiveRef.current = false;
348
- setSubmitting(true);
349
- setPhase("connecting");
350
- setLocalError(null);
351
- (0, user_connect_invite_1.clearUserConnectPendingIntent)();
352
- (0, peers_sdk_1.userConnectStatus)(peers_sdk_1.USER_CONNECT_IDLE_STATUS);
353
- (0, peers_sdk_1.userConnectCodeAnswer)(normalized);
354
- }, [applyPendingInvite, reviewCode, showError, submitting]);
355
- const handleManualConnect = (0, react_1.useCallback)(async () => {
356
- if (submitting)
357
- return;
358
- const normalized = (0, user_connect_invite_1.normalizeUserConnectCode)(inputCode);
359
- if (!normalized) {
360
- setLocalError({ code: "invalid-code" });
361
- return;
362
- }
363
- ceremonyGenerationRef.current += 1;
364
- completedRef.current = false;
365
- answerWasActiveRef.current = true;
366
- protocolActiveRef.current = false;
367
- setSubmitting(true);
368
- setPhase("connecting");
369
- setLocalError(null);
370
- (0, user_connect_invite_1.clearUserConnectPendingIntent)();
371
- (0, peers_sdk_1.userConnectStatus)(peers_sdk_1.USER_CONNECT_IDLE_STATUS);
372
- (0, peers_sdk_1.userConnectCodeAnswer)(normalized);
373
- }, [inputCode, submitting]);
374
- const handleCancel = (0, react_1.useCallback)(() => {
375
- clearCeremony();
376
- setSubmitting(false);
377
- setLocalError(null);
378
- setResult(null);
379
- (0, globals_1.mainContentPath)("contacts");
380
- }, [clearCeremony]);
381
- const handleEnterManual = (0, react_1.useCallback)(() => {
382
- clearCeremony();
383
- setSubmitting(false);
384
- setLocalError(null);
385
- setInputCode("");
386
- setPhase("manual");
387
- }, [clearCeremony]);
388
- const handleCopyLink = (0, react_1.useCallback)(async () => {
389
- if (!inviteUrl)
390
- return;
391
- const ok = await (0, peers_sdk_1.copyToClipboard)(inviteUrl);
392
- setFeedback(ok ? "Link copied" : "Could not copy link");
393
- window.setTimeout(() => setFeedback(""), 2000);
394
- }, [inviteUrl]);
395
- const handleShareLink = (0, react_1.useCallback)(async () => {
396
- if (!inviteUrl || !canShare()) {
397
- await handleCopyLink();
398
- return;
399
- }
400
- try {
401
- await navigator.share({
402
- title: "Connect on Peers",
403
- text: (0, user_connect_invite_1.buildUserConnectShareText)(inviteUrl),
404
- url: inviteUrl,
405
- });
406
- setFeedback("Share sheet opened");
407
- window.setTimeout(() => setFeedback(""), 2000);
408
- }
409
- catch (err) {
410
- // User cancel is neutral.
411
- if (err instanceof DOMException && err.name === "AbortError") {
412
- return;
413
- }
414
- await handleCopyLink();
415
- }
416
- }, [handleCopyLink, inviteUrl]);
417
- const handleTrustAndView = (0, react_1.useCallback)(async () => {
418
- if (!result)
419
- return;
420
- try {
421
- const userContext = await (0, peers_sdk_1.getUserContext)();
422
- await (0, peers_sdk_1.setUserTrustLevel)(result.remoteUser.userId, peers_sdk_1.TrustLevel.Trusted, userContext.userDataContext);
423
- clearCeremony();
424
- (0, globals_1.mainContentPath)(`contacts/${result.remoteUser.userId}`);
425
- }
426
- catch (err) {
427
- showError({
428
- code: "protocol-error",
429
- detail: err instanceof Error ? err.message : String(err),
430
- });
431
- }
432
- }, [clearCeremony, result, showError]);
433
- const handleViewWithoutTrust = (0, react_1.useCallback)(() => {
434
- if (!result)
435
- return;
436
- clearCeremony();
437
- (0, globals_1.mainContentPath)(`contacts/${result.remoteUser.userId}`);
438
- }, [clearCeremony, result]);
439
- const handleCodeInput = (value) => {
440
- const cleaned = value.toUpperCase().replace(/[^0-9A-Z]/g, "");
441
- let formatted = "";
442
- for (let i = 0; i < cleaned.length && i < 12; i++) {
443
- if (i === 4 || i === 8)
444
- formatted += "-";
445
- formatted += cleaned[i];
446
- }
447
- setInputCode(formatted);
448
- setLocalError(null);
449
- };
450
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-3", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-center mb-3", children: [(0, jsx_runtime_1.jsxs)("h4", { className: "mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-plus-fill me-2", "aria-hidden": "true" }), "Connect to user"] }), phase !== "offer" && phase !== "success" && ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-outline-secondary btn-sm", onClick: handleCancel, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-arrow-left me-1", "aria-hidden": "true" }), "Back"] }))] }), hasRoute === false && phase !== "success" && ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-warning", role: "status", children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex justify-content-between align-items-start gap-2 flex-wrap", children: [(0, jsx_runtime_1.jsx)("div", { children: "This device isn\u2019t connected to the internet or another Peers device yet. You can still prepare an invite; connection will work once a route appears." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-sm btn-outline-warning", onClick: () => void refreshRouteStatus(), children: "Check again" })] }) })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", className: "visually-hidden", children: feedback }), phase === "offer" && ((0, jsx_runtime_1.jsx)(OfferPanel, { connectionCode: connectionCode, inviteUrl: inviteUrl, remainingMs: remainingMs, feedback: feedback, onShare: () => void handleShareLink(), onCopy: () => void handleCopyLink(), onEnterManual: handleEnterManual, onCancel: handleCancel })), phase === "manual" && ((0, jsx_runtime_1.jsx)(ManualEntryPanel, { inputCode: inputCode, localError: localError, submitting: submitting, onChange: handleCodeInput, onConnect: () => void handleManualConnect() })), phase === "review" && ((0, jsx_runtime_1.jsx)(ReviewPanel, { reviewCode: reviewCode, submitting: submitting, onConfirm: () => void handleConfirmReview(), onEnterManual: handleEnterManual })), phase === "connecting" && (0, jsx_runtime_1.jsx)(ConnectingPanel, {}), phase === "success" && result && ((0, jsx_runtime_1.jsx)(SuccessPanel, { result: result, onTrust: () => void handleTrustAndView(), onViewWithoutTrust: handleViewWithoutTrust })), phase === "error" && localError && ((0, jsx_runtime_1.jsx)(ErrorPanel, { error: localError, onNewInvite: startOffer, onEnterCode: handleEnterManual })), phase === "expired" && ((0, jsx_runtime_1.jsx)(ExpiredPanel, { onNewInvite: startOffer, onEnterCode: handleEnterManual }))] }));
451
- }
452
- function OfferPanel(props) {
453
- const shareAvailable = canShare();
454
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 440, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-3", children: "Ask the other person to scan this with their camera" }), (0, jsx_runtime_1.jsx)("div", { className: "d-inline-block p-3 mb-3 rounded", style: { background: "#ffffff" }, "aria-label": "Connection QR code", children: props.inviteUrl ? ((0, jsx_runtime_1.jsx)(react_qr_code_1.default, { value: props.inviteUrl, size: 220, bgColor: "#FFFFFF", fgColor: "#000000", style: { height: "auto", maxWidth: "100%", width: "min(70vw, 220px)" }, viewBox: "0 0 256 256" })) : ((0, jsx_runtime_1.jsx)("div", { className: "spinner-border text-primary", role: "status", "aria-label": "Preparing invite" })) }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm text-primary me-2", role: "status" }), (0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "Waiting for connection\u2026" })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-muted small mb-3", "aria-live": "polite", children: ["Expires in ", formatCountdown(props.remainingMs)] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-2 mb-3", children: [shareAvailable ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-primary", onClick: props.onShare, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-share me-2", "aria-hidden": "true" }), "Share link"] })) : null, (0, jsx_runtime_1.jsxs)("button", { type: "button", className: "btn btn-outline-primary", onClick: props.onCopy, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-clipboard me-2", "aria-hidden": "true" }), "Copy link"] }), props.feedback ? (0, jsx_runtime_1.jsx)("small", { className: "text-success", children: props.feedback }) : null] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small text-muted mb-1", htmlFor: "user-connect-fallback-code", children: "Or share this code" }), (0, jsx_runtime_1.jsx)("input", { id: "user-connect-fallback-code", type: "text", className: "form-control form-control-lg text-center font-monospace", value: props.connectionCode, readOnly: true, "aria-label": "Connection code" })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center flex-wrap", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-link", onClick: props.onEnterManual, children: "Enter a code instead" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onCancel, children: "Cancel" })] })] }));
455
- }
456
- function ManualEntryPanel(props) {
457
- const ready = props.inputCode.replace(/[^0-9A-Z]/gi, "").length === 12;
458
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 400, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-3", children: "Enter the connection code shared with you" }), (0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-lg text-center font-monospace mb-3", placeholder: "XXXX-YYYY-ZZZZ", value: props.inputCode, onChange: (e) => props.onChange(e.target.value), maxLength: 14, "aria-label": "Connection code", autoComplete: "off" }), props.localError ? ((0, jsx_runtime_1.jsx)("div", { className: "alert alert-danger", children: errorMessage(props.localError).body })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary btn-lg", onClick: props.onConnect, disabled: props.submitting || !ready, children: props.submitting ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2", role: "status" }), "Connecting\u2026"] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-link-45deg me-2", "aria-hidden": "true" }), "Connect"] })) })] }));
459
- }
460
- function ReviewPanel(props) {
461
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 400, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-2", children: "You opened a connection invite" }), (0, jsx_runtime_1.jsx)("p", { className: "font-monospace fs-4 mb-3", children: props.reviewCode }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted small mb-4", children: "Both of you need Peers open. Confirm to connect with the person who shared this invite." }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary btn-lg mb-2", onClick: props.onConfirm, disabled: props.submitting, children: props.submitting ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2", role: "status" }), "Connecting\u2026"] })) : ("Connect") }), (0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-link", onClick: props.onEnterManual, children: "Enter a different code" }) })] }));
462
- }
463
- function ConnectingPanel() {
464
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center py-5", children: [(0, jsx_runtime_1.jsx)("div", { className: "spinner-border text-primary mb-3", role: "status", "aria-label": "Connecting" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-0", children: "Connecting\u2026" })] }));
465
- }
466
- function SuccessPanel(props) {
467
- const [showDetails, setShowDetails] = (0, react_1.useState)(false);
468
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 420, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-check-circle-fill text-success display-3 mb-3", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("h5", { className: "mb-3", children: "Connected" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-2", children: "Verify with the other person that you both see:" }), (0, jsx_runtime_1.jsx)("h3", { className: "font-monospace text-primary mb-3", style: { letterSpacing: "0.15em" }, children: props.result.confirmationHash }), (0, jsx_runtime_1.jsxs)("p", { className: "mb-1", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-muted", children: "Name: " }), props.result.remoteUser.name || "Unnamed"] }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-link btn-sm mb-3", onClick: () => setShowDetails((v) => !v), "aria-expanded": showDetails, children: showDetails ? "Hide details" : "Show details" }), showDetails ? ((0, jsx_runtime_1.jsx)("p", { className: "font-monospace small text-muted mb-3", children: props.result.remoteUser.userId })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "d-grid gap-2", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: props.onTrust, children: "Trust and view contact" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onViewWithoutTrust, children: "View without trusting" })] })] }));
469
- }
470
- function ErrorPanel(props) {
471
- const copy = errorMessage(props.error);
472
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 420, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-x-circle-fill text-danger display-3 mb-3", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("h5", { className: "mb-2", children: copy.title }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-4", children: copy.body }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center flex-wrap", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: props.onNewInvite, children: "Create a new invite" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onEnterCode, children: "Enter another code" })] })] }));
473
- }
474
- function ExpiredPanel(props) {
475
- return ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", style: { maxWidth: 420, margin: "0 auto" }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-hourglass-bottom text-warning display-3 mb-3", "aria-hidden": "true" }), (0, jsx_runtime_1.jsx)("h5", { className: "mb-2", children: "Invite expired" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-4", children: "Create a new invite or enter a fresh code from the other person." }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2 justify-content-center flex-wrap", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-primary", onClick: props.onNewInvite, children: "Create a new invite" }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "btn btn-outline-secondary", onClick: props.onEnterCode, children: "Enter another code" })] })] }));
476
- }
477
- (0, ui_loader_1.registerInternalPeersUI)({
478
- peersUIId: "000user00connect0screen01",
479
- component: UserConnect,
480
- routes: [
481
- {
482
- isMatch: (_props, context) => context.path === "contacts/connect",
483
- uiCategory: "screen",
484
- priority: 3,
485
- },
486
- ],
487
- });
@@ -1 +0,0 @@
1
- export {};