@peers-app/peers-ui 0.25.1 → 0.25.3

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 (177) hide show
  1. package/README.md +2 -1
  2. package/dist/components/markdown-editor/mention-node.d.ts +4 -0
  3. package/dist/components/markdown-editor/mention-node.js +4 -1
  4. package/dist/components/responsive-section-nav.d.ts +34 -0
  5. package/dist/components/responsive-section-nav.js +19 -0
  6. package/dist/components/responsive-section-nav.test.d.ts +1 -0
  7. package/dist/components/responsive-section-nav.test.js +45 -0
  8. package/dist/components/router.d.ts +2 -3
  9. package/dist/components/router.js +6 -9
  10. package/dist/globals-hash.test.d.ts +1 -0
  11. package/dist/globals-hash.test.js +16 -0
  12. package/dist/globals.d.ts +19 -0
  13. package/dist/globals.js +51 -12
  14. package/dist/hooks/use-host-contract.d.ts +18 -0
  15. package/dist/hooks/use-host-contract.js +53 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/peers-services-registration.d.ts +1 -5
  19. package/dist/peers-services-registration.js +6 -16
  20. package/dist/root-layout/root-layout-host.js +7 -4
  21. package/dist/root-layout/root-layout-host.test.js +3 -6
  22. package/dist/screens/account/account-screen.d.ts +1 -0
  23. package/dist/screens/account/account-screen.js +29 -53
  24. package/dist/screens/account/account-screen.test.d.ts +1 -0
  25. package/dist/screens/account/account-screen.test.js +47 -0
  26. package/dist/screens/contacts/contact-details.js +2 -23
  27. package/dist/screens/contacts/contact-list.d.ts +24 -1
  28. package/dist/screens/contacts/contact-list.js +128 -143
  29. package/dist/screens/contacts/contact-list.test.d.ts +1 -0
  30. package/dist/screens/contacts/contact-list.test.js +48 -0
  31. package/dist/screens/contacts/index.d.ts +0 -2
  32. package/dist/screens/contacts/index.js +0 -3
  33. package/dist/screens/device-pairing/device-pairing.js +17 -16
  34. package/dist/screens/device-pairing/device-pairing.test.js +4 -9
  35. package/dist/screens/device-pairing/identity-ipc.d.ts +11 -0
  36. package/dist/screens/device-pairing/identity-ipc.js +20 -0
  37. package/dist/screens/device-pairing/use-device-pairing-state.d.ts +2 -2
  38. package/dist/screens/device-pairing/use-device-pairing-state.js +7 -25
  39. package/dist/screens/devices/devices-list.d.ts +18 -0
  40. package/dist/screens/devices/devices-list.js +173 -0
  41. package/dist/screens/devices/devices-list.test.d.ts +1 -0
  42. package/dist/screens/devices/devices-list.test.js +53 -0
  43. package/dist/screens/devices/index.d.ts +1 -0
  44. package/dist/screens/devices/index.js +5 -0
  45. package/dist/screens/groups/group-details.d.ts +1 -0
  46. package/dist/screens/groups/group-details.js +4 -63
  47. package/dist/screens/groups/group-invite-panel.js +2 -2
  48. package/dist/screens/groups/group-list.d.ts +18 -1
  49. package/dist/screens/groups/group-list.js +107 -136
  50. package/dist/screens/groups/group-list.test.d.ts +1 -0
  51. package/dist/screens/groups/group-list.test.js +38 -0
  52. package/dist/screens/groups/group-members.d.ts +1 -0
  53. package/dist/screens/groups/group-members.js +2 -1
  54. package/dist/screens/groups/index.d.ts +0 -3
  55. package/dist/screens/groups/index.js +0 -4
  56. package/dist/screens/identity/identity-routes.d.ts +107 -0
  57. package/dist/screens/identity/identity-routes.js +245 -0
  58. package/dist/screens/identity/identity-routes.test.d.ts +1 -0
  59. package/dist/screens/identity/identity-routes.test.js +51 -0
  60. package/dist/screens/identity/identity-screen.d.ts +9 -0
  61. package/dist/screens/identity/identity-screen.js +143 -0
  62. package/dist/screens/identity/identity-screen.test.d.ts +1 -0
  63. package/dist/screens/identity/identity-screen.test.js +103 -0
  64. package/dist/screens/identity/index.d.ts +2 -0
  65. package/dist/screens/identity/index.js +6 -0
  66. package/dist/screens/identity/invite-contact-panel.d.ts +4 -0
  67. package/dist/screens/identity/invite-contact-panel.js +30 -0
  68. package/dist/screens/identity/profile-invite.d.ts +7 -0
  69. package/dist/screens/identity/profile-invite.js +62 -0
  70. package/dist/screens/invites/accept-invite.js +1 -1
  71. package/dist/screens/invites/activity-view.d.ts +24 -0
  72. package/dist/screens/invites/activity-view.js +64 -0
  73. package/dist/screens/invites/activity-view.test.d.ts +1 -0
  74. package/dist/screens/invites/activity-view.test.js +108 -0
  75. package/dist/screens/invites/index.d.ts +1 -2
  76. package/dist/screens/invites/index.js +1 -3
  77. package/dist/screens/invites/invite-capture.d.ts +0 -2
  78. package/dist/screens/invites/invite-capture.js +1 -3
  79. package/dist/screens/invites/invite-classify.test.d.ts +1 -0
  80. package/dist/screens/invites/invite-classify.test.js +98 -0
  81. package/dist/screens/invites/invites-ipc.d.ts +1 -1
  82. package/dist/screens/invites/invites-ipc.js +5 -45
  83. package/dist/screens/invites/pending-invites-list.d.ts +35 -16
  84. package/dist/screens/invites/pending-invites-list.js +69 -30
  85. package/dist/screens/network-viewer/network-viewer-ipc.js +4 -59
  86. package/dist/screens/search/global-search.d.ts +1 -0
  87. package/dist/screens/search/global-search.js +2 -1
  88. package/dist/screens/settings/settings-page.js +25 -58
  89. package/dist/screens/setup-user.js +5 -5
  90. package/dist/screens/tools/tool-access-ipc.d.ts +2 -0
  91. package/dist/screens/tools/tool-access-ipc.js +8 -55
  92. package/dist/screens/welcome-modal.js +3 -2
  93. package/dist/system-apps/identity.app.d.ts +3 -0
  94. package/dist/system-apps/identity.app.js +10 -0
  95. package/dist/system-apps/index.d.ts +1 -4
  96. package/dist/system-apps/index.js +5 -17
  97. package/dist/tabs-layout/network-status-indicator.js +10 -3
  98. package/dist/tabs-layout/network-status-indicator.test.js +15 -0
  99. package/dist/tabs-layout/tabs-layout.js +3 -1
  100. package/dist/tabs-layout/tabs-state.d.ts +79 -1
  101. package/dist/tabs-layout/tabs-state.js +256 -79
  102. package/dist/tabs-layout/tabs-state.test.d.ts +1 -0
  103. package/dist/tabs-layout/tabs-state.test.js +110 -0
  104. package/package.json +3 -3
  105. package/src/components/markdown-editor/mention-node.ts +5 -1
  106. package/src/components/responsive-section-nav.test.tsx +88 -0
  107. package/src/components/responsive-section-nav.tsx +92 -0
  108. package/src/components/router.tsx +6 -9
  109. package/src/globals-hash.test.ts +19 -0
  110. package/src/globals.tsx +53 -13
  111. package/src/hooks/use-host-contract.ts +75 -0
  112. package/src/index.tsx +1 -0
  113. package/src/peers-services-registration.ts +6 -17
  114. package/src/root-layout/root-layout-host.test.tsx +4 -7
  115. package/src/root-layout/root-layout-host.tsx +9 -11
  116. package/src/screens/account/account-screen.test.tsx +61 -0
  117. package/src/screens/account/account-screen.tsx +116 -95
  118. package/src/screens/contacts/contact-details.tsx +4 -26
  119. package/src/screens/contacts/contact-list.test.tsx +59 -0
  120. package/src/screens/contacts/contact-list.tsx +246 -260
  121. package/src/screens/contacts/index.ts +0 -4
  122. package/src/screens/device-pairing/device-pairing.test.tsx +7 -9
  123. package/src/screens/device-pairing/device-pairing.tsx +19 -23
  124. package/src/screens/device-pairing/identity-ipc.ts +25 -0
  125. package/src/screens/device-pairing/use-device-pairing-state.ts +10 -33
  126. package/src/screens/devices/devices-list.test.tsx +68 -0
  127. package/src/screens/devices/devices-list.tsx +361 -0
  128. package/src/screens/devices/index.ts +1 -0
  129. package/src/screens/groups/group-details.tsx +8 -142
  130. package/src/screens/groups/group-invite-panel.tsx +2 -7
  131. package/src/screens/groups/group-list.test.tsx +44 -0
  132. package/src/screens/groups/group-list.tsx +227 -288
  133. package/src/screens/groups/group-members.tsx +3 -2
  134. package/src/screens/groups/index.ts +0 -5
  135. package/src/screens/identity/identity-routes.test.ts +70 -0
  136. package/src/screens/identity/identity-routes.ts +301 -0
  137. package/src/screens/identity/identity-screen.test.tsx +138 -0
  138. package/src/screens/identity/identity-screen.tsx +282 -0
  139. package/src/screens/identity/index.ts +2 -0
  140. package/src/screens/identity/invite-contact-panel.tsx +45 -0
  141. package/src/screens/identity/profile-invite.tsx +93 -0
  142. package/src/screens/invites/accept-invite.tsx +1 -1
  143. package/src/screens/invites/activity-view.test.tsx +127 -0
  144. package/src/screens/invites/activity-view.tsx +162 -0
  145. package/src/screens/invites/index.ts +1 -4
  146. package/src/screens/invites/invite-capture.ts +0 -3
  147. package/src/screens/invites/invite-classify.test.ts +110 -0
  148. package/src/screens/invites/invites-ipc.ts +6 -55
  149. package/src/screens/invites/pending-invites-list.tsx +85 -61
  150. package/src/screens/network-viewer/network-viewer-ipc.ts +8 -82
  151. package/src/screens/search/global-search.tsx +2 -1
  152. package/src/screens/settings/settings-page.tsx +53 -126
  153. package/src/screens/setup-user.tsx +5 -6
  154. package/src/screens/tools/tool-access-ipc.ts +13 -74
  155. package/src/screens/welcome-modal.tsx +3 -3
  156. package/src/system-apps/identity.app.ts +9 -0
  157. package/src/system-apps/index.ts +3 -12
  158. package/src/tabs-layout/network-status-indicator.test.tsx +22 -0
  159. package/src/tabs-layout/network-status-indicator.tsx +31 -0
  160. package/src/tabs-layout/tabs-layout.tsx +7 -1
  161. package/src/tabs-layout/tabs-state.test.ts +152 -0
  162. package/src/tabs-layout/tabs-state.ts +300 -83
  163. package/dist/screens/invites/invites-screen.d.ts +0 -9
  164. package/dist/screens/invites/invites-screen.js +0 -36
  165. package/dist/system-apps/account.app.d.ts +0 -2
  166. package/dist/system-apps/account.app.js +0 -8
  167. package/dist/system-apps/contacts.app.d.ts +0 -2
  168. package/dist/system-apps/contacts.app.js +0 -9
  169. package/dist/system-apps/groups.app.d.ts +0 -2
  170. package/dist/system-apps/groups.app.js +0 -9
  171. package/dist/system-apps/invites.app.d.ts +0 -2
  172. package/dist/system-apps/invites.app.js +0 -9
  173. package/src/screens/invites/invites-screen.tsx +0 -78
  174. package/src/system-apps/account.app.ts +0 -7
  175. package/src/system-apps/contacts.app.ts +0 -8
  176. package/src/system-apps/groups.app.ts +0 -8
  177. package/src/system-apps/invites.app.ts +0 -8
