@peers-app/peers-ui 0.23.13 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/biome.json +2 -2
- package/dist/components/qr-scanner.d.ts +18 -0
- package/dist/components/qr-scanner.js +165 -0
- package/dist/components/router.d.ts +1 -1
- package/dist/components/router.js +1 -1
- package/dist/root-layout/root-layout-host.js +18 -19
- package/dist/root-layout/root-layout-host.test.js +14 -9
- package/dist/screens/account/account-screen.js +52 -1
- package/dist/screens/contacts/contact-list.js +28 -1
- package/dist/screens/contacts/index.d.ts +0 -2
- package/dist/screens/contacts/index.js +0 -2
- package/dist/screens/device-pairing/device-pairing.d.ts +2 -4
- package/dist/screens/device-pairing/device-pairing.js +127 -128
- package/dist/screens/device-pairing/device-pairing.test.js +51 -15
- package/dist/screens/device-pairing/index.d.ts +0 -1
- package/dist/screens/device-pairing/index.js +0 -1
- package/dist/screens/groups/group-details.js +2 -6
- package/dist/screens/groups/group-invite-panel.d.ts +14 -0
- package/dist/screens/groups/group-invite-panel.js +125 -0
- package/dist/screens/groups/group-members.js +9 -103
- package/dist/screens/invites/accept-invite.d.ts +11 -0
- package/dist/screens/invites/accept-invite.js +114 -0
- package/dist/screens/invites/index.d.ts +7 -0
- package/dist/screens/{join-group → invites}/index.js +8 -3
- package/dist/screens/invites/invite-capture.d.ts +34 -0
- package/dist/screens/invites/invite-capture.js +94 -0
- package/dist/screens/invites/invite-share.d.ts +15 -0
- package/dist/screens/invites/invite-share.js +40 -0
- package/dist/screens/invites/invites-ipc.d.ts +10 -0
- package/dist/screens/invites/invites-ipc.js +61 -0
- package/dist/screens/invites/invites-screen.d.ts +9 -0
- package/dist/screens/invites/invites-screen.js +36 -0
- package/dist/screens/invites/pending-invites-list.d.ts +29 -0
- package/dist/screens/invites/pending-invites-list.js +156 -0
- package/dist/screens/settings/settings-page.js +1 -1
- package/dist/screens/setup-user.js +4 -18
- package/dist/system-apps/index.d.ts +1 -1
- package/dist/system-apps/index.js +5 -5
- package/dist/system-apps/{join-group.app.d.ts → invites.app.d.ts} +1 -1
- package/dist/system-apps/invites.app.js +9 -0
- package/package.json +4 -3
- package/src/components/qr-scanner.tsx +180 -0
- package/src/components/router.tsx +1 -1
- package/src/root-layout/root-layout-host.test.tsx +18 -13
- package/src/root-layout/root-layout-host.tsx +23 -21
- package/src/screens/account/account-screen.tsx +89 -2
- package/src/screens/contacts/contact-list.tsx +64 -4
- package/src/screens/contacts/index.ts +0 -2
- package/src/screens/device-pairing/device-pairing.test.tsx +61 -16
- package/src/screens/device-pairing/device-pairing.tsx +283 -208
- package/src/screens/device-pairing/index.ts +0 -1
- package/src/screens/groups/group-details.tsx +1 -41
- package/src/screens/groups/group-invite-panel.tsx +316 -0
- package/src/screens/groups/group-members.tsx +15 -177
- package/src/screens/invites/accept-invite.tsx +271 -0
- package/src/screens/invites/index.ts +9 -0
- package/src/screens/invites/invite-capture.ts +107 -0
- package/src/screens/invites/invite-share.tsx +104 -0
- package/src/screens/invites/invites-ipc.ts +74 -0
- package/src/screens/invites/invites-screen.tsx +78 -0
- package/src/screens/invites/pending-invites-list.tsx +224 -0
- package/src/screens/settings/settings-page.tsx +2 -2
- package/src/screens/setup-user.tsx +29 -47
- package/src/system-apps/index.ts +3 -3
- package/src/system-apps/invites.app.ts +8 -0
- package/dist/screens/contacts/user-connect-invite.d.ts +0 -118
- package/dist/screens/contacts/user-connect-invite.js +0 -264
- package/dist/screens/contacts/user-connect-invite.test.d.ts +0 -1
- package/dist/screens/contacts/user-connect-invite.test.js +0 -215
- package/dist/screens/contacts/user-connect.d.ts +0 -8
- package/dist/screens/contacts/user-connect.js +0 -487
- package/dist/screens/contacts/user-connect.test.d.ts +0 -1
- package/dist/screens/contacts/user-connect.test.js +0 -291
- package/dist/screens/device-pairing/device-pairing-invite.d.ts +0 -10
- package/dist/screens/device-pairing/device-pairing-invite.js +0 -50
- package/dist/screens/device-pairing/device-pairing-invite.test.d.ts +0 -1
- package/dist/screens/device-pairing/device-pairing-invite.test.js +0 -30
- package/dist/screens/groups/group-invite-listener.d.ts +0 -14
- package/dist/screens/groups/group-invite-listener.js +0 -93
- package/dist/screens/join-group/index.d.ts +0 -2
- package/dist/screens/join-group/join-group.d.ts +0 -12
- package/dist/screens/join-group/join-group.js +0 -102
- package/dist/system-apps/join-group.app.js +0 -9
- package/src/screens/contacts/user-connect-invite.test.ts +0 -256
- package/src/screens/contacts/user-connect-invite.ts +0 -332
- package/src/screens/contacts/user-connect.test.tsx +0 -382
- package/src/screens/contacts/user-connect.tsx +0 -883
- package/src/screens/device-pairing/device-pairing-invite.test.ts +0 -50
- package/src/screens/device-pairing/device-pairing-invite.ts +0 -55
- package/src/screens/groups/group-invite-listener.tsx +0 -282
- package/src/screens/join-group/index.ts +0 -4
- package/src/screens/join-group/join-group.tsx +0 -296
- package/src/system-apps/join-group.app.ts +0 -8
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
4
|
-
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
5
|
-
const react_1 = require("@testing-library/react");
|
|
6
|
-
const globals_1 = require("../../globals");
|
|
7
|
-
const user_connect_1 = require("./user-connect");
|
|
8
|
-
const user_connect_invite_1 = require("./user-connect-invite");
|
|
9
|
-
jest.mock("../../globals", () => ({
|
|
10
|
-
mainContentPath: jest.fn(),
|
|
11
|
-
}));
|
|
12
|
-
jest.mock("../../ui-router/ui-loader", () => ({
|
|
13
|
-
registerInternalPeersUI: jest.fn(),
|
|
14
|
-
}));
|
|
15
|
-
jest.mock("../../hooks", () => {
|
|
16
|
-
const React = jest.requireActual("react");
|
|
17
|
-
return {
|
|
18
|
-
useObservable: (observable) => {
|
|
19
|
-
const [value, setValue] = React.useState(observable());
|
|
20
|
-
React.useEffect(() => {
|
|
21
|
-
const subscription = observable.subscribe(() => setValue(observable()));
|
|
22
|
-
return () => subscription.dispose();
|
|
23
|
-
}, [observable]);
|
|
24
|
-
return [value, observable];
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
});
|
|
28
|
-
jest.mock("@peers-app/peers-sdk", () => {
|
|
29
|
-
const actual = jest.requireActual("@peers-app/peers-sdk");
|
|
30
|
-
const createObservable = (initial) => {
|
|
31
|
-
let value = initial;
|
|
32
|
-
const listeners = new Set();
|
|
33
|
-
const observable = jest.fn((next) => {
|
|
34
|
-
if (next !== undefined) {
|
|
35
|
-
value = next;
|
|
36
|
-
for (const listener of listeners)
|
|
37
|
-
listener();
|
|
38
|
-
}
|
|
39
|
-
return value;
|
|
40
|
-
});
|
|
41
|
-
observable.subscribe = (listener) => {
|
|
42
|
-
listeners.add(listener);
|
|
43
|
-
return { dispose: () => listeners.delete(listener) };
|
|
44
|
-
};
|
|
45
|
-
observable.loadingPromise = Promise.resolve();
|
|
46
|
-
observable.reset = (next) => {
|
|
47
|
-
value = next;
|
|
48
|
-
for (const listener of listeners)
|
|
49
|
-
listener();
|
|
50
|
-
};
|
|
51
|
-
return observable;
|
|
52
|
-
};
|
|
53
|
-
const idle = { kind: "idle" };
|
|
54
|
-
const status = createObservable(idle);
|
|
55
|
-
const offer = createObservable("");
|
|
56
|
-
const answer = createObservable("");
|
|
57
|
-
const remoteUser = {
|
|
58
|
-
userId: "remote-user",
|
|
59
|
-
name: "Ada",
|
|
60
|
-
publicKey: "remote-public-key",
|
|
61
|
-
publicBoxKey: "remote-public-box-key",
|
|
62
|
-
};
|
|
63
|
-
return {
|
|
64
|
-
...actual,
|
|
65
|
-
copyToClipboard: jest.fn(async () => true),
|
|
66
|
-
Devices: jest.fn(() => ({
|
|
67
|
-
get: jest.fn(async () => ({ deviceId: "remote-device", userId: remoteUser.userId })),
|
|
68
|
-
})),
|
|
69
|
-
generateConnectionCode: jest.fn(() => ({
|
|
70
|
-
code: "ABCD1234EFGH",
|
|
71
|
-
alias: "ABCD",
|
|
72
|
-
secret: "1234EFGH",
|
|
73
|
-
})),
|
|
74
|
-
getMe: jest.fn(async () => ({
|
|
75
|
-
userId: "local-user",
|
|
76
|
-
name: "Grace",
|
|
77
|
-
publicKey: "local-public-key",
|
|
78
|
-
publicBoxKey: "local-public-box-key",
|
|
79
|
-
})),
|
|
80
|
-
getUserContext: jest.fn(async () => ({
|
|
81
|
-
userDataContext: {},
|
|
82
|
-
deviceId: () => "local-device",
|
|
83
|
-
})),
|
|
84
|
-
rpcServerCalls: {
|
|
85
|
-
...actual.rpcServerCalls,
|
|
86
|
-
listConnectedDevices: jest.fn(async () => [
|
|
87
|
-
{
|
|
88
|
-
deviceId: "route-device",
|
|
89
|
-
userId: "route-user",
|
|
90
|
-
connectionId: "route-connection",
|
|
91
|
-
trustLevel: 0,
|
|
92
|
-
verified: true,
|
|
93
|
-
},
|
|
94
|
-
]),
|
|
95
|
-
},
|
|
96
|
-
setUserTrustLevel: jest.fn(async () => undefined),
|
|
97
|
-
Users: jest.fn(() => ({
|
|
98
|
-
get: jest.fn(async () => remoteUser),
|
|
99
|
-
})),
|
|
100
|
-
userConnectCodeAnswer: answer,
|
|
101
|
-
userConnectCodeOffer: offer,
|
|
102
|
-
userConnectStatus: status,
|
|
103
|
-
};
|
|
104
|
-
});
|
|
105
|
-
const sdk = jest.requireMock("@peers-app/peers-sdk");
|
|
106
|
-
const validCode = "ABCD1234EFGH";
|
|
107
|
-
function resetMocks() {
|
|
108
|
-
sessionStorage.clear();
|
|
109
|
-
window.history.replaceState(null, "", "/");
|
|
110
|
-
sdk.userConnectStatus.reset({ kind: "idle" });
|
|
111
|
-
sdk.userConnectCodeOffer.reset("");
|
|
112
|
-
sdk.userConnectCodeAnswer.reset("");
|
|
113
|
-
jest.clearAllMocks();
|
|
114
|
-
}
|
|
115
|
-
describe("UserConnect", () => {
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
resetMocks();
|
|
118
|
-
});
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
jest.useRealTimers();
|
|
121
|
-
Reflect.deleteProperty(navigator, "share");
|
|
122
|
-
});
|
|
123
|
-
it("starts with a QR invite and readable fallback code", async () => {
|
|
124
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
125
|
-
expect(await react_1.screen.findByText("Ask the other person to scan this with their camera")).toBeVisible();
|
|
126
|
-
expect(react_1.screen.getByLabelText("Connection QR code")).toBeVisible();
|
|
127
|
-
expect(react_1.screen.getByLabelText("Connection code")).toHaveValue("ABCD-1234-EFGH");
|
|
128
|
-
expect(sdk.userConnectCodeOffer).toHaveBeenCalledWith(validCode);
|
|
129
|
-
});
|
|
130
|
-
it("opens a captured inbound invite in review without auto-submitting", async () => {
|
|
131
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
132
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
133
|
-
expect(await react_1.screen.findByText("You opened a connection invite")).toBeVisible();
|
|
134
|
-
expect(react_1.screen.getByText("ABCD-1234-EFGH")).toBeVisible();
|
|
135
|
-
expect(sdk.userConnectCodeAnswer).not.toHaveBeenCalledWith(validCode);
|
|
136
|
-
});
|
|
137
|
-
it("keeps an unconfirmed invite available across a screen remount", async () => {
|
|
138
|
-
window.history.replaceState(null, "", "/#contacts/connect");
|
|
139
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
140
|
-
const firstRender = (0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
141
|
-
expect(await react_1.screen.findByText("You opened a connection invite")).toBeVisible();
|
|
142
|
-
firstRender.unmount();
|
|
143
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
144
|
-
expect(await react_1.screen.findByText("You opened a connection invite")).toBeVisible();
|
|
145
|
-
expect(react_1.screen.getByText("ABCD-1234-EFGH")).toBeVisible();
|
|
146
|
-
});
|
|
147
|
-
it("clears an unconfirmed invite when navigating away", async () => {
|
|
148
|
-
window.history.replaceState(null, "", "/#contacts/connect");
|
|
149
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
150
|
-
const rendered = (0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
151
|
-
expect(await react_1.screen.findByText("You opened a connection invite")).toBeVisible();
|
|
152
|
-
window.history.replaceState(null, "", "/#contacts");
|
|
153
|
-
rendered.unmount();
|
|
154
|
-
expect((0, user_connect_invite_1.readUserConnectPendingIntent)()).toEqual({ ok: false, reason: "none" });
|
|
155
|
-
});
|
|
156
|
-
it("captures an invite opened while the screen is already mounted", async () => {
|
|
157
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
158
|
-
await react_1.screen.findByText("Ask the other person to scan this with their camera");
|
|
159
|
-
window.history.replaceState(null, "", `/?app#contacts/connect?code=${encodeURIComponent("ABCD-1234-EFGH")}`);
|
|
160
|
-
(0, react_1.fireEvent)(window, new HashChangeEvent("hashchange"));
|
|
161
|
-
expect(await react_1.screen.findByText("You opened a connection invite")).toBeVisible();
|
|
162
|
-
expect(react_1.screen.getByText("ABCD-1234-EFGH")).toBeVisible();
|
|
163
|
-
expect(window.location.hash).toBe("#contacts/connect");
|
|
164
|
-
expect(sdk.userConnectCodeOffer).toHaveBeenCalledWith("");
|
|
165
|
-
expect(sdk.userConnectCodeAnswer).not.toHaveBeenCalledWith(validCode);
|
|
166
|
-
});
|
|
167
|
-
it("copies the complete invite link", async () => {
|
|
168
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
169
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Copy link" }));
|
|
170
|
-
await (0, react_1.waitFor)(() => {
|
|
171
|
-
expect(sdk.copyToClipboard).toHaveBeenCalledWith((0, user_connect_invite_1.buildUserConnectInviteUrl)(validCode));
|
|
172
|
-
});
|
|
173
|
-
expect((await react_1.screen.findAllByText("Link copied")).length).toBeGreaterThan(0);
|
|
174
|
-
});
|
|
175
|
-
it("shares the complete invite through the Web Share API", async () => {
|
|
176
|
-
const share = jest.fn(async () => undefined);
|
|
177
|
-
Object.defineProperty(navigator, "share", {
|
|
178
|
-
configurable: true,
|
|
179
|
-
value: share,
|
|
180
|
-
});
|
|
181
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
182
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Share link" }));
|
|
183
|
-
await (0, react_1.waitFor)(() => {
|
|
184
|
-
expect(share).toHaveBeenCalledWith(expect.objectContaining({
|
|
185
|
-
title: "Connect on Peers",
|
|
186
|
-
url: (0, user_connect_invite_1.buildUserConnectInviteUrl)(validCode),
|
|
187
|
-
text: expect.stringContaining("expires in 10 minutes"),
|
|
188
|
-
}));
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
it("stops waiting when the invite expires", async () => {
|
|
192
|
-
jest.useFakeTimers();
|
|
193
|
-
jest.setSystemTime(new Date("2026-07-16T12:00:00Z"));
|
|
194
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
195
|
-
await (0, react_1.act)(async () => {
|
|
196
|
-
jest.advanceTimersByTime(600_000);
|
|
197
|
-
});
|
|
198
|
-
expect(react_1.screen.getByText("Invite expired")).toBeVisible();
|
|
199
|
-
expect(sdk.userConnectCodeOffer).toHaveBeenCalledWith("");
|
|
200
|
-
});
|
|
201
|
-
it("submits an inbound invite only once on a double click", async () => {
|
|
202
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
203
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
204
|
-
const connect = await react_1.screen.findByRole("button", { name: "Connect" });
|
|
205
|
-
react_1.fireEvent.click(connect);
|
|
206
|
-
react_1.fireEvent.click(connect);
|
|
207
|
-
expect(sdk.userConnectCodeAnswer.mock.calls.filter(([code]) => code === validCode)).toHaveLength(1);
|
|
208
|
-
expect((0, user_connect_invite_1.readUserConnectPendingIntent)()).toEqual({ ok: false, reason: "none" });
|
|
209
|
-
});
|
|
210
|
-
it("rechecks invite expiry before confirming review", async () => {
|
|
211
|
-
jest.useFakeTimers();
|
|
212
|
-
jest.setSystemTime(new Date("2026-07-16T12:00:00Z"));
|
|
213
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
214
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
215
|
-
const connect = await react_1.screen.findByRole("button", { name: "Connect" });
|
|
216
|
-
jest.setSystemTime(new Date("2026-07-16T12:10:00Z"));
|
|
217
|
-
react_1.fireEvent.click(connect);
|
|
218
|
-
expect(react_1.screen.getByText("Invite expired")).toBeVisible();
|
|
219
|
-
expect(sdk.userConnectCodeAnswer).not.toHaveBeenCalledWith(validCode);
|
|
220
|
-
});
|
|
221
|
-
it("does not treat the initial idle status as a canceled connection", async () => {
|
|
222
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
223
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
224
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Connect" }));
|
|
225
|
-
expect(react_1.screen.getByText("Connecting…")).toBeVisible();
|
|
226
|
-
expect(react_1.screen.queryByText("Connection canceled")).not.toBeInTheDocument();
|
|
227
|
-
});
|
|
228
|
-
it("leaves connecting when the answer expires back to idle", async () => {
|
|
229
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
230
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
231
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Connect" }));
|
|
232
|
-
(0, react_1.act)(() => {
|
|
233
|
-
sdk.userConnectCodeAnswer("");
|
|
234
|
-
sdk.userConnectStatus({ kind: "idle" });
|
|
235
|
-
});
|
|
236
|
-
expect(await react_1.screen.findByText("Invite expired")).toBeVisible();
|
|
237
|
-
});
|
|
238
|
-
it("reports cancellation when an active protocol resets to idle", async () => {
|
|
239
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
240
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
241
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Connect" }));
|
|
242
|
-
(0, react_1.act)(() => {
|
|
243
|
-
sdk.userConnectStatus({ kind: "connecting", attempt: 1 });
|
|
244
|
-
});
|
|
245
|
-
(0, react_1.act)(() => {
|
|
246
|
-
sdk.userConnectStatus({ kind: "idle" });
|
|
247
|
-
});
|
|
248
|
-
expect(await react_1.screen.findByText("Connection canceled")).toBeVisible();
|
|
249
|
-
});
|
|
250
|
-
it("renders actionable structured errors", async () => {
|
|
251
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
252
|
-
(0, react_1.act)(() => {
|
|
253
|
-
sdk.userConnectStatus({ kind: "error", code: "no-route" });
|
|
254
|
-
});
|
|
255
|
-
expect(await react_1.screen.findByText("This device isn’t connected yet")).toBeVisible();
|
|
256
|
-
expect(react_1.screen.getByText("Connect this device to the internet or another Peers device, then try again.")).toBeVisible();
|
|
257
|
-
});
|
|
258
|
-
it("trusts only when the primary success action is selected", async () => {
|
|
259
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
260
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
261
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Connect" }));
|
|
262
|
-
(0, react_1.act)(() => {
|
|
263
|
-
sdk.userConnectStatus({
|
|
264
|
-
kind: "connected",
|
|
265
|
-
remoteUserId: "remote-user",
|
|
266
|
-
remoteDeviceId: "remote-device",
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Trust and view contact" }));
|
|
270
|
-
await (0, react_1.waitFor)(() => {
|
|
271
|
-
expect(sdk.Devices.mock.results.at(-1)?.value.get).toHaveBeenCalledWith("remote-device");
|
|
272
|
-
expect(sdk.setUserTrustLevel).toHaveBeenCalledWith("remote-user", peers_sdk_1.TrustLevel.Trusted, expect.anything());
|
|
273
|
-
expect(globals_1.mainContentPath).toHaveBeenCalledWith("contacts/remote-user");
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
it("can view a connected contact without changing trust", async () => {
|
|
277
|
-
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() });
|
|
278
|
-
(0, react_1.render)((0, jsx_runtime_1.jsx)(user_connect_1.UserConnect, {}));
|
|
279
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "Connect" }));
|
|
280
|
-
(0, react_1.act)(() => {
|
|
281
|
-
sdk.userConnectStatus({
|
|
282
|
-
kind: "connected",
|
|
283
|
-
remoteUserId: "remote-user",
|
|
284
|
-
remoteDeviceId: "remote-device",
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
react_1.fireEvent.click(await react_1.screen.findByRole("button", { name: "View without trusting" }));
|
|
288
|
-
expect(sdk.setUserTrustLevel).not.toHaveBeenCalled();
|
|
289
|
-
expect(globals_1.mainContentPath).toHaveBeenCalledWith("contacts/remote-user");
|
|
290
|
-
});
|
|
291
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/** Browser event emitted after a pairing invitation is captured and removed from the URL. */
|
|
2
|
-
export declare const DEVICE_PAIRING_INVITE_CAPTURED_EVENT = "device-pairing:invite-captured";
|
|
3
|
-
/** Captures a pairing fragment in memory and immediately removes it from browser history. */
|
|
4
|
-
export declare function captureDevicePairingInviteFromLocation(location?: Location): string | undefined;
|
|
5
|
-
/** Returns the in-memory invitation without consuming it. */
|
|
6
|
-
export declare function readPendingDevicePairingInvite(): string | undefined;
|
|
7
|
-
/** Returns and erases the in-memory invitation. */
|
|
8
|
-
export declare function consumePendingDevicePairingInvite(): string | undefined;
|
|
9
|
-
/** Erases any unconsumed invitation. */
|
|
10
|
-
export declare function clearPendingDevicePairingInvite(): void;
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT = void 0;
|
|
4
|
-
exports.captureDevicePairingInviteFromLocation = captureDevicePairingInviteFromLocation;
|
|
5
|
-
exports.readPendingDevicePairingInvite = readPendingDevicePairingInvite;
|
|
6
|
-
exports.consumePendingDevicePairingInvite = consumePendingDevicePairingInvite;
|
|
7
|
-
exports.clearPendingDevicePairingInvite = clearPendingDevicePairingInvite;
|
|
8
|
-
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
9
|
-
let pendingInvitation;
|
|
10
|
-
/** Browser event emitted after a pairing invitation is captured and removed from the URL. */
|
|
11
|
-
exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT = "device-pairing:invite-captured";
|
|
12
|
-
/** Captures a pairing fragment in memory and immediately removes it from browser history. */
|
|
13
|
-
function captureDevicePairingInviteFromLocation(location = window.location) {
|
|
14
|
-
if (!location.hash.startsWith("#pair=") && !location.hash.startsWith("#pair?")) {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
let capturedInvitation;
|
|
18
|
-
try {
|
|
19
|
-
capturedInvitation = (0, peers_sdk_1.encodeDevicePairingInvitation)((0, peers_sdk_1.parseDevicePairingInvitation)(location.href));
|
|
20
|
-
pendingInvitation = capturedInvitation;
|
|
21
|
-
return capturedInvitation;
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
if (typeof window !== "undefined" && location === window.location) {
|
|
28
|
-
window.history.replaceState(window.history.state, document.title, `${location.pathname}${location.search}`);
|
|
29
|
-
if (capturedInvitation) {
|
|
30
|
-
window.dispatchEvent(new CustomEvent(exports.DEVICE_PAIRING_INVITE_CAPTURED_EVENT, {
|
|
31
|
-
detail: capturedInvitation,
|
|
32
|
-
}));
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/** Returns the in-memory invitation without consuming it. */
|
|
38
|
-
function readPendingDevicePairingInvite() {
|
|
39
|
-
return pendingInvitation;
|
|
40
|
-
}
|
|
41
|
-
/** Returns and erases the in-memory invitation. */
|
|
42
|
-
function consumePendingDevicePairingInvite() {
|
|
43
|
-
const invitation = pendingInvitation;
|
|
44
|
-
pendingInvitation = undefined;
|
|
45
|
-
return invitation;
|
|
46
|
-
}
|
|
47
|
-
/** Erases any unconsumed invitation. */
|
|
48
|
-
function clearPendingDevicePairingInvite() {
|
|
49
|
-
pendingInvitation = undefined;
|
|
50
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
4
|
-
const device_pairing_invite_1 = require("./device-pairing-invite");
|
|
5
|
-
describe("device pairing invitation capture", () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
(0, device_pairing_invite_1.clearPendingDevicePairingInvite)();
|
|
8
|
-
window.history.replaceState({}, "", "/?app");
|
|
9
|
-
});
|
|
10
|
-
it("captures the fragment only in memory and removes it from browser history", () => {
|
|
11
|
-
const invitation = (0, peers_sdk_1.createDevicePairingInvitation)({
|
|
12
|
-
serviceOrigin: "https://peers.app",
|
|
13
|
-
});
|
|
14
|
-
const inviteUrl = (0, peers_sdk_1.createDevicePairingInvitationUrl)(`${window.location.origin}/?app`, invitation);
|
|
15
|
-
window.history.replaceState({}, "", new URL(inviteUrl).pathname + new URL(inviteUrl).search + new URL(inviteUrl).hash);
|
|
16
|
-
const token = (0, device_pairing_invite_1.captureDevicePairingInviteFromLocation)();
|
|
17
|
-
expect(token).toBeDefined();
|
|
18
|
-
expect((0, peers_sdk_1.parseDevicePairingInvitation)(token ?? "")).toEqual(invitation);
|
|
19
|
-
expect(window.location.hash).toBe("");
|
|
20
|
-
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBe(token);
|
|
21
|
-
expect((0, device_pairing_invite_1.consumePendingDevicePairingInvite)()).toBe(token);
|
|
22
|
-
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBeUndefined();
|
|
23
|
-
});
|
|
24
|
-
it("ignores invalid fragments without retaining them", () => {
|
|
25
|
-
window.history.replaceState({}, "", "/?app#pair=invalid");
|
|
26
|
-
expect((0, device_pairing_invite_1.captureDevicePairingInviteFromLocation)()).toBeUndefined();
|
|
27
|
-
expect((0, device_pairing_invite_1.readPendingDevicePairingInvite)()).toBeUndefined();
|
|
28
|
-
expect(window.location.hash).toBe("");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Component for admins to prepare group invitations and manage join requests.
|
|
3
|
-
*
|
|
4
|
-
* Shows in the group details Members tab for users with Admin role or above.
|
|
5
|
-
*
|
|
6
|
-
* Communicates with device layer through pvars (not direct imports).
|
|
7
|
-
*/
|
|
8
|
-
import { type UserContext } from "@peers-app/peers-sdk";
|
|
9
|
-
interface GroupInviteListenerProps {
|
|
10
|
-
groupId: string;
|
|
11
|
-
userContext: UserContext;
|
|
12
|
-
}
|
|
13
|
-
export declare const GroupInviteListener: (props: GroupInviteListenerProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
-
export {};
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GroupInviteListener = void 0;
|
|
4
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
/**
|
|
6
|
-
* Component for admins to prepare group invitations and manage join requests.
|
|
7
|
-
*
|
|
8
|
-
* Shows in the group details Members tab for users with Admin role or above.
|
|
9
|
-
*
|
|
10
|
-
* Communicates with device layer through pvars (not direct imports).
|
|
11
|
-
*/
|
|
12
|
-
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
13
|
-
const react_1 = require("react");
|
|
14
|
-
const hooks_1 = require("../../hooks");
|
|
15
|
-
const GroupInviteListener = (props) => {
|
|
16
|
-
const { groupId } = props;
|
|
17
|
-
// State
|
|
18
|
-
const [password, setPassword] = (0, react_1.useState)("");
|
|
19
|
-
const [isListening, setIsListening] = (0, react_1.useState)(false);
|
|
20
|
-
const [copied, setCopied] = (0, react_1.useState)(false);
|
|
21
|
-
const [approvalRole, setApprovalRole] = (0, react_1.useState)(peers_sdk_1.GroupMemberRole.Reader);
|
|
22
|
-
const [processingRequestId, setProcessingRequestId] = (0, react_1.useState)(null);
|
|
23
|
-
// Observables
|
|
24
|
-
const [listeners] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteListeners);
|
|
25
|
-
const [requests] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteRequests);
|
|
26
|
-
const [status] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteStatus);
|
|
27
|
-
// Check if we're currently listening for this group
|
|
28
|
-
const currentListener = listeners?.[groupId];
|
|
29
|
-
const pendingRequests = (requests || []).filter((r) => r.groupId === groupId);
|
|
30
|
-
// Sync local state with listener state
|
|
31
|
-
(0, react_1.useEffect)(() => {
|
|
32
|
-
if (currentListener) {
|
|
33
|
-
setPassword(currentListener.password);
|
|
34
|
-
setIsListening(true);
|
|
35
|
-
}
|
|
36
|
-
else {
|
|
37
|
-
setIsListening(false);
|
|
38
|
-
}
|
|
39
|
-
}, [currentListener]);
|
|
40
|
-
// Generate a new password
|
|
41
|
-
const handleGeneratePassword = (0, react_1.useCallback)(() => {
|
|
42
|
-
const newPassword = (0, peers_sdk_1.generateInvitePassword)();
|
|
43
|
-
setPassword(newPassword);
|
|
44
|
-
}, []);
|
|
45
|
-
// Start listening for invitations (set pvar, device layer reacts)
|
|
46
|
-
const handleStartListening = (0, react_1.useCallback)(() => {
|
|
47
|
-
if (!password.trim() || password.length < 4) {
|
|
48
|
-
alert("Password must be at least 4 characters");
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
// Set the action pvar - device layer will process it
|
|
52
|
-
(0, peers_sdk_1.groupInviteStartListening)({ groupId, password });
|
|
53
|
-
}, [groupId, password]);
|
|
54
|
-
// Stop listening (set pvar, device layer reacts)
|
|
55
|
-
const handleStopListening = (0, react_1.useCallback)(() => {
|
|
56
|
-
// Set the action pvar - device layer will process it
|
|
57
|
-
(0, peers_sdk_1.groupInviteStopListening)(groupId);
|
|
58
|
-
}, [groupId]);
|
|
59
|
-
// Copy password to clipboard
|
|
60
|
-
const handleCopyPassword = (0, react_1.useCallback)(async () => {
|
|
61
|
-
const success = await (0, peers_sdk_1.copyToClipboard)(password);
|
|
62
|
-
if (success) {
|
|
63
|
-
setCopied(true);
|
|
64
|
-
setTimeout(() => setCopied(false), 2000);
|
|
65
|
-
}
|
|
66
|
-
}, [password]);
|
|
67
|
-
// Approve a join request (set pvar, device layer reacts)
|
|
68
|
-
const handleApprove = (0, react_1.useCallback)((request) => {
|
|
69
|
-
setProcessingRequestId(request.requestId);
|
|
70
|
-
// Set the action pvar - device layer will process it
|
|
71
|
-
(0, peers_sdk_1.groupInviteProcessRequest)({
|
|
72
|
-
requestId: request.requestId,
|
|
73
|
-
approved: true,
|
|
74
|
-
role: approvalRole,
|
|
75
|
-
});
|
|
76
|
-
// Clear processing state after a short delay
|
|
77
|
-
setTimeout(() => setProcessingRequestId(null), 1000);
|
|
78
|
-
}, [approvalRole]);
|
|
79
|
-
// Deny a join request (set pvar, device layer reacts)
|
|
80
|
-
const handleDeny = (0, react_1.useCallback)((request) => {
|
|
81
|
-
setProcessingRequestId(request.requestId);
|
|
82
|
-
// Set the action pvar - device layer will process it
|
|
83
|
-
(0, peers_sdk_1.groupInviteProcessRequest)({
|
|
84
|
-
requestId: request.requestId,
|
|
85
|
-
approved: false,
|
|
86
|
-
role: peers_sdk_1.GroupMemberRole.None,
|
|
87
|
-
});
|
|
88
|
-
// Clear processing state after a short delay
|
|
89
|
-
setTimeout(() => setProcessingRequestId(null), 1000);
|
|
90
|
-
}, []);
|
|
91
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "card mb-3", children: [(0, jsx_runtime_1.jsx)("div", { className: "card-header", children: (0, jsx_runtime_1.jsxs)("h6", { className: "mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-plus-fill me-2" }), "Invite New Members"] }) }), (0, jsx_runtime_1.jsxs)("div", { className: "card-body", children: [(0, jsx_runtime_1.jsxs)("div", { className: "mb-3", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small text-muted", children: "Invitation Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "input-group", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control font-monospace", value: password, onChange: (e) => setPassword(e.target.value), placeholder: "Enter or generate a password...", disabled: isListening }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary", onClick: handleGeneratePassword, disabled: isListening, title: "Generate random password", children: (0, jsx_runtime_1.jsx)("i", { className: "bi-shuffle" }) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary", onClick: handleCopyPassword, disabled: !password, title: "Copy password", children: (0, jsx_runtime_1.jsx)("i", { className: copied ? "bi-check-lg text-success" : "bi-clipboard" }) })] }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: "Share this password with people you want to invite. They can enter it in the \"Join Group\" screen." })] }), (0, jsx_runtime_1.jsx)("div", { className: "d-flex gap-2", children: !isListening ? ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-primary", onClick: handleStartListening, disabled: !password || password.length < 4, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-broadcast me-2" }), "Start Listening"] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-danger", onClick: handleStopListening, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-stop-circle me-2" }), "Stop Listening"] })) }), isListening && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-success mt-3 mb-0 d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "spinner-border spinner-border-sm me-2", role: "status", children: (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: "Listening..." }) }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: "Listening for join requests" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsxs)("small", { children: ["Password: ", (0, jsx_runtime_1.jsx)("code", { children: password })] })] })] })), status && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-info mt-3 mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-info-circle me-2" }), status] })), pendingRequests.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "mt-3", children: [(0, jsx_runtime_1.jsxs)("h6", { className: "mb-2", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-inbox-fill me-2" }), "Pending Requests (", pendingRequests.length, ")"] }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-2", children: [(0, jsx_runtime_1.jsx)("label", { className: "form-label small text-muted", children: "Role for new members:" }), (0, jsx_runtime_1.jsxs)("select", { className: "form-select form-select-sm", value: approvalRole, onChange: (e) => setApprovalRole(Number(e.target.value)), style: { width: "auto" }, children: [(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)("div", { className: "list-group", children: pendingRequests.map((request) => ((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.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person-fill me-2" }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("strong", { children: request.requester.name || request.requester.userId }), request.requester.name && ((0, jsx_runtime_1.jsx)("small", { className: "text-muted d-block", children: request.requester.userId }))] })] }), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["Device: ", request.requester.deviceId.slice(0, 8), "...", " | ", "Received: ", new Date(request.receivedAt).toLocaleTimeString()] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-success btn-sm", onClick: () => handleApprove(request), disabled: processingRequestId === request.requestId, children: processingRequestId === request.requestId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-check-lg me-1" }), "Approve"] })) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-danger btn-sm", onClick: () => handleDeny(request), disabled: processingRequestId === request.requestId, children: (0, jsx_runtime_1.jsx)("i", { className: "bi-x-lg" }) })] })] }) }, request.requestId))) })] }))] })] }));
|
|
92
|
-
};
|
|
93
|
-
exports.GroupInviteListener = GroupInviteListener;
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Join Group screen - allows users to join a group by entering an invitation password.
|
|
3
|
-
*
|
|
4
|
-
* Flow:
|
|
5
|
-
* 1. User enters the invitation password
|
|
6
|
-
* 2. System discovers all devices listening for that password
|
|
7
|
-
* 3. User selects a group/device to join
|
|
8
|
-
* 4. System sends join request and waits for approval
|
|
9
|
-
*
|
|
10
|
-
* Communicates with device layer through pvars (not direct imports).
|
|
11
|
-
*/
|
|
12
|
-
export declare const JoinGroup: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.JoinGroup = void 0;
|
|
4
|
-
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
-
/**
|
|
6
|
-
* Join Group screen - allows users to join a group by entering an invitation password.
|
|
7
|
-
*
|
|
8
|
-
* Flow:
|
|
9
|
-
* 1. User enters the invitation password
|
|
10
|
-
* 2. System discovers all devices listening for that password
|
|
11
|
-
* 3. User selects a group/device to join
|
|
12
|
-
* 4. System sends join request and waits for approval
|
|
13
|
-
*
|
|
14
|
-
* Communicates with device layer through pvars (not direct imports).
|
|
15
|
-
*/
|
|
16
|
-
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
17
|
-
const react_1 = require("react");
|
|
18
|
-
const loading_indicator_1 = require("../../components/loading-indicator");
|
|
19
|
-
const globals_1 = require("../../globals");
|
|
20
|
-
const hooks_1 = require("../../hooks");
|
|
21
|
-
const ui_loader_1 = require("../../ui-router/ui-loader");
|
|
22
|
-
const JoinGroup = () => {
|
|
23
|
-
const userContext = (0, hooks_1.usePromise)(() => (0, peers_sdk_1.getUserContext)());
|
|
24
|
-
// State
|
|
25
|
-
const [password, setPassword] = (0, react_1.useState)("");
|
|
26
|
-
const [isSearching, setIsSearching] = (0, react_1.useState)(false);
|
|
27
|
-
const [isJoining, setIsJoining] = (0, react_1.useState)(false);
|
|
28
|
-
const [error, setError] = (0, react_1.useState)(null);
|
|
29
|
-
const [_currentJoiningGroup, setCurrentJoiningGroup] = (0, react_1.useState)(null);
|
|
30
|
-
// Observable state from device layer
|
|
31
|
-
const [status] = (0, hooks_1.useObservable)(peers_sdk_1.groupJoinStatus);
|
|
32
|
-
const [discoveredListeners] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteDiscoveryResult);
|
|
33
|
-
const [joinResult] = (0, hooks_1.useObservable)(peers_sdk_1.groupInviteJoinResult);
|
|
34
|
-
// Watch for join results
|
|
35
|
-
(0, react_1.useEffect)(() => {
|
|
36
|
-
if (isJoining && joinResult) {
|
|
37
|
-
setIsJoining(false);
|
|
38
|
-
if (joinResult.success && joinResult.groupId) {
|
|
39
|
-
// Navigate to the group after a brief delay
|
|
40
|
-
setTimeout(() => {
|
|
41
|
-
(0, globals_1.mainContentPath)(`groups/${joinResult.groupId}`);
|
|
42
|
-
}, 2000);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}, [joinResult, isJoining]);
|
|
46
|
-
// Search for groups (set pvar, device layer reacts)
|
|
47
|
-
const handleSearch = (0, react_1.useCallback)(() => {
|
|
48
|
-
if (!(0, peers_sdk_1.isValidInvitePassword)(password)) {
|
|
49
|
-
setError("Password must be at least 4 characters");
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
setError(null);
|
|
53
|
-
setIsSearching(true);
|
|
54
|
-
// Clear previous results and trigger discovery
|
|
55
|
-
(0, peers_sdk_1.groupInviteDiscoveryResult)([]);
|
|
56
|
-
(0, peers_sdk_1.groupInviteJoinResult)(null);
|
|
57
|
-
(0, peers_sdk_1.groupInviteDiscover)(password);
|
|
58
|
-
}, [password]);
|
|
59
|
-
// Join a group (set pvar, device layer reacts)
|
|
60
|
-
const handleJoin = (0, react_1.useCallback)((listener) => {
|
|
61
|
-
setIsJoining(true);
|
|
62
|
-
setError(null);
|
|
63
|
-
setCurrentJoiningGroup(listener);
|
|
64
|
-
// Clear previous result and trigger join request
|
|
65
|
-
(0, peers_sdk_1.groupInviteJoinResult)(null);
|
|
66
|
-
(0, peers_sdk_1.groupInviteSendRequest)({ listener, password });
|
|
67
|
-
}, [password]);
|
|
68
|
-
// Reset state
|
|
69
|
-
const handleReset = (0, react_1.useCallback)(() => {
|
|
70
|
-
setPassword("");
|
|
71
|
-
setError(null);
|
|
72
|
-
setCurrentJoiningGroup(null);
|
|
73
|
-
setIsSearching(false);
|
|
74
|
-
setIsJoining(false);
|
|
75
|
-
// Clear pvars
|
|
76
|
-
(0, peers_sdk_1.groupInviteDiscoveryResult)([]);
|
|
77
|
-
(0, peers_sdk_1.groupInviteJoinResult)(null);
|
|
78
|
-
}, []);
|
|
79
|
-
// Handle Enter key
|
|
80
|
-
const handleKeyDown = (0, react_1.useCallback)((e) => {
|
|
81
|
-
if (e.key === "Enter" && password && !isSearching) {
|
|
82
|
-
handleSearch();
|
|
83
|
-
}
|
|
84
|
-
}, [password, isSearching, handleSearch]);
|
|
85
|
-
if (!userContext) {
|
|
86
|
-
return (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {});
|
|
87
|
-
}
|
|
88
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid p-4", style: { maxWidth: "600px" }, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-4", children: [(0, jsx_runtime_1.jsxs)("h2", { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-box-arrow-in-right me-2" }), "Join Group"] }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Enter the invitation password to find and join a group" })] }), joinResult?.success && ((0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("div", { className: "mb-4", children: (0, jsx_runtime_1.jsx)("i", { className: "bi-check-circle-fill text-success", style: { fontSize: "4rem" } }) }), (0, jsx_runtime_1.jsx)("h4", { className: "text-success mb-3", children: "Successfully Joined!" }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Redirecting to the group..." }), (0, jsx_runtime_1.jsxs)("button", { className: "btn btn-outline-primary", onClick: handleReset, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-plus-lg me-2" }), "Join Another Group"] })] })), !joinResult?.success && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(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.jsxs)("label", { className: "form-label", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-key-fill me-2" }), "Invitation Password"] }), (0, jsx_runtime_1.jsxs)("div", { className: "input-group", children: [(0, jsx_runtime_1.jsx)("input", { type: "text", className: "form-control form-control-lg font-monospace", value: password, onChange: (e) => setPassword(e.target.value), onKeyDown: handleKeyDown, placeholder: "Enter password (e.g., apple-banana-cherry-date)", disabled: isSearching || isJoining }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-primary", onClick: handleSearch, disabled: !password || isSearching || isJoining, children: isSearching ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2" }), "Searching..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-search me-2" }), "Search"] })) })] }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: "Ask the group admin for the invitation password" })] }) }), error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-exclamation-triangle me-2" }), error] })), status && !error && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-info", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-info-circle me-2" }), status] })), joinResult && !joinResult.success && ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-warning", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-exclamation-circle me-2" }), joinResult.error || "Join request was not approved"] })), discoveredListeners && discoveredListeners.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "card", children: [(0, jsx_runtime_1.jsx)("div", { className: "card-header", children: (0, jsx_runtime_1.jsxs)("h6", { className: "mb-0", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-broadcast me-2" }), "Found Groups (", discoveredListeners.length, ")"] }) }), (0, jsx_runtime_1.jsx)("div", { className: "list-group list-group-flush", children: discoveredListeners.map((listener, index) => ((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.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center mb-1", children: [(0, jsx_runtime_1.jsx)("i", { className: `${listener.group.iconClassName || "bi-people-fill"} me-2` }), (0, jsx_runtime_1.jsx)("strong", { children: listener.group.name })] }), listener.group.description && ((0, jsx_runtime_1.jsx)("p", { className: "text-muted mb-1 small", children: listener.group.description })), (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-person me-1" }), "Invited by: ", listener.name || listener.userId] })] }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-success", onClick: () => handleJoin(listener), disabled: isJoining, children: isJoining ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-2" }), "Joining..."] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("i", { className: "bi-box-arrow-in-right me-2" }), "Join"] })) })] }) }, `${listener.deviceId}-${index}`))) })] })), isSearching && ((0, jsx_runtime_1.jsxs)("div", { className: "text-center py-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "spinner-border text-primary mb-3", role: "status", children: (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: "Searching..." }) }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Searching for groups..." })] })), isJoining && ((0, jsx_runtime_1.jsxs)("div", { className: "text-center py-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "spinner-border text-success mb-3", role: "status", children: (0, jsx_runtime_1.jsx)("span", { className: "visually-hidden", children: "Joining..." }) }), (0, jsx_runtime_1.jsx)("p", { className: "text-muted", children: "Waiting for approval..." }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted", children: "The group admin needs to approve your request" })] }))] }))] }));
|
|
89
|
-
};
|
|
90
|
-
exports.JoinGroup = JoinGroup;
|
|
91
|
-
// Register the route
|
|
92
|
-
(0, ui_loader_1.registerInternalPeersUI)({
|
|
93
|
-
peersUIId: "00join0group0screen000000",
|
|
94
|
-
component: exports.JoinGroup,
|
|
95
|
-
routes: [
|
|
96
|
-
{
|
|
97
|
-
isMatch: (_props, context) => context.path === "join-group",
|
|
98
|
-
uiCategory: "screen",
|
|
99
|
-
priority: 2,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
});
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.joinGroupApp = void 0;
|
|
4
|
-
exports.joinGroupApp = {
|
|
5
|
-
name: "Join Group",
|
|
6
|
-
displayName: "Join Group",
|
|
7
|
-
iconClassName: "bi-box-arrow-in-right",
|
|
8
|
-
navigationPath: "join-group",
|
|
9
|
-
};
|