@peers-app/peers-ui 0.20.3 → 0.20.5
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/dist/root-layout/root-layout-host.js +26 -0
- package/dist/root-layout/root-layout-host.test.d.ts +1 -0
- package/dist/root-layout/root-layout-host.test.js +84 -0
- package/dist/screens/contacts/user-connect-invite.d.ts +118 -0
- package/dist/screens/contacts/user-connect-invite.js +264 -0
- package/dist/screens/contacts/user-connect-invite.test.d.ts +1 -0
- package/dist/screens/contacts/user-connect-invite.test.js +215 -0
- package/dist/screens/contacts/user-connect.d.ts +7 -0
- package/dist/screens/contacts/user-connect.js +402 -126
- package/dist/screens/contacts/user-connect.test.d.ts +1 -0
- package/dist/screens/contacts/user-connect.test.js +291 -0
- package/package.json +5 -4
- package/src/root-layout/root-layout-host.test.tsx +106 -0
- package/src/root-layout/root-layout-host.tsx +33 -2
- package/src/screens/contacts/user-connect-invite.test.ts +256 -0
- package/src/screens/contacts/user-connect-invite.ts +332 -0
- package/src/screens/contacts/user-connect.test.tsx +382 -0
- package/src/screens/contacts/user-connect.tsx +735 -310
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const peers_sdk_1 = require("@peers-app/peers-sdk");
|
|
4
|
+
const user_connect_invite_1 = require("./user-connect-invite");
|
|
5
|
+
function createMemoryStorage() {
|
|
6
|
+
const map = new Map();
|
|
7
|
+
return {
|
|
8
|
+
get length() {
|
|
9
|
+
return map.size;
|
|
10
|
+
},
|
|
11
|
+
clear() {
|
|
12
|
+
map.clear();
|
|
13
|
+
},
|
|
14
|
+
getItem(key) {
|
|
15
|
+
return map.get(key) ?? null;
|
|
16
|
+
},
|
|
17
|
+
key(index) {
|
|
18
|
+
return [...map.keys()][index] ?? null;
|
|
19
|
+
},
|
|
20
|
+
removeItem(key) {
|
|
21
|
+
map.delete(key);
|
|
22
|
+
},
|
|
23
|
+
setItem(key, value) {
|
|
24
|
+
map.set(key, value);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
describe("user-connect-invite", () => {
|
|
29
|
+
const validCode = "ABCD1234EFGH";
|
|
30
|
+
const formattedCode = "ABCD-1234-EFGH";
|
|
31
|
+
describe("normalizeUserConnectCode", () => {
|
|
32
|
+
it("accepts formatted and unformatted valid codes", () => {
|
|
33
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)(validCode)).toBe(validCode);
|
|
34
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)(formattedCode)).toBe(validCode);
|
|
35
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("abcd-1234-efgh")).toBe(validCode);
|
|
36
|
+
});
|
|
37
|
+
it("rejects invalid alphabet and length", () => {
|
|
38
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("ABCD1234EFG")).toBeNull();
|
|
39
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("ABCD1234EFGHI")).toBeNull();
|
|
40
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("ABCD1234EFGI")).toBeNull(); // I invalid in Crockford
|
|
41
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("A-B-C-D-1-2-3-4-E-F-G-H")).toBeNull();
|
|
42
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("ABCD 1234 EFGH")).toBeNull();
|
|
43
|
+
expect((0, user_connect_invite_1.normalizeUserConnectCode)("")).toBeNull();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe("extractUserConnectCodeFromFragment", () => {
|
|
47
|
+
it("extracts codes from contacts/connect fragments", () => {
|
|
48
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)(`#contacts/connect?code=${formattedCode}`)).toBe(formattedCode);
|
|
49
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)(`contacts/connect?code=${validCode}`)).toBe(validCode);
|
|
50
|
+
});
|
|
51
|
+
it("returns null for unrelated routes", () => {
|
|
52
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)("#contacts/list")).toBeNull();
|
|
53
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)("#home")).toBeNull();
|
|
54
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)("#contacts/connect")).toBeNull();
|
|
55
|
+
expect((0, user_connect_invite_1.extractUserConnectCodeFromFragment)("")).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("buildUserConnectInviteUrl", () => {
|
|
59
|
+
it("builds the canonical peers.app URL with ?app and fragment code", () => {
|
|
60
|
+
const url = (0, user_connect_invite_1.buildUserConnectInviteUrl)(validCode);
|
|
61
|
+
expect(url).toBe(`https://peers.app/?app#contacts/connect?code=${encodeURIComponent(formattedCode)}`);
|
|
62
|
+
expect(url.includes("?app#")).toBe(true);
|
|
63
|
+
expect(url.split("?")[1]?.startsWith("app#")).toBe(true);
|
|
64
|
+
const beforeHash = url.split("#")[0];
|
|
65
|
+
expect(beforeHash.includes(validCode)).toBe(false);
|
|
66
|
+
expect(beforeHash.includes(formattedCode)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it("accepts an explicit base URL without putting the code in the HTTP query", () => {
|
|
69
|
+
const url = (0, user_connect_invite_1.buildUserConnectInviteUrl)(formattedCode, "https://example.test/");
|
|
70
|
+
expect(url.startsWith("https://example.test/?app#")).toBe(true);
|
|
71
|
+
expect(url).toContain(`code=${encodeURIComponent(formattedCode)}`);
|
|
72
|
+
const query = new URL(url.split("#")[0]).searchParams;
|
|
73
|
+
expect(query.has("code")).toBe(false);
|
|
74
|
+
expect(query.has("app")).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("sessionStorage intent lifecycle", () => {
|
|
78
|
+
it("writes, reads, and one-time consumes a pending intent", () => {
|
|
79
|
+
const storage = createMemoryStorage();
|
|
80
|
+
const now = 1_000_000;
|
|
81
|
+
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: now }, storage);
|
|
82
|
+
const read = (0, user_connect_invite_1.readUserConnectPendingIntent)(storage, now + 1000);
|
|
83
|
+
expect(read).toEqual({
|
|
84
|
+
ok: true,
|
|
85
|
+
intent: { code: validCode, capturedAt: now },
|
|
86
|
+
});
|
|
87
|
+
expect((0, user_connect_invite_1.hasPendingUserConnectInvite)(storage, now + 1000)).toBe(true);
|
|
88
|
+
const consumed = (0, user_connect_invite_1.consumeUserConnectPendingIntent)(storage, now + 1000);
|
|
89
|
+
expect(consumed).toEqual({
|
|
90
|
+
ok: true,
|
|
91
|
+
intent: { code: validCode, capturedAt: now },
|
|
92
|
+
});
|
|
93
|
+
expect(storage.getItem(user_connect_invite_1.USER_CONNECT_INVITE_STORAGE_KEY)).toBeNull();
|
|
94
|
+
expect((0, user_connect_invite_1.consumeUserConnectPendingIntent)(storage, now + 1000)).toEqual({
|
|
95
|
+
ok: false,
|
|
96
|
+
reason: "none",
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it("expires stale intents using the shared TTL", () => {
|
|
100
|
+
const storage = createMemoryStorage();
|
|
101
|
+
const now = 1_000_000;
|
|
102
|
+
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: now }, storage);
|
|
103
|
+
expect((0, user_connect_invite_1.readUserConnectPendingIntent)(storage, now + peers_sdk_1.USER_CONNECT_TTL_MS)).toEqual({
|
|
104
|
+
ok: false,
|
|
105
|
+
reason: "expired",
|
|
106
|
+
});
|
|
107
|
+
expect((0, user_connect_invite_1.consumeUserConnectPendingIntent)(storage, now + peers_sdk_1.USER_CONNECT_TTL_MS)).toEqual({
|
|
108
|
+
ok: false,
|
|
109
|
+
reason: "expired",
|
|
110
|
+
});
|
|
111
|
+
expect(storage.getItem(user_connect_invite_1.USER_CONNECT_INVITE_STORAGE_KEY)).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
it("rejects implausible future capture timestamps", () => {
|
|
114
|
+
const storage = createMemoryStorage();
|
|
115
|
+
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: 20_000 }, storage);
|
|
116
|
+
expect((0, user_connect_invite_1.readUserConnectPendingIntent)(storage, 10_000)).toEqual({
|
|
117
|
+
ok: false,
|
|
118
|
+
reason: "expired",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it("treats malformed stored data as invalid", () => {
|
|
122
|
+
const storage = createMemoryStorage();
|
|
123
|
+
storage.setItem(user_connect_invite_1.USER_CONNECT_INVITE_STORAGE_KEY, "{not-json");
|
|
124
|
+
expect((0, user_connect_invite_1.readUserConnectPendingIntent)(storage)).toEqual({ ok: false, reason: "invalid" });
|
|
125
|
+
expect((0, user_connect_invite_1.consumeUserConnectPendingIntent)(storage)).toEqual({
|
|
126
|
+
ok: false,
|
|
127
|
+
reason: "invalid",
|
|
128
|
+
});
|
|
129
|
+
expect(storage.getItem(user_connect_invite_1.USER_CONNECT_INVITE_STORAGE_KEY)).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
it("clears pending intent explicitly", () => {
|
|
132
|
+
const storage = createMemoryStorage();
|
|
133
|
+
(0, user_connect_invite_1.writeUserConnectPendingIntent)({ code: validCode, capturedAt: Date.now() }, storage);
|
|
134
|
+
(0, user_connect_invite_1.clearUserConnectPendingIntent)(storage);
|
|
135
|
+
expect((0, user_connect_invite_1.readUserConnectPendingIntent)(storage)).toEqual({ ok: false, reason: "none" });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe("capture and URL cleanup", () => {
|
|
139
|
+
it("captures a valid invite, stores it, and strips the code from the URL", () => {
|
|
140
|
+
const storage = createMemoryStorage();
|
|
141
|
+
let replacedUrl = "";
|
|
142
|
+
const locationLike = {
|
|
143
|
+
pathname: "/",
|
|
144
|
+
search: "?app",
|
|
145
|
+
hash: `#contacts/connect?code=${formattedCode}`,
|
|
146
|
+
};
|
|
147
|
+
const historyLike = {
|
|
148
|
+
state: null,
|
|
149
|
+
replaceState: (_state, _title, url) => {
|
|
150
|
+
replacedUrl = String(url ?? "");
|
|
151
|
+
locationLike.hash = replacedUrl.includes("#") ? `#${replacedUrl.split("#")[1]}` : "";
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const result = (0, user_connect_invite_1.captureUserConnectInviteFromLocation)({
|
|
155
|
+
locationLike,
|
|
156
|
+
historyLike,
|
|
157
|
+
storage,
|
|
158
|
+
nowMs: 5_000,
|
|
159
|
+
});
|
|
160
|
+
expect(result).toEqual({
|
|
161
|
+
ok: true,
|
|
162
|
+
intent: { code: validCode, capturedAt: 5_000 },
|
|
163
|
+
});
|
|
164
|
+
expect(replacedUrl).toBe("/?app#contacts/connect");
|
|
165
|
+
expect(locationLike.hash).toBe("#contacts/connect");
|
|
166
|
+
expect((0, user_connect_invite_1.hasPendingUserConnectInvite)(storage, 5_000)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
it("stores an invalid marker for malformed codes and still cleans the URL", () => {
|
|
169
|
+
const storage = createMemoryStorage();
|
|
170
|
+
const locationLike = {
|
|
171
|
+
pathname: "/",
|
|
172
|
+
search: "?app",
|
|
173
|
+
hash: "#contacts/connect?code=BAD",
|
|
174
|
+
};
|
|
175
|
+
const historyLike = {
|
|
176
|
+
state: null,
|
|
177
|
+
replaceState: (_state, _title, url) => {
|
|
178
|
+
locationLike.hash = String(url ?? "").includes("#")
|
|
179
|
+
? `#${String(url).split("#")[1]}`
|
|
180
|
+
: "";
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
expect((0, user_connect_invite_1.captureUserConnectInviteFromLocation)({
|
|
184
|
+
locationLike,
|
|
185
|
+
historyLike,
|
|
186
|
+
storage,
|
|
187
|
+
nowMs: 9_000,
|
|
188
|
+
})).toEqual({ ok: false, reason: "invalid" });
|
|
189
|
+
expect(locationLike.hash).toBe("#contacts/connect");
|
|
190
|
+
expect((0, user_connect_invite_1.readUserConnectPendingIntent)(storage, 9_000)).toEqual({
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "invalid",
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
it("stripUserConnectCodeFromUrl is a no-op without a code", () => {
|
|
196
|
+
const locationLike = { pathname: "/", search: "", hash: "#home" };
|
|
197
|
+
let calls = 0;
|
|
198
|
+
(0, user_connect_invite_1.stripUserConnectCodeFromUrl)(locationLike, {
|
|
199
|
+
state: null,
|
|
200
|
+
replaceState: () => {
|
|
201
|
+
calls += 1;
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
expect(calls).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe("buildUserConnectShareText", () => {
|
|
208
|
+
it("mentions expiry and that both people need Peers open", () => {
|
|
209
|
+
const text = (0, user_connect_invite_1.buildUserConnectShareText)("https://peers.app/?app#contacts/connect?code=X");
|
|
210
|
+
expect(text).toContain("https://peers.app/?app#contacts/connect?code=X");
|
|
211
|
+
expect(text.toLowerCase()).toContain("expire");
|
|
212
|
+
expect(text.toLowerCase()).toContain("peers open");
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect-to-user screen: QR invite, link share, manual code entry, and trust confirmation.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Polished Connect to user screen. Creating an invite is the default path;
|
|
6
|
+
* inbound links open a review state before connecting.
|
|
7
|
+
*/
|
|
1
8
|
export declare function UserConnect(): import("react/jsx-runtime").JSX.Element;
|