@@ -1,18 +1,11 @@
1
1
  import {
2
- createContractConsumer,
3
- getHostContractTransport,
4
2
  getNetworkControlContractDefinition,
5
3
  getNetworkDiagnosticsContractDefinition,
6
- type IContractTransport,
4
+ hostContractConsumer,
7
5
  type INetworkControlConsumer,
8
6
  type INetworkDiagnosticsConsumer,
9
- subscribeHostContractTransport,
10
- waitForHostContractTransport,
11
7
  } from "@peers-app/peers-sdk";
12
8
 
13
- /** Default wait for the host transport before treating diagnostics as unavailable. */
14
- const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
15
-
16
9
  /** Electron-only capabilities intentionally excluded from portable network contracts. */
17
10
  export type NetworkViewerPlatformApi = {
18
11
  downloadRemoteDatabase?: (groupId: string, deviceId: string) => Promise<unknown>;
@@ -23,88 +16,21 @@ type WindowWithNetworkViewer = Window & {
23
16
  electronAPI?: { networkViewer?: NetworkViewerPlatformApi };
24
17
  };
25
18
 
26
- interface INetworkViewerConsumers {
27
- transport: IContractTransport;
28
- diagnostics: INetworkDiagnosticsConsumer;
29
- control: INetworkControlConsumer;
30
- }
31
-
32
- let cachedConsumers: INetworkViewerConsumers | undefined;
33
- let pendingConsumers: Promise<INetworkViewerConsumers> | undefined;
34
-
35
- function disposeCachedConsumers(): void {
36
- cachedConsumers?.diagnostics.dispose();
37
- cachedConsumers?.control.dispose();
38
- cachedConsumers = undefined;
39
- }
40
-
41
- subscribeHostContractTransport(
42
- (transport) => {
43
- if (cachedConsumers && cachedConsumers.transport !== transport) {
44
- disposeCachedConsumers();
45
- }
46
- },
47
- { emitCurrent: false },
19
+ const diagnostics = hostContractConsumer<INetworkDiagnosticsConsumer>(
20
+ getNetworkDiagnosticsContractDefinition(),
21
+ );
22
+ const control = hostContractConsumer<INetworkControlConsumer>(
23
+ getNetworkControlContractDefinition(),
48
24
  );
49
-
50
- async function loadConsumers(transport: IContractTransport): Promise<INetworkViewerConsumers> {
51
- disposeCachedConsumers();
52
- const diagnostics = createContractConsumer(
53
- getNetworkDiagnosticsContractDefinition(),
54
- transport,
55
- ) as unknown as INetworkDiagnosticsConsumer;
56
- const control = createContractConsumer(
57
- getNetworkControlContractDefinition(),
58
- transport,
59
- ) as unknown as INetworkControlConsumer;
60
- try {
61
- await diagnostics.loadingPromise;
62
- if (getHostContractTransport() !== transport) {
63
- diagnostics.dispose();
64
- control.dispose();
65
- // Clear before recursing so getConsumers can start a fresh in-flight memo for the
66
- // current transport, and so this promise's finally does not wipe that newer memo.
67
- pendingConsumers = undefined;
68
- return getConsumers();
69
- }
70
- cachedConsumers = { transport, diagnostics, control };
71
- return cachedConsumers;
72
- } catch (error) {
73
- diagnostics.dispose();
74
- control.dispose();
75
- throw error;
76
- }
77
- }
78
-
79
- async function getConsumers(): Promise<INetworkViewerConsumers> {
80
- const transport =
81
- getHostContractTransport() ??
82
- (await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
83
- if (cachedConsumers?.transport === transport) {
84
- return cachedConsumers;
85
- }
86
- if (pendingConsumers) {
87
- return pendingConsumers;
88
- }
89
- const loadPromise = loadConsumers(transport);
90
- pendingConsumers = loadPromise;
91
- try {
92
- return await loadPromise;
93
- } finally {
94
- if (pendingConsumers === loadPromise) {
95
- pendingConsumers = undefined;
96
- }
97
- }
98
- }
99
25
 
100
26
  /** Return a typed diagnostics consumer bound to the current local host transport. */
101
27
  export async function getNetworkDiagnosticsConsumer(): Promise<INetworkDiagnosticsConsumer> {
102
- return (await getConsumers()).diagnostics;
28
+ return diagnostics.get();
103
29
  }
104
30
 
105
31
  /** Return a typed control consumer bound to the current local host transport. */
106
32
  export async function getNetworkControlConsumer(): Promise<INetworkControlConsumer> {
107
- return (await getConsumers()).control;
33
+ return control.get();
108
34
  }
109
35
 
110
36
  /** Return the context-isolated desktop-only capability bridge, when available. */
@@ -39,9 +39,10 @@ const searchConfigs: Array<{
39
39
  { config: assistantMentionConfig, category: "Assistants", navigationPath: "assistants" },
40
40
  { config: workflowMentionConfig, category: "Workflows", navigationPath: "workflows" },
41
41
  { config: valueTypeMentionConfig, category: "Types", navigationPath: "peer-types" },
42
- { config: userMentionConfig, category: "Users", navigationPath: "contacts" },
42
+ { config: userMentionConfig, category: "People", navigationPath: "identity/people" },
43
43
  ];
44
44
 
45
+ /** Search system apps and known entities, then navigate to their canonical routes. */
45
46
  export function GlobalSearch() {
46
47
  const [_colorMode] = useObservable(colorMode);
47
48
  const [packages] = useObservable(allPackages);
@@ -1,14 +1,10 @@
1
1
  import {
2
2
  autoUpdatePeersCore,
3
- Devices,
4
3
  DISPLAY_NAME_MAX_LENGTH,
5
4
  getUserContext,
6
- type IDevice,
7
5
  packagesRootDir,
8
6
  reloadPackagesOnPageRefresh,
9
7
  rpcServerCalls,
10
- TrustLevel,
11
- thisDeviceId,
12
8
  Users,
13
9
  } from "@peers-app/peers-sdk";
14
10
  import type React from "react";
@@ -17,9 +13,10 @@ import { Input } from "../../components/input";
17
13
  import { Tabs } from "../../components/tabs";
18
14
  import { Tooltip } from "../../components/tooltip";
19
15
  import * as globals from "../../globals";
20
- import { useObservable, useObservableState, usePromise } from "../../hooks";
16
+ import { useObservable, usePromise } from "../../hooks";
21
17
  import { LayoutPicker } from "../../root-layout/layout-picker";
22
- import { SourceDevicePairing } from "../device-pairing";
18
+ import { identityTools } from "../device-pairing/identity-ipc";
19
+ import { IDENTITY_ACCOUNT_PATH, IDENTITY_PAIR_PATH } from "../identity/identity-routes";
23
20
  import { ColorModeDropdown } from "./color-mode-dropdown";
24
21
 
25
22
  /** Electron file picker attaches a filesystem path on `File`. */
@@ -46,24 +43,29 @@ function getAppVersion() {
46
43
  /** Application settings organized by user, system, and developer concerns. */
47
44
  export const SettingsPage: React.FC = () => {
48
45
  return (
49
- <div className="container-fluid">
50
- <Tabs
51
- tabs={[
52
- {
53
- name: "User",
54
- content: <UserSettingsTab />,
55
- active: true,
56
- },
57
- {
58
- name: "Appearance",
59
- content: <AppearanceSettingsTab />,
60
- },
61
- {
62
- name: "Advanced",
63
- content: <AdvancedSettingsTab />,
64
- },
65
- ]}
66
- />
46
+ <div className="d-flex flex-column h-100 overflow-hidden" style={{ minHeight: 0 }}>
47
+ <div className="container-fluid flex-grow-1 overflow-auto" style={{ minHeight: 0 }}>
48
+ <Tabs
49
+ tabs={[
50
+ {
51
+ name: "User",
52
+ content: <UserSettingsTab />,
53
+ active: true,
54
+ },
55
+ {
56
+ name: "Appearance",
57
+ content: <AppearanceSettingsTab />,
58
+ },
59
+ {
60
+ name: "Advanced",
61
+ content: <AdvancedSettingsTab />,
62
+ },
63
+ ]}
64
+ />
65
+ </div>
66
+ <footer className="flex-shrink-0 border-top px-3 py-2 small text-muted">
67
+ Peers {getAppVersion()}
68
+ </footer>
67
69
  </div>
68
70
  );
69
71
  };
@@ -72,6 +74,7 @@ const UserSettingsTab: React.FC = () => {
72
74
  return (
73
75
  <>
74
76
  <ProfileSection />
77
+ <IdentityAccountSection />
75
78
  <AddDeviceSection />
76
79
  <LogoutSection />
77
80
  </>
@@ -109,31 +112,11 @@ const AdvancedSettingsTab: React.FC = () => {
109
112
  };
110
113
 
111
114
  const ProfileSection: React.FC = () => {
112
- const [deviceId] = useObservable(thisDeviceId);
113
- const deviceName = useObservableState<string>("");
114
- const savedDeviceName = useObservableState<string>("");
115
-
116
115
  const me = usePromise(async () => {
117
116
  const userContext = await getUserContext();
118
117
  return Users(userContext.userDataContext).initDoc(globals.me);
119
118
  });
120
119
 
121
- // Load device name
122
- useEffect(() => {
123
- const loadDevice = async () => {
124
- if (!deviceId) return;
125
- try {
126
- const userContext = await getUserContext();
127
- const device = await Devices(userContext.userDataContext).get(deviceId);
128
- deviceName((device?.name || "").slice(0, DISPLAY_NAME_MAX_LENGTH));
129
- savedDeviceName(device?.name || "");
130
- } catch (err) {
131
- console.error("Error loading device name:", err);
132
- }
133
- };
134
- loadDevice();
135
- }, [deviceId, deviceName, savedDeviceName]);
136
-
137
120
  const handleSave = async () => {
138
121
  try {
139
122
  // Save user profile (only when it actually changed). Pass me.toJS() — the
@@ -147,22 +130,6 @@ const ProfileSection: React.FC = () => {
147
130
  me.qs.name(updatedUser.name);
148
131
  me.q(0); // Reset change counter
149
132
  }
150
-
151
- // Save device name if changed
152
- if (deviceId && deviceName() !== savedDeviceName()) {
153
- const userContext = await getUserContext();
154
- const devicesTable = Devices(userContext.userDataContext);
155
- const device: IDevice = (await devicesTable.get(deviceId)) || {
156
- deviceId,
157
- userId: userContext.userId,
158
- firstSeen: new Date(),
159
- lastSeen: new Date(),
160
- trustLevel: TrustLevel.NewDevice,
161
- };
162
- device.name = deviceName() || undefined;
163
- await devicesTable.save(device);
164
- savedDeviceName(deviceName());
165
- }
166
133
  } catch (err) {
167
134
  console.error("Error saving profile:", err);
168
135
  alert(`Failed to save: ${(err as Error).message}`);
@@ -171,103 +138,63 @@ const ProfileSection: React.FC = () => {
171
138
 
172
139
  // Subscribe to changes
173
140
  useObservable(me?.q);
174
- useObservable(deviceName);
175
141
 
176
142
  if (!me) {
177
143
  return null;
178
144
  }
179
145
 
180
- const hasChanges = !!me.q() || deviceName() !== savedDeviceName();
146
+ const hasChanges = !!me.q();
181
147
 
182
148
  return (
183
149
  <>
184
150
  <div className="d-flex align-items-center mb-3">
185
- <h5 className="mb-0 me-auto">Settings</h5>
151
+ <h5 className="mb-0 me-auto">Profile</h5>
186
152
  <button className="btn btn-primary btn-sm" onClick={handleSave} disabled={!hasChanges}>
187
153
  Save
188
154
  </button>
189
155
  </div>
190
156
 
191
157
  <div className="mb-3">
192
- <small className="form-label">Display Name:</small>
158
+ <label className="form-label small" htmlFor="settings-display-name">
159
+ Display name
160
+ </label>
193
161
  <Input
162
+ id="settings-display-name"
194
163
  value={me.qs.name}
195
164
  className="form-control"
196
165
  maxLength={DISPLAY_NAME_MAX_LENGTH}
197
166
  placeholder="Enter your name"
198
167
  />
199
168
  </div>
200
-
201
- <div className="mb-3">
202
- <small className="form-label">
203
- Device Name:
204
- <Tooltip markdownContent="Give this device a friendly name to identify it in your network." />
205
- </small>
206
- <Input
207
- value={deviceName}
208
- className="form-control"
209
- maxLength={DISPLAY_NAME_MAX_LENGTH}
210
- placeholder="e.g., My Laptop"
211
- />
212
- </div>
213
-
214
- <div className="mb-3">
215
- <small className="form-label">
216
- User ID:
217
- <Tooltip markdownContent="This uniquely identifies you to all other users." />
218
- </small>
219
- <Input value={me.userId} className="form-control" disabled />
220
- </div>
221
-
222
- <div className="row">
223
- <div className="col-md-6 mb-3">
224
- <small className="form-label">
225
- Public Key:
226
- <Tooltip markdownContent="This is your public key that other users will use to verify your signatures." />
227
- </small>
228
- <Input value={me.publicKey} className="form-control form-control-sm" disabled />
229
- </div>
230
- <div className="col-md-6 mb-3">
231
- <small className="form-label">
232
- Public Box Key:
233
- <Tooltip markdownContent="This is the public key that other users can use to encrypt data so that only you can open it." />
234
- </small>
235
- <Input value={me.publicBoxKey} className="form-control form-control-sm" disabled />
236
- </div>
237
- </div>
238
-
239
- <div className="mb-3">
240
- <small className="form-label">
241
- Device ID:
242
- <Tooltip markdownContent="This uniquely identifies this device on Peers networks." />
243
- </small>
244
- <Input value={deviceId || ""} className="form-control form-control-sm" disabled />
245
- </div>
246
-
247
- <div className="mb-3">
248
- <small className="form-label">App Version:</small>
249
- <Input value={getAppVersion()} className="form-control form-control-sm" disabled />
250
- </div>
251
169
  </>
252
170
  );
253
171
  };
254
172
 
173
+ const IdentityAccountSection: React.FC = () => {
174
+ return (
175
+ <section className="mt-4 pt-3 border-top">
176
+ <h6>Identity account</h6>
177
+ <p className="small text-muted">
178
+ View account identifiers, security keys, this device’s ID, and Peers Services registration.
179
+ </p>
180
+ <a href={`#${IDENTITY_ACCOUNT_PATH}`} className="btn btn-outline-primary">
181
+ View Identity account
182
+ </a>
183
+ </section>
184
+ );
185
+ };
186
+
255
187
  const AddDeviceSection: React.FC = () => {
256
- const [open, setOpen] = useState(false);
257
188
  return (
258
189
  <section className="mt-4 pt-3 border-top">
259
190
  <h6>Add another device</h6>
260
191
  <p className="small text-muted">
261
- On the new device choose “Sign in from another device”, then type the code it shows here and
262
- approve. Account access is transferred only after you approve.
192
+ On the new device choose “Sign in from another device”, then type the code it shows on
193
+ Identity → Devices and approve. Account access is transferred only after you approve.
263
194
  </p>
264
- {open ? (
265
- <SourceDevicePairing onClose={() => setOpen(false)} />
266
- ) : (
267
- <button type="button" className="btn btn-outline-primary" onClick={() => setOpen(true)}>
268
- Add another device
269
- </button>
270
- )}
195
+ <a href={`#${IDENTITY_PAIR_PATH}`} className="btn btn-outline-primary">
196
+ Add another device
197
+ </a>
271
198
  </section>
272
199
  );
273
200
  };
@@ -728,7 +655,7 @@ const LogoutSection: React.FC = () => {
728
655
  "Make sure you have your User ID and Secret Key saved securely, or you will lose access to your account.",
729
656
  );
730
657
  if (!confirmed) return;
731
- await rpcServerCalls.logout();
658
+ await (await identityTools()).signOut();
732
659
  }}
733
660
  >
734
661
  Logout
@@ -4,12 +4,12 @@ import {
4
4
  newid,
5
5
  newKeys,
6
6
  openMessageWithPublicKey,
7
- rpcServerCalls,
8
7
  signMessageWithSecretKey,
9
8
  } from "@peers-app/peers-sdk";
10
9
  import { useState } from "react";
11
10
  import { useObservable } from "../hooks";
12
11
  import { DestinationDevicePairing } from "./device-pairing";
12
+ import { identityTools } from "./device-pairing/identity-ipc";
13
13
  import { colorMode } from "./settings/color-mode-dropdown";
14
14
 
15
15
  type SetupStep = "select-user-type" | "complete";
@@ -37,9 +37,9 @@ export const SetupUser = () => {
37
37
  const keys = newKeys();
38
38
  const _userId = newid();
39
39
 
40
- // Call setUserIdAndSecretKey. This triggers device init, which installs
41
- // peers-core into the personal context (S3-first, bundle fallback).
42
- await rpcServerCalls.setUserIdAndSecretKey(_userId, keys.secretKey);
40
+ // signIn triggers device init, which installs peers-core into the personal
41
+ // context (S3-first, bundle fallback).
42
+ await (await identityTools()).signIn({ userId: _userId, secretKey: keys.secretKey });
43
43
 
44
44
  // Success - reload the app (peers-services registration runs after Welcome modal)
45
45
  window.location.reload();
@@ -76,8 +76,7 @@ export const SetupUser = () => {
76
76
  throw new Error("Secret key validation failed");
77
77
  }
78
78
 
79
- // Call setUserIdAndSecretKey
80
- await rpcServerCalls.setUserIdAndSecretKey(userId, keys.secretKey);
79
+ await (await identityTools()).signIn({ userId, secretKey: keys.secretKey });
81
80
 
82
81
  // Success - reload the app
83
82
  window.location.reload();
@@ -1,88 +1,27 @@
1
1
  import {
2
- createContractConsumer,
3
- getHostContractTransport,
4
2
  getToolAccessControlContractDefinition,
5
- type IContractTransport,
3
+ hostContractConsumer,
4
+ type IHostContractConsumer,
6
5
  type IToolAccessControlConsumer,
7
- subscribeHostContractTransport,
8
- waitForHostContractTransport,
9
6
  } from "@peers-app/peers-sdk";
10
7
 
11
- const HOST_TRANSPORT_TIMEOUT_MS = 10_000;
12
-
13
- type CachedConsumer = {
14
- transport: IContractTransport;
15
- consumer: IToolAccessControlConsumer;
16
- };
17
-
18
- const cachedByContext = new Map<string, CachedConsumer>();
19
- const pendingByContext = new Map<string, Promise<IToolAccessControlConsumer>>();
20
-
21
- function disposeCachedConsumers(): void {
22
- for (const cached of cachedByContext.values()) {
23
- cached.consumer.dispose();
24
- }
25
- cachedByContext.clear();
26
- }
27
-
28
- subscribeHostContractTransport(
29
- (transport) => {
30
- if ([...cachedByContext.values()].some((cached) => cached.transport !== transport)) {
31
- disposeCachedConsumers();
32
- pendingByContext.clear();
33
- }
34
- },
35
- { emitCurrent: false },
36
- );
37
-
38
- async function loadConsumer(
39
- transport: IContractTransport,
40
- dataContextId: string,
41
- ): Promise<IToolAccessControlConsumer> {
42
- cachedByContext.get(dataContextId)?.consumer.dispose();
43
- cachedByContext.delete(dataContextId);
44
- const consumer = createContractConsumer(getToolAccessControlContractDefinition(), transport, {
45
- dataContextId,
46
- }) as unknown as IToolAccessControlConsumer;
47
- try {
48
- await consumer.loadingPromise;
49
- if (getHostContractTransport() !== transport) {
50
- consumer.dispose();
51
- pendingByContext.delete(dataContextId);
52
- return getToolAccessControlConsumer(dataContextId);
53
- }
54
- cachedByContext.set(dataContextId, { transport, consumer });
55
- return consumer;
56
- } catch (error) {
57
- consumer.dispose();
58
- throw error;
59
- }
60
- }
8
+ const consumers = new Map<string, IHostContractConsumer<IToolAccessControlConsumer>>();
61
9
 
62
10
  /**
63
11
  * Return a typed local-host consumer for Tool Access Control in `dataContextId`.
12
+ *
13
+ * One consumer is cached per data context and rebuilt when the host transport changes.
64
14
  */
65
15
  export async function getToolAccessControlConsumer(
66
16
  dataContextId: string,
67
17
  ): Promise<IToolAccessControlConsumer> {
68
- const transport =
69
- getHostContractTransport() ??
70
- (await waitForHostContractTransport({ timeoutMs: HOST_TRANSPORT_TIMEOUT_MS }));
71
- const cached = cachedByContext.get(dataContextId);
72
- if (cached?.transport === transport) {
73
- return cached.consumer;
74
- }
75
- const pending = pendingByContext.get(dataContextId);
76
- if (pending) {
77
- return pending;
78
- }
79
- const loadPromise = loadConsumer(transport, dataContextId);
80
- pendingByContext.set(dataContextId, loadPromise);
81
- try {
82
- return await loadPromise;
83
- } finally {
84
- if (pendingByContext.get(dataContextId) === loadPromise) {
85
- pendingByContext.delete(dataContextId);
86
- }
18
+ let handle = consumers.get(dataContextId);
19
+ if (!handle) {
20
+ handle = hostContractConsumer<IToolAccessControlConsumer>(
21
+ getToolAccessControlContractDefinition(),
22
+ { dataContextId: async () => dataContextId },
23
+ );
24
+ consumers.set(dataContextId, handle);
87
25
  }
26
+ return handle.get();
88
27
  }
@@ -5,7 +5,6 @@ import {
5
5
  getUserContext,
6
6
  hasShownWelcomeModal,
7
7
  type IDevice,
8
- rpcServerCalls,
9
8
  TrustLevel,
10
9
  thisDeviceId,
11
10
  Users,
@@ -14,6 +13,7 @@ import type React from "react";
14
13
  import { useEffect, useState } from "react";
15
14
  import { useObservable, usePromise } from "../hooks";
16
15
  import { persistPeersServicesRegistration } from "../peers-services-registration";
16
+ import { identityTools } from "./device-pairing/identity-ipc";
17
17
 
18
18
  interface WelcomeModalProps {
19
19
  colorMode: string;
@@ -22,8 +22,8 @@ interface WelcomeModalProps {
22
22
 
23
23
  async function tryRegisterPeersServicesAfterWelcome(): Promise<void> {
24
24
  try {
25
- const token = await rpcServerCalls.registerWithPeersServices();
26
- persistPeersServicesRegistration(token);
25
+ const { token } = await (await identityTools()).registerWithPeersServices();
26
+ await persistPeersServicesRegistration(token);
27
27
  } catch (err: unknown) {
28
28
  console.warn("Peers services registration after welcome modal:", err);
29
29
  }
@@ -0,0 +1,9 @@
1
+ import type { IAppNav } from "@peers-app/peers-sdk";
2
+
3
+ /** Identity system app: activity, people, groups, devices, and account. */
4
+ export const identityApp: IAppNav = {
5
+ name: "Identity",
6
+ displayName: "Identity",
7
+ iconClassName: "bi-person-badge",
8
+ navigationPath: "identity",
9
+ };
@@ -1,13 +1,10 @@
1
1
  import type { IAppNav } from "@peers-app/peers-sdk";
2
2
  import type { IPackageWithNavs } from "../ui-router/routes-loader";
3
3
 
4
- export { accountApp } from "./account.app";
5
4
  export { assistantsApp } from "./assistants.app";
6
5
  export { consoleLogsApp } from "./console-logs.app";
7
- export { contactsApp } from "./contacts.app";
8
6
  export { dataExplorerApp } from "./data-explorer.app";
9
- export { groupsApp } from "./groups.app";
10
- export { invitesApp } from "./invites.app";
7
+ export { identityApp } from "./identity.app";
11
8
  export { mobileSettingsApp } from "./mobile-settings.app";
12
9
  export { networkViewerApp } from "./network-viewer.app";
13
10
  export { packagesApp } from "./packages.app";
@@ -20,13 +17,10 @@ export { typesApp } from "./types.app";
20
17
  export { variablesApp } from "./variables.app";
21
18
  export { workflowsApp } from "./workflows.app";
22
19
 
23
- import { accountApp } from "./account.app";
24
20
  import { assistantsApp } from "./assistants.app";
25
21
  import { consoleLogsApp } from "./console-logs.app";
26
- import { contactsApp } from "./contacts.app";
27
22
  import { dataExplorerApp } from "./data-explorer.app";
28
- import { groupsApp } from "./groups.app";
29
- import { invitesApp } from "./invites.app";
23
+ import { identityApp } from "./identity.app";
30
24
  import { mobileSettingsApp } from "./mobile-settings.app";
31
25
  import { networkViewerApp } from "./network-viewer.app";
32
26
  import { packagesApp } from "./packages.app";
@@ -62,12 +56,9 @@ export const systemApps: IAppNav[] = [
62
56
  workflowsApp,
63
57
  assistantsApp,
64
58
  packagesApp,
65
- contactsApp,
66
- groupsApp,
67
- invitesApp,
59
+ identityApp,
68
60
 
69
61
  // User & Settings Apps
70
- accountApp,
71
62
  settingsApp,
72
63
  // Mobile Settings (only in React Native)
73
64
  ...(isReactNative() ? [mobileSettingsApp] : []),
@@ -2,6 +2,7 @@ import type { INetworkConnectionInfo, INetworkStatusSummary } from "@peers-app/p
2
2
  import { TrustLevel } from "@peers-app/peers-sdk";
3
3
  import { act, fireEvent, render, waitFor } from "@testing-library/react";
4
4
  import moment from "moment";
5
+ import { IDENTITY_ACCEPT_PATH, IDENTITY_ACCOUNT_PATH } from "../screens/identity/identity-routes";
5
6
  import {
6
7
  connectionIdentityLabel,
7
8
  formatRelativeSyncTime,
@@ -496,6 +497,27 @@ describe("NetworkStatusIndicator", () => {
496
497
  });
497
498
  });
498
499
 
500
+ it("offers shortcuts to use an invite and open the Identity account", () => {
501
+ mockViewState = { availability: "ready", status: status({ state: "synced" }) };
502
+ const view = render(<NetworkStatusIndicator />);
503
+
504
+ fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
505
+ expect(
506
+ view.getByRole("button", { name: "Use Invite" }).querySelector(".bi-qr-code"),
507
+ ).toBeTruthy();
508
+ expect(
509
+ view.getByRole("button", { name: "Identity" }).querySelector(".bi-person-badge"),
510
+ ).toBeTruthy();
511
+ fireEvent.click(view.getByRole("button", { name: "Use Invite" }));
512
+ expect(goToTabPath).toHaveBeenCalledWith(IDENTITY_ACCEPT_PATH);
513
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
514
+
515
+ fireEvent.click(view.getByRole("button", { name: /Network synced/ }));
516
+ fireEvent.click(view.getByRole("button", { name: "Identity" }));
517
+ expect(goToTabPath).toHaveBeenCalledWith(IDENTITY_ACCOUNT_PATH);
518
+ expect(view.queryByRole("dialog", { name: "Network status details" })).toBeNull();
519
+ });
520
+
499
521
  it("updates on rerender and opens details before navigating to Network Viewer", async () => {
500
522
  const view = render(<NetworkStatusIndicator />);
501
523
  expect(view.getByRole("button", { name: /loading/ })).toBeTruthy();