@peers-app/peers-ui 0.27.3 → 0.27.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/screens/network-viewer/group-details-modal.d.ts +4 -0
- package/dist/screens/network-viewer/group-details-modal.js +6 -46
- package/dist/screens/network-viewer/network-viewer-ipc.d.ts +3 -2
- package/dist/screens/network-viewer/network-viewer-ipc.js +6 -0
- package/dist/screens/network-viewer/sync-recovery.js +103 -1
- package/dist/screens/variables/variable-details.js +2 -1
- package/dist/tabs-layout/network-status-indicator.js +26 -12
- package/dist/tabs-layout/use-network-status.d.ts +6 -1
- package/dist/tabs-layout/use-network-status.js +27 -4
- package/package.json +9 -3
- package/.claude/settings.local.json +0 -34
- package/.github/README.md +0 -52
- package/.github/workflows/delete-all-artifacts.yml +0 -49
- package/.github/workflows/publish.yml +0 -51
- package/babel.config.js +0 -7
- package/biome.json +0 -198
- package/dist/app-updates/app-updates-consumer.test.d.ts +0 -1
- package/dist/app-updates/app-updates-consumer.test.js +0 -56
- package/dist/app-updates/app-updates-settings.test.d.ts +0 -1
- package/dist/app-updates/app-updates-settings.test.js +0 -93
- package/dist/app-updates/remote-app-update-panel.test.d.ts +0 -1
- package/dist/app-updates/remote-app-update-panel.test.js +0 -165
- package/dist/components/access-level-field.test.d.ts +0 -1
- package/dist/components/access-level-field.test.js +0 -28
- package/dist/components/message-logs/tool-access-approval-card.test.d.ts +0 -1
- package/dist/components/message-logs/tool-access-approval-card.test.js +0 -102
- package/dist/components/responsive-section-nav.test.d.ts +0 -1
- package/dist/components/responsive-section-nav.test.js +0 -45
- package/dist/globals-hash.test.d.ts +0 -1
- package/dist/globals-hash.test.js +0 -16
- package/dist/hooks/use-observable-state.test.d.ts +0 -1
- package/dist/hooks/use-observable-state.test.js +0 -64
- package/dist/hooks/use-observable.test.d.ts +0 -1
- package/dist/hooks/use-observable.test.js +0 -191
- package/dist/hooks/use-rows.test.d.ts +0 -1
- package/dist/hooks/use-rows.test.js +0 -175
- package/dist/hooks/use-tool.test.d.ts +0 -1
- package/dist/hooks/use-tool.test.js +0 -190
- package/dist/host-shell/host-shell-consumer.test.d.ts +0 -1
- package/dist/host-shell/host-shell-consumer.test.js +0 -28
- package/dist/root-layout/root-layout-host.test.d.ts +0 -1
- package/dist/root-layout/root-layout-host.test.js +0 -86
- package/dist/screens/account/account-screen.test.d.ts +0 -1
- package/dist/screens/account/account-screen.test.js +0 -51
- package/dist/screens/assistants/assistant-config.test.d.ts +0 -1
- package/dist/screens/assistants/assistant-config.test.js +0 -57
- package/dist/screens/contacts/contact-list.test.d.ts +0 -1
- package/dist/screens/contacts/contact-list.test.js +0 -48
- package/dist/screens/device-pairing/device-pairing.test.d.ts +0 -1
- package/dist/screens/device-pairing/device-pairing.test.js +0 -119
- package/dist/screens/devices/devices-list.test.d.ts +0 -1
- package/dist/screens/devices/devices-list.test.js +0 -53
- package/dist/screens/groups/group-list.test.d.ts +0 -1
- package/dist/screens/groups/group-list.test.js +0 -38
- package/dist/screens/identity/identity-routes.test.d.ts +0 -1
- package/dist/screens/identity/identity-routes.test.js +0 -51
- package/dist/screens/identity/identity-screen.test.d.ts +0 -1
- package/dist/screens/identity/identity-screen.test.js +0 -103
- package/dist/screens/invites/accept-invite.test.d.ts +0 -1
- package/dist/screens/invites/accept-invite.test.js +0 -103
- package/dist/screens/invites/activity-view.test.d.ts +0 -1
- package/dist/screens/invites/activity-view.test.js +0 -108
- package/dist/screens/invites/invite-classify.test.d.ts +0 -1
- package/dist/screens/invites/invite-classify.test.js +0 -115
- package/dist/screens/network-viewer/network-viewer-ipc.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer-ipc.test.js +0 -61
- package/dist/screens/network-viewer/network-viewer.section.test.d.ts +0 -1
- package/dist/screens/network-viewer/network-viewer.section.test.js +0 -63
- package/dist/screens/network-viewer/sync-recovery.test.d.ts +0 -1
- package/dist/screens/network-viewer/sync-recovery.test.js +0 -104
- package/dist/screens/packages/package-helpers.test.d.ts +0 -1
- package/dist/screens/packages/package-helpers.test.js +0 -53
- package/dist/screens/packages/package-runtime-status.test.d.ts +0 -1
- package/dist/screens/packages/package-runtime-status.test.js +0 -50
- package/dist/screens/packages/packages-ipc.test.d.ts +0 -1
- package/dist/screens/packages/packages-ipc.test.js +0 -43
- package/dist/screens/settings/settings-page.test.d.ts +0 -1
- package/dist/screens/settings/settings-page.test.js +0 -19
- package/dist/screens/tools/tool-access-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-access-ipc.test.js +0 -40
- package/dist/screens/tools/tool-catalog-ipc.test.d.ts +0 -1
- package/dist/screens/tools/tool-catalog-ipc.test.js +0 -52
- package/dist/tabs-layout/network-status-indicator.test.d.ts +0 -1
- package/dist/tabs-layout/network-status-indicator.test.js +0 -665
- package/dist/tabs-layout/tabs-state.test.d.ts +0 -1
- package/dist/tabs-layout/tabs-state.test.js +0 -110
- package/dist/tabs-layout/use-network-status.test.d.ts +0 -1
- package/dist/tabs-layout/use-network-status.test.js +0 -45
- package/dist/ui-router/ui-loader.test.d.ts +0 -1
- package/dist/ui-router/ui-loader.test.js +0 -91
- package/jest.config.js +0 -25
- package/release-guard.js +0 -10
- package/src/app-updates/app-update-controls.tsx +0 -73
- package/src/app-updates/app-updates-consumer.test.ts +0 -83
- package/src/app-updates/app-updates-consumer.ts +0 -121
- package/src/app-updates/app-updates-settings.test.tsx +0 -109
- package/src/app-updates/app-updates-settings.tsx +0 -136
- package/src/app-updates/remote-app-update-panel.test.tsx +0 -205
- package/src/app-updates/remote-app-update-panel.tsx +0 -210
- package/src/app-updates/use-app-update-status.ts +0 -16
- package/src/command-palette/command-palette-ui.tsx +0 -564
- package/src/command-palette/command-palette.ts +0 -242
- package/src/components/access-level-field.test.tsx +0 -32
- package/src/components/access-level-field.tsx +0 -68
- package/src/components/checkbox.tsx +0 -25
- package/src/components/group-switcher.tsx +0 -526
- package/src/components/input-date.tsx +0 -29
- package/src/components/input-number.tsx +0 -83
- package/src/components/input.tsx +0 -25
- package/src/components/inverse-lazy-list.tsx +0 -107
- package/src/components/io-schema-values.tsx +0 -103
- package/src/components/io-schema.tsx +0 -259
- package/src/components/lazy-list.tsx +0 -115
- package/src/components/list-screen.tsx +0 -126
- package/src/components/loading-indicator.tsx +0 -7
- package/src/components/markdown-editor/autolink-plugin.tsx +0 -29
- package/src/components/markdown-editor/editor-inline.tsx +0 -9
- package/src/components/markdown-editor/editor.tsx +0 -158
- package/src/components/markdown-editor/markdown-plugin.tsx +0 -202
- package/src/components/markdown-editor/mention-node.ts +0 -204
- package/src/components/markdown-editor/mentions-plugin.tsx +0 -354
- package/src/components/markdown-editor/move-line-plugin.tsx +0 -186
- package/src/components/markdown-editor/select-line-boundary-plugin.tsx +0 -95
- package/src/components/markdown-editor/theme.ts +0 -46
- package/src/components/markdown-editor/toolbar.tsx +0 -269
- package/src/components/markdown-with-mentions.tsx +0 -198
- package/src/components/message-logs/message-logs.tsx +0 -523
- package/src/components/message-logs/tool-access-approval-card.test.tsx +0 -127
- package/src/components/message-logs/tool-access-approval-card.tsx +0 -178
- package/src/components/messages/avatar.tsx +0 -171
- package/src/components/messages/channel-message-list.tsx +0 -189
- package/src/components/messages/channel-view.tsx +0 -92
- package/src/components/messages/message-compose.tsx +0 -192
- package/src/components/messages/message-display.tsx +0 -231
- package/src/components/messages/thread-message-list.tsx +0 -135
- package/src/components/qr-scanner.tsx +0 -180
- package/src/components/responsive-section-nav.test.tsx +0 -88
- package/src/components/responsive-section-nav.tsx +0 -92
- package/src/components/router.tsx +0 -175
- package/src/components/save-button.tsx +0 -134
- package/src/components/sortable-list.tsx +0 -130
- package/src/components/tabs.tsx +0 -76
- package/src/components/tooltip.tsx +0 -43
- package/src/components/trust-level-badge.tsx +0 -44
- package/src/components/trust-level-dropdown.tsx +0 -136
- package/src/components/typeahead.tsx +0 -357
- package/src/globals-hash.test.ts +0 -19
- package/src/globals.tsx +0 -226
- package/src/hooks/index.ts +0 -8
- package/src/hooks/use-host-contract.ts +0 -75
- package/src/hooks/use-observable-state.test.tsx +0 -101
- package/src/hooks/use-observable-state.ts +0 -29
- package/src/hooks/use-observable.test.tsx +0 -258
- package/src/hooks/use-observable.ts +0 -67
- package/src/hooks/use-on-screen.ts +0 -24
- package/src/hooks/use-promise.ts +0 -34
- package/src/hooks/use-rows.test.tsx +0 -196
- package/src/hooks/use-rows.ts +0 -120
- package/src/hooks/use-subscription.ts +0 -24
- package/src/hooks/use-tool.test.tsx +0 -225
- package/src/hooks/use-tool.ts +0 -116
- package/src/host-shell/host-shell-consumer.test.ts +0 -32
- package/src/host-shell/host-shell-consumer.ts +0 -53
- package/src/index.tsx +0 -42
- package/src/mention-configs.ts +0 -185
- package/src/operator-console/operator-console-state.ts +0 -119
- package/src/operator-console/operator-console.tsx +0 -233
- package/src/peers-services-registration.ts +0 -6
- package/src/root-layout/global-overlays.ts +0 -50
- package/src/root-layout/layout-picker.tsx +0 -47
- package/src/root-layout/layouts/full-screen-layout.tsx +0 -144
- package/src/root-layout/layouts/tabs-with-console-layout.tsx +0 -260
- package/src/root-layout/root-layout-host.test.tsx +0 -108
- package/src/root-layout/root-layout-host.tsx +0 -147
- package/src/root-layout/root-layout-registry.ts +0 -51
- package/src/root-layout/root-layout-state.ts +0 -55
- package/src/root-layout/root-layout.ts +0 -32
- package/src/screens/account/account-screen.test.tsx +0 -65
- package/src/screens/account/account-screen.tsx +0 -942
- package/src/screens/assistants/assistant-config.test.tsx +0 -54
- package/src/screens/assistants/assistant-config.tsx +0 -121
- package/src/screens/assistants/assistant-details.tsx +0 -89
- package/src/screens/assistants/assistant-info.tsx +0 -66
- package/src/screens/assistants/assistant-list.tsx +0 -145
- package/src/screens/assistants/assistant-tools.tsx +0 -82
- package/src/screens/console-logs/console-logs-list.tsx +0 -374
- package/src/screens/console-logs/log-display.tsx +0 -137
- package/src/screens/console-logs/log-filters.tsx +0 -64
- package/src/screens/console-logs/mobile-log-card.tsx +0 -161
- package/src/screens/console-logs/resizable-table-header.tsx +0 -110
- package/src/screens/contacts/contact-details.tsx +0 -123
- package/src/screens/contacts/contact-list.test.tsx +0 -59
- package/src/screens/contacts/contact-list.tsx +0 -286
- package/src/screens/contacts/index.ts +0 -2
- package/src/screens/data-explorer/data-explorer.tsx +0 -633
- package/src/screens/data-explorer/index.ts +0 -6
- package/src/screens/data-explorer/query-executor.tsx +0 -520
- package/src/screens/device-pairing/device-pairing.test.tsx +0 -143
- package/src/screens/device-pairing/device-pairing.tsx +0 -461
- package/src/screens/device-pairing/identity-ipc.ts +0 -25
- package/src/screens/device-pairing/index.ts +0 -2
- package/src/screens/device-pairing/use-device-pairing-state.ts +0 -15
- package/src/screens/devices/devices-list.test.tsx +0 -68
- package/src/screens/devices/devices-list.tsx +0 -365
- package/src/screens/devices/index.ts +0 -1
- package/src/screens/groups/group-details.tsx +0 -160
- package/src/screens/groups/group-invite-panel.tsx +0 -311
- package/src/screens/groups/group-list.test.tsx +0 -44
- package/src/screens/groups/group-list.tsx +0 -317
- package/src/screens/groups/group-members.tsx +0 -343
- package/src/screens/groups/index.ts +0 -3
- package/src/screens/identity/identity-routes.test.ts +0 -70
- package/src/screens/identity/identity-routes.ts +0 -301
- package/src/screens/identity/identity-screen.test.tsx +0 -138
- package/src/screens/identity/identity-screen.tsx +0 -282
- package/src/screens/identity/index.ts +0 -2
- package/src/screens/identity/invite-contact-panel.tsx +0 -45
- package/src/screens/identity/profile-invite.tsx +0 -93
- package/src/screens/invites/accept-invite.test.tsx +0 -115
- package/src/screens/invites/accept-invite.tsx +0 -318
- package/src/screens/invites/activity-view.test.tsx +0 -127
- package/src/screens/invites/activity-view.tsx +0 -162
- package/src/screens/invites/index.ts +0 -6
- package/src/screens/invites/invite-capture.ts +0 -104
- package/src/screens/invites/invite-classify.test.ts +0 -129
- package/src/screens/invites/invite-share.tsx +0 -104
- package/src/screens/invites/invites-ipc.ts +0 -25
- package/src/screens/invites/pending-invites-list.tsx +0 -256
- package/src/screens/network-viewer/connection-troubleshooter.tsx +0 -395
- package/src/screens/network-viewer/cpu-usage-graph.tsx +0 -294
- package/src/screens/network-viewer/device-details-modal.tsx +0 -339
- package/src/screens/network-viewer/group-details-modal.tsx +0 -421
- package/src/screens/network-viewer/index.ts +0 -8
- package/src/screens/network-viewer/network-viewer-ipc.test.ts +0 -72
- package/src/screens/network-viewer/network-viewer-ipc.ts +0 -39
- package/src/screens/network-viewer/network-viewer-section.ts +0 -12
- package/src/screens/network-viewer/network-viewer.section.test.tsx +0 -68
- package/src/screens/network-viewer/network-viewer.tsx +0 -708
- package/src/screens/network-viewer/remote-logs-panel.tsx +0 -190
- package/src/screens/network-viewer/sync-recovery.test.tsx +0 -114
- package/src/screens/network-viewer/sync-recovery.tsx +0 -461
- package/src/screens/network-viewer/usage-graph.tsx +0 -455
- package/src/screens/packages/local-folder-grants-panel.tsx +0 -290
- package/src/screens/packages/local-folders-ipc.ts +0 -28
- package/src/screens/packages/package-details.tsx +0 -166
- package/src/screens/packages/package-helpers.test.ts +0 -57
- package/src/screens/packages/package-helpers.ts +0 -137
- package/src/screens/packages/package-info.tsx +0 -354
- package/src/screens/packages/package-list.tsx +0 -519
- package/src/screens/packages/package-network-panel.tsx +0 -82
- package/src/screens/packages/package-new-local.tsx +0 -100
- package/src/screens/packages/package-runtime-status.test.tsx +0 -58
- package/src/screens/packages/package-runtime-status.tsx +0 -68
- package/src/screens/packages/package-versions.tsx +0 -298
- package/src/screens/packages/packages-ipc.test.ts +0 -47
- package/src/screens/packages/packages-ipc.ts +0 -125
- package/src/screens/peer-types/peer-type-details.tsx +0 -172
- package/src/screens/peer-types/peer-type-list.tsx +0 -64
- package/src/screens/search/global-search.tsx +0 -406
- package/src/screens/settings/color-mode-dropdown.tsx +0 -82
- package/src/screens/settings/settings-page.test.tsx +0 -24
- package/src/screens/settings/settings-page.tsx +0 -426
- package/src/screens/setup-user.tsx +0 -355
- package/src/screens/tools/tool-access-ipc.test.ts +0 -55
- package/src/screens/tools/tool-access-ipc.ts +0 -27
- package/src/screens/tools/tool-catalog-ipc.test.ts +0 -52
- package/src/screens/tools/tool-catalog-ipc.ts +0 -51
- package/src/screens/tools/tool-details.tsx +0 -52
- package/src/screens/tools/tool-info.tsx +0 -45
- package/src/screens/tools/tool-list.tsx +0 -106
- package/src/screens/tools/tool-schema.tsx +0 -44
- package/src/screens/tools/tool-test-details.tsx +0 -89
- package/src/screens/tools/tool-test-list.tsx +0 -73
- package/src/screens/variables/variable-details.tsx +0 -289
- package/src/screens/variables/variable-list.tsx +0 -110
- package/src/screens/welcome-modal.tsx +0 -294
- package/src/screens/workflows/workflow-details.tsx +0 -115
- package/src/screens/workflows/workflow-info.tsx +0 -22
- package/src/screens/workflows/workflow-instructions.tsx +0 -134
- package/src/screens/workflows/workflow-list.tsx +0 -117
- package/src/setupTests.ts +0 -38
- package/src/system-apps/assistants.app.ts +0 -7
- package/src/system-apps/console-logs.app.ts +0 -7
- package/src/system-apps/data-explorer.app.ts +0 -8
- package/src/system-apps/identity.app.ts +0 -9
- package/src/system-apps/index.ts +0 -77
- package/src/system-apps/mobile-settings.app.ts +0 -7
- package/src/system-apps/network-viewer.app.ts +0 -8
- package/src/system-apps/packages.app.ts +0 -7
- package/src/system-apps/search.app.ts +0 -8
- package/src/system-apps/settings.app.ts +0 -7
- package/src/system-apps/threads.app.ts +0 -7
- package/src/system-apps/tools.app.ts +0 -7
- package/src/system-apps/types.app.ts +0 -7
- package/src/system-apps/variables.app.ts +0 -7
- package/src/system-apps/workflows.app.ts +0 -7
- package/src/tabs-layout/network-status-indicator.test.tsx +0 -799
- package/src/tabs-layout/network-status-indicator.tsx +0 -756
- package/src/tabs-layout/tabs-layout.tsx +0 -857
- package/src/tabs-layout/tabs-state.test.ts +0 -152
- package/src/tabs-layout/tabs-state.ts +0 -587
- package/src/tabs-layout/use-network-status.test.tsx +0 -58
- package/src/tabs-layout/use-network-status.ts +0 -68
- package/src/ui-defaults/index.ts +0 -2
- package/src/ui-defaults/list-screen.tsx +0 -94
- package/src/ui-defaults/markdown-field.tsx +0 -87
- package/src/ui-router/routes-loader.ts +0 -193
- package/src/ui-router/ui-loader.test.tsx +0 -108
- package/src/ui-router/ui-loader.tsx +0 -606
- package/src/utils.ts +0 -95
- package/tsconfig.json +0 -18
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
import { copyToClipboard, type DevicePairingState, parsePairingCode } from "@peers-app/peers-sdk";
|
|
2
|
-
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import QRCode from "react-qr-code";
|
|
4
|
-
import { canScanQr, QrScanner } from "../../components/qr-scanner";
|
|
5
|
-
import { identityTools } from "./identity-ipc";
|
|
6
|
-
import { useDevicePairingState } from "./use-device-pairing-state";
|
|
7
|
-
|
|
8
|
-
function isPairingCode(text: string): boolean {
|
|
9
|
-
try {
|
|
10
|
-
parsePairingCode(text);
|
|
11
|
-
return true;
|
|
12
|
-
} catch {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const TERMINAL_PHASES: DevicePairingState["phase"][] = ["complete", "rejected", "error"];
|
|
18
|
-
|
|
19
|
-
function formatCountdown(expiresAt?: number): string {
|
|
20
|
-
if (!expiresAt) {
|
|
21
|
-
return "";
|
|
22
|
-
}
|
|
23
|
-
const seconds = Math.max(0, Math.ceil((expiresAt - Date.now()) / 1000));
|
|
24
|
-
return `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function detectDeviceType(): string {
|
|
28
|
-
return /Mobi|Android/i.test(navigator.userAgent) ? "mobile" : "browser";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function useCountdownClock(expiresAt?: number) {
|
|
32
|
-
const [, setClock] = useState(0);
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (!expiresAt) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
const timer = window.setInterval(() => setClock((value) => value + 1), 1000);
|
|
38
|
-
return () => window.clearInterval(timer);
|
|
39
|
-
}, [expiresAt]);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function PairingProgress(props: {
|
|
43
|
-
state: DevicePairingState;
|
|
44
|
-
messages: Partial<Record<string, string>>;
|
|
45
|
-
}) {
|
|
46
|
-
return (
|
|
47
|
-
<output className="text-center py-4 d-block">
|
|
48
|
-
<div className="spinner-border text-primary mb-3" aria-hidden="true" />
|
|
49
|
-
<p className="text-muted mb-0">{props.messages[props.state.phase] ?? "Working…"}</p>
|
|
50
|
-
</output>
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function ProgressWithCancel(props: {
|
|
55
|
-
state: DevicePairingState;
|
|
56
|
-
messages: Partial<Record<string, string>>;
|
|
57
|
-
onCancel: () => void;
|
|
58
|
-
}) {
|
|
59
|
-
return (
|
|
60
|
-
<>
|
|
61
|
-
<PairingProgress state={props.state} messages={props.messages} />
|
|
62
|
-
<div className="text-center">
|
|
63
|
-
<button type="button" className="btn btn-outline-secondary" onClick={props.onCancel}>
|
|
64
|
-
Cancel
|
|
65
|
-
</button>
|
|
66
|
-
</div>
|
|
67
|
-
</>
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function RemoteDeviceLabel({ state }: { state: DevicePairingState }) {
|
|
72
|
-
return (
|
|
73
|
-
<>
|
|
74
|
-
{state.remoteDevice?.deviceName || "Unnamed device"}
|
|
75
|
-
{state.remoteDevice?.deviceType ? ` (${state.remoteDevice.deviceType})` : ""}
|
|
76
|
-
</>
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function SourceApproval(props: {
|
|
81
|
-
state: DevicePairingState;
|
|
82
|
-
approving: boolean;
|
|
83
|
-
onApprove: () => void;
|
|
84
|
-
onReject: () => void;
|
|
85
|
-
onCancel: () => void;
|
|
86
|
-
}) {
|
|
87
|
-
return (
|
|
88
|
-
<div className="text-center" style={{ maxWidth: 440, margin: "0 auto" }}>
|
|
89
|
-
<h4>Add this device?</h4>
|
|
90
|
-
<p className="text-muted">
|
|
91
|
-
The code matched. Approving sends your account credentials to this device so it can sign in
|
|
92
|
-
as you.
|
|
93
|
-
</p>
|
|
94
|
-
<p className="fs-5 mb-4">
|
|
95
|
-
<RemoteDeviceLabel state={props.state} />
|
|
96
|
-
</p>
|
|
97
|
-
<div className="d-flex gap-2 justify-content-center">
|
|
98
|
-
<button
|
|
99
|
-
type="button"
|
|
100
|
-
className="btn btn-primary"
|
|
101
|
-
disabled={props.approving}
|
|
102
|
-
onClick={props.onApprove}
|
|
103
|
-
>
|
|
104
|
-
{props.approving ? "Approving…" : "Approve"}
|
|
105
|
-
</button>
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
className="btn btn-outline-danger"
|
|
109
|
-
disabled={props.approving}
|
|
110
|
-
onClick={props.onReject}
|
|
111
|
-
>
|
|
112
|
-
Reject
|
|
113
|
-
</button>
|
|
114
|
-
<button
|
|
115
|
-
type="button"
|
|
116
|
-
className="btn btn-outline-secondary"
|
|
117
|
-
disabled={props.approving}
|
|
118
|
-
onClick={props.onCancel}
|
|
119
|
-
>
|
|
120
|
-
Cancel
|
|
121
|
-
</button>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function TerminalPanel(props: { state: DevicePairingState; onClose: () => void }) {
|
|
128
|
-
const complete = props.state.phase === "complete";
|
|
129
|
-
return (
|
|
130
|
-
<div className="text-center" style={{ maxWidth: 440, margin: "0 auto" }}>
|
|
131
|
-
<output className={`alert d-block ${complete ? "alert-success" : "alert-warning"}`}>
|
|
132
|
-
{complete
|
|
133
|
-
? "Device added successfully."
|
|
134
|
-
: props.state.phase === "rejected"
|
|
135
|
-
? "Pairing was rejected. No credentials were installed."
|
|
136
|
-
: props.state.error || "Pairing could not be completed. No credentials were installed."}
|
|
137
|
-
</output>
|
|
138
|
-
<button type="button" className="btn btn-outline-secondary" onClick={props.onClose}>
|
|
139
|
-
{complete ? "Done" : "Back"}
|
|
140
|
-
</button>
|
|
141
|
-
</div>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function usePairingSessionLifecycle(state: DevicePairingState) {
|
|
146
|
-
const sessionId = useRef<string | undefined>(undefined);
|
|
147
|
-
const phase = useRef<DevicePairingState["phase"]>("idle");
|
|
148
|
-
const mounted = useRef(true);
|
|
149
|
-
|
|
150
|
-
useEffect(() => {
|
|
151
|
-
if (state.sessionId) {
|
|
152
|
-
// The host may rotate to a new session (destination code rotation); follow it.
|
|
153
|
-
sessionId.current = state.sessionId;
|
|
154
|
-
phase.current = state.phase;
|
|
155
|
-
}
|
|
156
|
-
}, [state.phase, state.sessionId]);
|
|
157
|
-
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
mounted.current = true;
|
|
160
|
-
return () => {
|
|
161
|
-
mounted.current = false;
|
|
162
|
-
const activeSessionId = sessionId.current;
|
|
163
|
-
if (activeSessionId && !["idle", ...TERMINAL_PHASES].includes(phase.current)) {
|
|
164
|
-
void identityTools()
|
|
165
|
-
.then((tools) => tools.cancel({ sessionId: activeSessionId }))
|
|
166
|
-
.catch(() => undefined);
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
return { sessionId, phase, mounted };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function useCeremonyActions(
|
|
175
|
-
lifecycle: ReturnType<typeof usePairingSessionLifecycle>,
|
|
176
|
-
onLeave: () => void,
|
|
177
|
-
setLocalError: (value: string) => void,
|
|
178
|
-
) {
|
|
179
|
-
const [approving, setApproving] = useState(false);
|
|
180
|
-
|
|
181
|
-
const act = async (action: "approve" | "reject") => {
|
|
182
|
-
const sessionId = lifecycle.sessionId.current;
|
|
183
|
-
if (!sessionId) {
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
setApproving(true);
|
|
187
|
-
try {
|
|
188
|
-
const tools = await identityTools();
|
|
189
|
-
if (action === "approve") {
|
|
190
|
-
await tools.approve({ sessionId });
|
|
191
|
-
} else {
|
|
192
|
-
await tools.reject({ sessionId });
|
|
193
|
-
}
|
|
194
|
-
} catch {
|
|
195
|
-
setLocalError("Could not update the pairing ceremony.");
|
|
196
|
-
} finally {
|
|
197
|
-
setApproving(false);
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
const cancel = async () => {
|
|
202
|
-
const sessionId = lifecycle.sessionId.current;
|
|
203
|
-
lifecycle.phase.current = "idle";
|
|
204
|
-
if (sessionId) {
|
|
205
|
-
await identityTools()
|
|
206
|
-
.then((tools) => tools.cancel({ sessionId }))
|
|
207
|
-
.catch(() => undefined);
|
|
208
|
-
}
|
|
209
|
-
onLeave();
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
return { approving, act, cancel };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
const SOURCE_PROGRESS_MESSAGES: Partial<Record<DevicePairingState["phase"], string>> = {
|
|
216
|
-
connecting: "Connecting to the new device…",
|
|
217
|
-
authenticating: "Verifying the pairing code…",
|
|
218
|
-
transferring: "Sending encrypted account credentials…",
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
/** Signed-in source flow opened from Settings: type the code shown on the new device, then approve. */
|
|
222
|
-
export function SourceDevicePairing(props: { onClose: () => void }) {
|
|
223
|
-
const state = useDevicePairingState();
|
|
224
|
-
const lifecycle = usePairingSessionLifecycle(state);
|
|
225
|
-
const [code, setCode] = useState("");
|
|
226
|
-
const [scanning, setScanning] = useState(false);
|
|
227
|
-
const [starting, setStarting] = useState(false);
|
|
228
|
-
const [localError, setLocalError] = useState("");
|
|
229
|
-
const { approving, act, cancel } = useCeremonyActions(lifecycle, props.onClose, setLocalError);
|
|
230
|
-
useCountdownClock(state.expiresAt);
|
|
231
|
-
|
|
232
|
-
const start = async (rawCode: string = code) => {
|
|
233
|
-
setLocalError("");
|
|
234
|
-
setStarting(true);
|
|
235
|
-
try {
|
|
236
|
-
const tools = await identityTools();
|
|
237
|
-
const result = await tools.startSource({ code: rawCode.trim() });
|
|
238
|
-
lifecycle.sessionId.current = result.sessionId;
|
|
239
|
-
lifecycle.phase.current = result.phase;
|
|
240
|
-
if (!lifecycle.mounted.current && result.sessionId) {
|
|
241
|
-
await tools.cancel({ sessionId: result.sessionId }).catch(() => undefined);
|
|
242
|
-
}
|
|
243
|
-
} catch {
|
|
244
|
-
setLocalError("That code was not recognized or the new device is no longer waiting.");
|
|
245
|
-
} finally {
|
|
246
|
-
setStarting(false);
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
if (TERMINAL_PHASES.includes(state.phase)) {
|
|
251
|
-
return <TerminalPanel state={state} onClose={props.onClose} />;
|
|
252
|
-
}
|
|
253
|
-
if (state.phase === "waiting-for-approval") {
|
|
254
|
-
return (
|
|
255
|
-
<SourceApproval
|
|
256
|
-
state={state}
|
|
257
|
-
approving={approving}
|
|
258
|
-
onApprove={() => void act("approve")}
|
|
259
|
-
onReject={() => void act("reject")}
|
|
260
|
-
onCancel={() => void cancel()}
|
|
261
|
-
/>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
if (
|
|
265
|
-
state.phase === "connecting" ||
|
|
266
|
-
state.phase === "authenticating" ||
|
|
267
|
-
state.phase === "transferring"
|
|
268
|
-
) {
|
|
269
|
-
return (
|
|
270
|
-
<ProgressWithCancel
|
|
271
|
-
state={state}
|
|
272
|
-
messages={SOURCE_PROGRESS_MESSAGES}
|
|
273
|
-
onCancel={() => void cancel()}
|
|
274
|
-
/>
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (scanning) {
|
|
278
|
-
return (
|
|
279
|
-
<div className="text-center" style={{ maxWidth: 440, margin: "0 auto" }}>
|
|
280
|
-
<p className="text-muted">Point the camera at the code on the new device.</p>
|
|
281
|
-
<QrScanner
|
|
282
|
-
accept={isPairingCode}
|
|
283
|
-
onScan={(scanned) => {
|
|
284
|
-
setScanning(false);
|
|
285
|
-
setCode(scanned);
|
|
286
|
-
void start(scanned);
|
|
287
|
-
}}
|
|
288
|
-
onError={(message) => {
|
|
289
|
-
setScanning(false);
|
|
290
|
-
setLocalError(message);
|
|
291
|
-
}}
|
|
292
|
-
/>
|
|
293
|
-
<button
|
|
294
|
-
type="button"
|
|
295
|
-
className="btn btn-outline-secondary mt-3"
|
|
296
|
-
onClick={() => setScanning(false)}
|
|
297
|
-
>
|
|
298
|
-
Type it instead
|
|
299
|
-
</button>
|
|
300
|
-
</div>
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
return (
|
|
304
|
-
<form
|
|
305
|
-
className="text-center"
|
|
306
|
-
style={{ maxWidth: 440, margin: "0 auto" }}
|
|
307
|
-
onSubmit={(event) => {
|
|
308
|
-
event.preventDefault();
|
|
309
|
-
void start();
|
|
310
|
-
}}
|
|
311
|
-
>
|
|
312
|
-
<p className="text-muted">
|
|
313
|
-
On the new device, open Peers and choose <strong>Sign in from another device</strong>. Scan
|
|
314
|
-
the code it shows, or type it here.
|
|
315
|
-
</p>
|
|
316
|
-
{canScanQr() ? (
|
|
317
|
-
<button
|
|
318
|
-
type="button"
|
|
319
|
-
className="btn btn-outline-primary mb-3"
|
|
320
|
-
disabled={starting}
|
|
321
|
-
onClick={() => setScanning(true)}
|
|
322
|
-
>
|
|
323
|
-
<i className="bi bi-qr-code-scan me-1" />
|
|
324
|
-
Scan code
|
|
325
|
-
</button>
|
|
326
|
-
) : null}
|
|
327
|
-
<input
|
|
328
|
-
className="form-control form-control-lg font-monospace text-center mb-3"
|
|
329
|
-
placeholder="7-guitar-revenge-tunnel"
|
|
330
|
-
value={code}
|
|
331
|
-
onChange={(event) => setCode(event.target.value)}
|
|
332
|
-
autoComplete="off"
|
|
333
|
-
autoCapitalize="none"
|
|
334
|
-
spellCheck={false}
|
|
335
|
-
aria-label="Pairing code"
|
|
336
|
-
disabled={starting}
|
|
337
|
-
/>
|
|
338
|
-
{localError ? <div className="alert alert-danger">{localError}</div> : null}
|
|
339
|
-
<div className="d-flex gap-2 justify-content-center">
|
|
340
|
-
<button
|
|
341
|
-
type="submit"
|
|
342
|
-
className="btn btn-primary"
|
|
343
|
-
disabled={starting || code.trim().split(/[\s-]+/).length < 4}
|
|
344
|
-
>
|
|
345
|
-
{starting ? "Connecting…" : "Connect"}
|
|
346
|
-
</button>
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
className="btn btn-outline-secondary"
|
|
350
|
-
disabled={starting}
|
|
351
|
-
onClick={props.onClose}
|
|
352
|
-
>
|
|
353
|
-
Cancel
|
|
354
|
-
</button>
|
|
355
|
-
</div>
|
|
356
|
-
</form>
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const DESTINATION_PROGRESS_MESSAGES: Partial<Record<DevicePairingState["phase"], string>> = {
|
|
361
|
-
idle: "Preparing a pairing code…",
|
|
362
|
-
connecting: "Connecting to your other device…",
|
|
363
|
-
authenticating: "Verifying the pairing code…",
|
|
364
|
-
"waiting-for-approval": "Waiting for approval on your other device…",
|
|
365
|
-
transferring: "Installing encrypted account credentials…",
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
function ShowingCode(props: { state: DevicePairingState; onCancel: () => void }) {
|
|
369
|
-
const code = props.state.code ?? "";
|
|
370
|
-
return (
|
|
371
|
-
<div className="text-center" style={{ maxWidth: 480, margin: "0 auto" }}>
|
|
372
|
-
<h3>Sign in from another device</h3>
|
|
373
|
-
<p className="text-muted">
|
|
374
|
-
On a device already signed in to Peers, open{" "}
|
|
375
|
-
<strong>Identity → Devices → Add a device</strong> and type this code or scan it.
|
|
376
|
-
</p>
|
|
377
|
-
{props.state.notice ? <div className="alert alert-warning">{props.state.notice}</div> : null}
|
|
378
|
-
<output
|
|
379
|
-
className="d-block fs-3 font-monospace text-primary mb-3"
|
|
380
|
-
style={{ letterSpacing: "0.04em", wordBreak: "break-word" }}
|
|
381
|
-
aria-label="Pairing code"
|
|
382
|
-
>
|
|
383
|
-
{code}
|
|
384
|
-
</output>
|
|
385
|
-
<div className="d-inline-block bg-white rounded p-3 mb-3">
|
|
386
|
-
<QRCode value={code} size={200} aria-label="Pairing code QR" />
|
|
387
|
-
</div>
|
|
388
|
-
<p className="small text-muted">Expires in {formatCountdown(props.state.expiresAt)}</p>
|
|
389
|
-
<div className="d-flex gap-2 justify-content-center">
|
|
390
|
-
<button
|
|
391
|
-
type="button"
|
|
392
|
-
className="btn btn-outline-primary"
|
|
393
|
-
onClick={() => void copyToClipboard(code)}
|
|
394
|
-
>
|
|
395
|
-
Copy code
|
|
396
|
-
</button>
|
|
397
|
-
<button type="button" className="btn btn-outline-secondary" onClick={props.onCancel}>
|
|
398
|
-
Cancel
|
|
399
|
-
</button>
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/** Signed-out destination flow: shows a code and QR, then installs credentials after approval. */
|
|
406
|
-
export function DestinationDevicePairing(props: { onBack: () => void }) {
|
|
407
|
-
const state = useDevicePairingState();
|
|
408
|
-
const lifecycle = usePairingSessionLifecycle(state);
|
|
409
|
-
const [localError, setLocalError] = useState("");
|
|
410
|
-
const started = useRef(false);
|
|
411
|
-
const { cancel } = useCeremonyActions(lifecycle, props.onBack, setLocalError);
|
|
412
|
-
useCountdownClock(state.expiresAt);
|
|
413
|
-
|
|
414
|
-
useEffect(() => {
|
|
415
|
-
if (started.current) {
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
started.current = true;
|
|
419
|
-
void identityTools()
|
|
420
|
-
.then(async (tools) => {
|
|
421
|
-
const result = await tools.startDestination({ deviceType: detectDeviceType() });
|
|
422
|
-
lifecycle.sessionId.current = result.sessionId;
|
|
423
|
-
lifecycle.phase.current = result.phase;
|
|
424
|
-
if (!lifecycle.mounted.current && result.sessionId) {
|
|
425
|
-
await tools.cancel({ sessionId: result.sessionId }).catch(() => undefined);
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
.catch(() => setLocalError("Could not reach the pairing service. Check your connection."));
|
|
429
|
-
}, []);
|
|
430
|
-
|
|
431
|
-
useEffect(() => {
|
|
432
|
-
if (state.phase !== "complete") {
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
window.setTimeout(() => window.location.reload(), 750);
|
|
436
|
-
}, [state.phase]);
|
|
437
|
-
|
|
438
|
-
if (localError) {
|
|
439
|
-
return (
|
|
440
|
-
<div className="text-center" style={{ maxWidth: 440, margin: "0 auto" }}>
|
|
441
|
-
<div className="alert alert-danger">{localError}</div>
|
|
442
|
-
<button type="button" className="btn btn-outline-secondary" onClick={props.onBack}>
|
|
443
|
-
Back
|
|
444
|
-
</button>
|
|
445
|
-
</div>
|
|
446
|
-
);
|
|
447
|
-
}
|
|
448
|
-
if (TERMINAL_PHASES.includes(state.phase)) {
|
|
449
|
-
return <TerminalPanel state={state} onClose={props.onBack} />;
|
|
450
|
-
}
|
|
451
|
-
if (state.phase === "showing-code" && state.code) {
|
|
452
|
-
return <ShowingCode state={state} onCancel={() => void cancel()} />;
|
|
453
|
-
}
|
|
454
|
-
return (
|
|
455
|
-
<ProgressWithCancel
|
|
456
|
-
state={state}
|
|
457
|
-
messages={DESTINATION_PROGRESS_MESSAGES}
|
|
458
|
-
onCancel={() => void cancel()}
|
|
459
|
-
/>
|
|
460
|
-
);
|
|
461
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getIdentityContractDefinition,
|
|
3
|
-
hostContractConsumer,
|
|
4
|
-
type IHostContractConsumer,
|
|
5
|
-
type IIdentityConsumer,
|
|
6
|
-
} from "@peers-app/peers-sdk";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Cached local-host consumer for System Identity.
|
|
10
|
-
*
|
|
11
|
-
* Pairing and session tools resolve before sign-in, so this handle passes no data context.
|
|
12
|
-
*/
|
|
13
|
-
export const identityContract: IHostContractConsumer<IIdentityConsumer> = hostContractConsumer(
|
|
14
|
-
getIdentityContractDefinition(),
|
|
15
|
-
);
|
|
16
|
-
|
|
17
|
-
/** Return the typed local-host consumer for System Identity. */
|
|
18
|
-
export async function getIdentityConsumer(): Promise<IIdentityConsumer> {
|
|
19
|
-
return identityContract.get();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Shorthand for `(await getIdentityConsumer()).tools`. */
|
|
23
|
-
export async function identityTools(): Promise<IIdentityConsumer["tools"]> {
|
|
24
|
-
return (await getIdentityConsumer()).tools;
|
|
25
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { DevicePairingState } from "@peers-app/peers-sdk";
|
|
2
|
-
import { useObservable } from "../../hooks";
|
|
3
|
-
import { useHostContract } from "../../hooks/use-host-contract";
|
|
4
|
-
import { identityContract } from "./identity-ipc";
|
|
5
|
-
|
|
6
|
-
const IDLE_PAIRING_STATE: DevicePairingState = { phase: "idle" };
|
|
7
|
-
|
|
8
|
-
/** Subscribe to the host pairing ceremony. Idle until the identity consumer is ready. */
|
|
9
|
-
export function useDevicePairingState(): DevicePairingState {
|
|
10
|
-
const { consumer } = useHostContract(identityContract);
|
|
11
|
-
const [state] = useObservable<DevicePairingState>(
|
|
12
|
-
consumer?.observables.state ?? IDLE_PAIRING_STATE,
|
|
13
|
-
);
|
|
14
|
-
return state ?? IDLE_PAIRING_STATE;
|
|
15
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { type IDevice, TrustLevel } from "@peers-app/peers-sdk";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { DeviceRow, devicesForUser } from "./devices-list";
|
|
4
|
-
|
|
5
|
-
jest.mock("../../globals", () => ({
|
|
6
|
-
mainContentPath: jest.fn(),
|
|
7
|
-
}));
|
|
8
|
-
|
|
9
|
-
jest.mock("../../hooks", () => ({
|
|
10
|
-
useObservable: jest.fn(),
|
|
11
|
-
useObservableState: jest.fn(),
|
|
12
|
-
usePromise: jest.fn(),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
jest.mock("../device-pairing", () => ({
|
|
16
|
-
SourceDevicePairing: () => null,
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
jest.mock("../invites/activity-view", () => ({
|
|
20
|
-
PendingWorkLink: () => null,
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
jest.mock("../network-viewer/network-viewer-ipc", () => ({
|
|
24
|
-
getNetworkControlConsumer: jest.fn(),
|
|
25
|
-
getNetworkDiagnosticsConsumer: jest.fn(),
|
|
26
|
-
}));
|
|
27
|
-
|
|
28
|
-
function device(deviceId: string, userId: string, name: string): IDevice {
|
|
29
|
-
return {
|
|
30
|
-
deviceId,
|
|
31
|
-
userId,
|
|
32
|
-
name,
|
|
33
|
-
firstSeen: new Date("2026-01-01T00:00:00Z"),
|
|
34
|
-
lastSeen: new Date(),
|
|
35
|
-
trustLevel: TrustLevel.Trusted,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
describe("Devices identity list", () => {
|
|
40
|
-
it("filters out devices owned by other users", () => {
|
|
41
|
-
const mine = device("abcdefghijklmnopqrstuvwxy", "mine000000000000000000001", "Laptop");
|
|
42
|
-
const theirs = device("zyxwvutsrqponmlkjihgfedcb", "theirs000000000000000001", "Other");
|
|
43
|
-
|
|
44
|
-
expect(devicesForUser([mine, theirs], mine.userId)).toEqual([mine]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("shows the current-device and connection context without a destructive row action", () => {
|
|
48
|
-
const mine = device("abcdefghijklmnopqrstuvwxy", "mine000000000000000000001", "Laptop");
|
|
49
|
-
render(
|
|
50
|
-
<DeviceRow
|
|
51
|
-
device={mine}
|
|
52
|
-
isThisDevice
|
|
53
|
-
connection={
|
|
54
|
-
{
|
|
55
|
-
deviceId: mine.deviceId,
|
|
56
|
-
connectionState: "connected",
|
|
57
|
-
pathType: "direct",
|
|
58
|
-
} as never
|
|
59
|
-
}
|
|
60
|
-
/>,
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
expect(screen.getByText("Laptop")).toBeInTheDocument();
|
|
64
|
-
expect(screen.getByText("This device")).toBeInTheDocument();
|
|
65
|
-
expect(screen.getByText("Online")).toBeInTheDocument();
|
|
66
|
-
expect(screen.queryByRole("button", { name: /Forget/i })).not.toBeInTheDocument();
|
|
67
|
-
});
|
|
68
|
-
});
|