@dxos/shell 0.3.11-next.0fb359e → 0.3.11-next.e28df4f

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 (129) hide show
  1. package/dist/bundle/shell.mjs +36774 -0
  2. package/dist/bundle/shell.mjs.map +1 -0
  3. package/dist/bundle/shell.umd.js +210 -0
  4. package/dist/bundle/shell.umd.js.map +1 -0
  5. package/dist/bundle/style.css +1 -0
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +755 -438
  8. package/dist/lib/browser/react.mjs.map +4 -4
  9. package/dist/lib/browser/testing/index.mjs +35 -14
  10. package/dist/lib/browser/testing/index.mjs.map +3 -3
  11. package/dist/types/src/components/IdentityList/DeviceList.d.ts.map +1 -1
  12. package/dist/types/src/components/IdentityList/DeviceList.js +5 -3
  13. package/dist/types/src/components/IdentityList/DeviceList.js.map +1 -1
  14. package/dist/types/src/components/IdentityList/SpaceMemberList.js +1 -1
  15. package/dist/types/src/components/IdentityList/SpaceMemberList.js.map +1 -1
  16. package/dist/types/src/components/Panel/Action.d.ts.map +1 -1
  17. package/dist/types/src/components/Panel/Action.js +3 -18
  18. package/dist/types/src/components/Panel/Action.js.map +1 -1
  19. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -1
  20. package/dist/types/src/composites/Shell/Shell.js +15 -5
  21. package/dist/types/src/composites/Shell/Shell.js.map +1 -1
  22. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  23. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +24 -7
  24. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  25. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.d.ts +6 -0
  26. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.d.ts.map +1 -1
  27. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js +26 -3
  28. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js.map +1 -1
  29. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts +19 -8
  30. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts.map +1 -1
  31. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts +1 -1
  32. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts.map +1 -1
  33. package/dist/types/src/panels/IdentityPanel/identityMachine.js +12 -3
  34. package/dist/types/src/panels/IdentityPanel/identityMachine.js.map +1 -1
  35. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.d.ts +3 -0
  36. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.d.ts.map +1 -0
  37. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.js +26 -0
  38. package/dist/types/src/panels/IdentityPanel/steps/AgentForm.js.map +1 -0
  39. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.d.ts +10 -0
  40. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.d.ts.map +1 -0
  41. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.js +23 -0
  42. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.js.map +1 -0
  43. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  44. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +9 -1
  45. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  46. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.d.ts +0 -1
  47. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.d.ts.map +1 -1
  48. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js +2 -3
  49. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js.map +1 -1
  50. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.d.ts +16 -0
  51. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.d.ts.map +1 -0
  52. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js +40 -0
  53. package/dist/types/src/panels/IdentityPanel/steps/SignOutChooser.js.map +1 -0
  54. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts +3 -0
  55. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts.map +1 -1
  56. package/dist/types/src/panels/IdentityPanel/steps/index.js +3 -0
  57. package/dist/types/src/panels/IdentityPanel/steps/index.js.map +1 -1
  58. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts +15 -0
  59. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.d.ts.map +1 -0
  60. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js +78 -0
  61. package/dist/types/src/panels/IdentityPanel/useAgentHandlers.js.map +1 -0
  62. package/dist/types/src/panels/JoinPanel/JoinPanel.js +4 -3
  63. package/dist/types/src/panels/JoinPanel/JoinPanel.js.map +1 -1
  64. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts +1 -1
  65. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -1
  66. package/dist/types/src/panels/JoinPanel/joinMachine.js +15 -18
  67. package/dist/types/src/panels/JoinPanel/joinMachine.js.map +1 -1
  68. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js +3 -3
  69. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js.map +1 -1
  70. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.d.ts.map +1 -1
  71. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js +13 -6
  72. package/dist/types/src/panels/JoinPanel/steps/InvitationInput.js.map +1 -1
  73. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js +1 -1
  74. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js.map +1 -1
  75. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -1
  76. package/dist/types/src/panels/SpacePanel/SpacePanel.js +5 -3
  77. package/dist/types/src/panels/SpacePanel/SpacePanel.js.map +1 -1
  78. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -1
  79. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js +2 -2
  80. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js.map +1 -1
  81. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts +1 -0
  82. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts.map +1 -1
  83. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts +1 -0
  84. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts.map +1 -1
  85. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js +3 -1
  86. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js.map +1 -1
  87. package/dist/types/src/playwright/invitations-manager.d.ts +1 -0
  88. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  89. package/dist/types/src/playwright/invitations-manager.js +4 -1
  90. package/dist/types/src/playwright/invitations-manager.js.map +1 -1
  91. package/dist/types/src/playwright/invitations.spec.js +15 -0
  92. package/dist/types/src/playwright/invitations.spec.js.map +1 -1
  93. package/dist/types/src/testing/shell-manager.d.ts +5 -4
  94. package/dist/types/src/testing/shell-manager.d.ts.map +1 -1
  95. package/dist/types/src/testing/shell-manager.js +36 -12
  96. package/dist/types/src/testing/shell-manager.js.map +1 -1
  97. package/dist/types/src/translations/locales/en-US.d.ts +6 -0
  98. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  99. package/dist/types/src/translations/locales/en-US.js +7 -1
  100. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  101. package/package.json +22 -21
  102. package/src/components/IdentityList/DeviceList.tsx +7 -5
  103. package/src/components/IdentityList/SpaceMemberList.tsx +1 -1
  104. package/src/components/Panel/Action.tsx +4 -19
  105. package/src/composites/Shell/Shell.tsx +16 -3
  106. package/src/panels/IdentityPanel/IdentityPanel.stories.tsx +44 -2
  107. package/src/panels/IdentityPanel/IdentityPanel.tsx +35 -8
  108. package/src/panels/IdentityPanel/IdentityPanelProps.ts +27 -8
  109. package/src/panels/IdentityPanel/identityMachine.ts +13 -4
  110. package/src/panels/IdentityPanel/steps/AgentForm.tsx +61 -0
  111. package/src/panels/IdentityPanel/steps/DeviceManager.tsx +41 -0
  112. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +13 -4
  113. package/src/panels/IdentityPanel/steps/ProfileForm.tsx +1 -1
  114. package/src/panels/IdentityPanel/steps/SignOutChooser.tsx +84 -0
  115. package/src/panels/IdentityPanel/steps/index.ts +3 -0
  116. package/src/panels/IdentityPanel/useAgentHandlers.ts +88 -0
  117. package/src/panels/JoinPanel/JoinPanel.tsx +2 -2
  118. package/src/panels/JoinPanel/joinMachine.ts +16 -19
  119. package/src/panels/JoinPanel/steps/AdditionMethodChooser.tsx +3 -3
  120. package/src/panels/JoinPanel/steps/InvitationInput.tsx +7 -1
  121. package/src/panels/JoinPanel/steps/ResetIdentity.tsx +1 -1
  122. package/src/panels/SpacePanel/SpacePanel.stories.tsx +2 -1
  123. package/src/panels/SpacePanel/SpacePanel.tsx +4 -1
  124. package/src/panels/SpacePanel/SpacePanelProps.ts +1 -0
  125. package/src/panels/SpacePanel/steps/SpaceManager.tsx +4 -1
  126. package/src/playwright/invitations-manager.ts +5 -1
  127. package/src/playwright/invitations.spec.ts +19 -0
  128. package/src/testing/shell-manager.ts +40 -13
  129. package/src/translations/locales/en-US.ts +7 -1
@@ -6,7 +6,7 @@ import React, { useEffect, useMemo } from 'react';
6
6
  import { generateName } from '@dxos/display-name';
7
7
  import { log } from '@dxos/log';
8
8
  import { useClient } from '@dxos/react-client';
9
- import { type Identity, useIdentity } from '@dxos/react-client/halo';
9
+ import { type Identity, useIdentity, useDevices, useHaloInvitations } from '@dxos/react-client/halo';
10
10
  import { useInvitationStatus } from '@dxos/react-client/invitations';
11
11
  import type { CancellableInvitationObservable } from '@dxos/react-client/invitations';
12
12
  import { Avatar, DensityProvider, useId, useJdenticonHref, useTranslation } from '@dxos/react-ui';
@@ -17,7 +17,8 @@ import {
17
17
  type IdentityPanelProps,
18
18
  } from './IdentityPanelProps';
19
19
  import { useIdentityMachine } from './identityMachine';
20
- import { IdentityActionChooser, ProfileForm } from './steps';
20
+ import { AgentForm, DeviceManager, IdentityActionChooser, ProfileForm, SignOutChooser } from './steps';
21
+ import { useAgentHandlers } from './useAgentHandlers';
21
22
  import { Viewport, Heading, CloseButton } from '../../components';
22
23
  import { InvitationManager } from '../../steps';
23
24
 
@@ -31,7 +32,7 @@ const IdentityHeading = ({ titleId, title, identity, onDone }: IdentityPanelHead
31
32
  <Avatar.Frame classNames='block mbs-4 mbe-2 mli-auto chromatic-ignore'>
32
33
  <Avatar.Fallback href={fallbackHref} />
33
34
  </Avatar.Frame>
34
- <Avatar.Label classNames='block text-center font-light text-xl'>
35
+ <Avatar.Label classNames='block text-center font-light text-xl' data-testid='identityHeading.displayName'>
35
36
  {identity.profile?.displayName ?? generateName(identity.identityKey.toHex())}
36
37
  </Avatar.Label>
37
38
  </Avatar.Root>
@@ -45,6 +46,8 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
45
46
  titleId,
46
47
  activeView,
47
48
  onUpdateProfile,
49
+ onResetDevice,
50
+ onJoinNewIdentity,
48
51
  IdentityActionChooser: IdentityActionChooserComponent = IdentityActionChooser,
49
52
  InvitationManager: InvitationManagerComponent = InvitationManager,
50
53
  onDone,
@@ -53,13 +56,15 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
53
56
  const { t } = useTranslation('os');
54
57
  const title = useMemo(() => {
55
58
  switch (activeView) {
56
- case 'device manager':
59
+ case 'agent manager':
60
+ return 'Manage Agent';
57
61
  case 'device invitation manager':
58
- return t('choose devices label');
62
+ return t('choose add device label');
59
63
  default:
60
64
  return t('identity heading');
61
65
  }
62
66
  }, [activeView, t]);
67
+
63
68
  return (
64
69
  <DensityProvider density='fine'>
65
70
  <IdentityHeading {...{ identity, titleId, title, onDone }} />
@@ -75,6 +80,9 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
75
80
  invitationUrl={rest.createInvitationUrl(rest.invitationCode!)}
76
81
  />
77
82
  </Viewport.View>
83
+ <Viewport.View id='device manager' classNames={viewStyles}>
84
+ <DeviceManager active={activeView === 'device manager'} {...rest} />
85
+ </Viewport.View>
78
86
  <Viewport.View classNames={viewStyles} id='update profile form'>
79
87
  <ProfileForm
80
88
  send={rest.send}
@@ -83,7 +91,17 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
83
91
  onUpdateProfile={onUpdateProfile}
84
92
  />
85
93
  </Viewport.View>
86
- {/* <Viewport.View id='signing out'></Viewport.View> */}
94
+ <Viewport.View classNames={viewStyles} id='signing out'>
95
+ <SignOutChooser
96
+ send={rest.send}
97
+ active={activeView === 'signing out'}
98
+ onResetDevice={onResetDevice}
99
+ onJoinNewIdentity={onJoinNewIdentity}
100
+ />
101
+ </Viewport.View>
102
+ <Viewport.View classNames={viewStyles} id='agent manager'>
103
+ <AgentForm send={rest.send} active={activeView === 'agent manager'} {...rest} />
104
+ </Viewport.View>
87
105
  </Viewport.Views>
88
106
  </Viewport.Root>
89
107
  </DensityProvider>
@@ -105,7 +123,10 @@ export const IdentityPanel = ({
105
123
  }: IdentityPanelProps) => {
106
124
  const titleId = useId('identityPanel__heading', propsTitleId);
107
125
  const client = useClient();
126
+ const devices = useDevices();
108
127
  const identity = useIdentity();
128
+ const invitations = useHaloInvitations();
129
+ const agentProps = useAgentHandlers({ client, identity, invitations });
109
130
  if (!identity) {
110
131
  console.error('IdentityPanel rendered with no active identity.');
111
132
  return null;
@@ -126,10 +147,14 @@ export const IdentityPanel = ({
126
147
  return 'identity action chooser';
127
148
  case identityState.matches('managingDeviceInvitation'):
128
149
  return 'device invitation manager';
150
+ case identityState.matches('managingDevices'):
151
+ return 'device manager';
129
152
  case [{ managingProfile: 'idle' }, { managingProfile: 'pending' }].some(identityState.matches):
130
153
  return 'update profile form';
131
- // case identityState.matches('signingOut'):
132
- // return 'identity exit';
154
+ case identityState.matches('signingOut'):
155
+ return 'signing out';
156
+ case [{ managingAgent: 'idle' }, { managingAgent: 'pending' }].some(identityState.matches):
157
+ return 'agent manager';
133
158
  default:
134
159
  return 'identity action chooser';
135
160
  }
@@ -143,11 +168,13 @@ export const IdentityPanel = ({
143
168
  const implProps = {
144
169
  ...props,
145
170
  identity,
171
+ devices,
146
172
  activeView,
147
173
  send: identitySend,
148
174
  titleId,
149
175
  createInvitationUrl,
150
176
  onUpdateProfile,
177
+ ...agentProps,
151
178
  } satisfies IdentityPanelImplProps;
152
179
 
153
180
  return identityState.context.invitation ? (
@@ -2,20 +2,38 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type cloneElement } from 'react';
6
- import { type Event, type SingleOrArray } from 'xstate';
5
+ import type { cloneElement } from 'react';
6
+ import type { Event, SingleOrArray } from 'xstate';
7
7
 
8
- import type { Identity } from '@dxos/react-client/halo';
9
- import { type InvitationStatus } from '@dxos/react-client/invitations';
8
+ import type { Device, Identity } from '@dxos/react-client/halo';
9
+ import type { InvitationStatus } from '@dxos/react-client/invitations';
10
10
 
11
- import { type IdentityEvent } from './identityMachine';
12
- import { type InvitationManagerProps } from '../../steps';
11
+ import type { IdentityEvent } from './identityMachine';
12
+ import type { InvitationManagerProps } from '../../steps';
13
+
14
+ export type AgentFormProps = {
15
+ onAgentCreate?: () => Promise<void>;
16
+ onAgentDestroy?: () => Promise<void>;
17
+ agentStatus?: string;
18
+ agentActive?: boolean;
19
+ agentProviderDisabled?: boolean;
20
+ validationMessage?: string;
21
+ };
13
22
 
14
23
  export type IdentityPanelImplProps = {
15
24
  titleId: string;
16
- activeView: 'device manager' | 'update profile form' | 'device invitation manager' | 'identity action chooser';
25
+ activeView:
26
+ | 'device manager'
27
+ | 'agent manager'
28
+ | 'update profile form'
29
+ | 'device invitation manager'
30
+ | 'identity action chooser'
31
+ | 'signing out';
17
32
  identity: Identity;
33
+ devices: Device[];
18
34
  onUpdateProfile?: (profile: NonNullable<Identity['profile']>) => Promise<void>;
35
+ onResetDevice?: () => Promise<void>;
36
+ onJoinNewIdentity?: () => void;
19
37
  createInvitationUrl: (invitationCode: string) => string;
20
38
  send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
21
39
  onDone?: () => void;
@@ -23,7 +41,8 @@ export type IdentityPanelImplProps = {
23
41
  invitationUrl?: string;
24
42
  IdentityActionChooser?: React.FC<IdentityPanelStepProps>;
25
43
  InvitationManager?: React.FC<InvitationManagerProps>;
26
- } & Partial<InvitationStatus>;
44
+ } & Partial<InvitationStatus> &
45
+ Partial<AgentFormProps>;
27
46
 
28
47
  export type IdentityPanelProps = Partial<Omit<IdentityPanelImplProps, 'send' | 'activeView' | 'identity'>>;
29
48
 
@@ -19,7 +19,7 @@ type IdentityMachineContext = {
19
19
  };
20
20
 
21
21
  type IdentityChooseActionEvent = {
22
- type: 'chooseDevices' | 'chooseProfile' | /* 'chooseSignOut' | */ 'unchooseAction';
22
+ type: 'chooseDevices' | 'chooseAgent' | 'chooseProfile' | 'chooseSignOut' | 'sigingOut' | 'unchooseAction';
23
23
  };
24
24
 
25
25
  type IdentitySelectDeviceInvitationEvent = {
@@ -58,6 +58,14 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
58
58
  states: {
59
59
  choosingAction: {},
60
60
  managingDeviceInvitation: {},
61
+ managingAgent: {
62
+ initial: 'idle',
63
+ states: {
64
+ idle: {},
65
+ pending: {},
66
+ },
67
+ },
68
+ managingDevices: {},
61
69
  managingProfile: {
62
70
  initial: 'idle',
63
71
  states: {
@@ -69,15 +77,16 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
69
77
  updateProfile: { target: '.pending', actions: 'log' },
70
78
  },
71
79
  },
72
- // signingOut: {},
80
+ signingOut: {},
73
81
  },
74
82
  on: {
75
83
  unchooseAction: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
76
- chooseDevices: { target: '.managingDeviceInvitation', actions: ['unsetInvitation', 'log'] },
77
84
  deselectInvitation: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
78
85
  selectInvitation: { target: '.managingDeviceInvitation', actions: ['setInvitation', 'log'] },
86
+ chooseDevices: { target: '.managingDevices', actions: 'log' },
79
87
  chooseProfile: { target: '.managingProfile', actions: 'log' },
80
- // chooseSignOut: { target: '.signingOut', actions: 'log' },
88
+ chooseSignOut: { target: '.signingOut', actions: 'log' },
89
+ chooseAgent: { target: '.managingAgent', actions: 'log' },
81
90
  },
82
91
  },
83
92
  {
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { useTranslation } from '@dxos/react-ui';
8
+
9
+ import { Action, Actions, StepHeading, Input } from '../../../components';
10
+ import { type AgentFormProps, type IdentityPanelStepProps } from '../IdentityPanelProps';
11
+
12
+ export const AgentForm = (props: IdentityPanelStepProps & AgentFormProps) => {
13
+ const {
14
+ active,
15
+ onAgentCreate,
16
+ onAgentDestroy,
17
+ send,
18
+ validationMessage,
19
+ agentStatus,
20
+ agentActive,
21
+ agentProviderDisabled,
22
+ } = props;
23
+ const disabled = !active;
24
+ const { t } = useTranslation('os');
25
+ return (
26
+ <>
27
+ <div role='none' className='grow flex flex-col justify-center'>
28
+ <Input
29
+ {...{ validationMessage }}
30
+ label={<StepHeading>Agent Status</StepHeading>}
31
+ disabled={disabled}
32
+ data-testid='agent-status'
33
+ value={agentStatus}
34
+ />
35
+ </div>
36
+ <Actions>
37
+ <Action
38
+ disabled={agentActive || agentProviderDisabled}
39
+ onClick={() => onAgentCreate?.()}
40
+ data-testid={'create-agent'}
41
+ >
42
+ Create Agent
43
+ </Action>
44
+ <Action
45
+ disabled={!agentActive || agentProviderDisabled}
46
+ onClick={() => onAgentDestroy?.()}
47
+ data-testid={'destroy-agent'}
48
+ >
49
+ Destroy Agent
50
+ </Action>
51
+ <Action
52
+ disabled={disabled}
53
+ onClick={() => send?.({ type: 'unchooseAction' })}
54
+ data-testid={'update-profile-form-back'}
55
+ >
56
+ {t('back label')}
57
+ </Action>
58
+ </Actions>
59
+ </>
60
+ );
61
+ };
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+ import { type Event, type SingleOrArray } from 'xstate';
7
+
8
+ import { type Device } from '@dxos/react-client/halo';
9
+ import { useTranslation } from '@dxos/react-ui';
10
+
11
+ import { Action, Actions, DeviceList } from '../../../components';
12
+ import type { IdentityPanelStepProps } from '../IdentityPanelProps';
13
+ import type { IdentityEvent } from '../identityMachine';
14
+
15
+ export interface DeviceManagerProps extends Omit<IdentityPanelStepProps, 'send'> {
16
+ send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
17
+ devices: Device[];
18
+ }
19
+
20
+ export const DeviceManager = ({ devices, active, send }: DeviceManagerProps) => {
21
+ const { t } = useTranslation('os');
22
+ const disabled = !active;
23
+
24
+ return (
25
+ <>
26
+ <div role='none' className='grow'>
27
+ <DeviceList devices={devices} />
28
+ </div>
29
+ <Actions>
30
+ <Action
31
+ variant='ghost'
32
+ disabled={disabled}
33
+ onClick={() => send?.({ type: 'unchooseAction' })}
34
+ data-testid={'manage-devices-back'}
35
+ >
36
+ {t('back label')}
37
+ </Action>
38
+ </Actions>
39
+ </>
40
+ );
41
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CaretRight, Plus, Power, UserGear } from '@phosphor-icons/react';
5
+ import { CaretRight, Devices, Plus, Power, UserGear, HardDrive } from '@phosphor-icons/react';
6
6
  import React, { useCallback } from 'react';
7
7
 
8
8
  import { useClient } from '@dxos/react-client';
@@ -63,6 +63,11 @@ export const IdentityActionChooserImpl = ({
63
63
  <div role='none' className='grow flex flex-col gap-1'>
64
64
  <DensityProvider density='coarse'>
65
65
  <div className='grow justify-center flex flex-col gap-1'>
66
+ <Action data-testid='manage-agent' onClick={() => send?.({ type: 'chooseAgent' })} classNames='plb-4'>
67
+ <HardDrive className={getSize(6)} />
68
+ <span className='grow mli-3'>Manage Agent</span>
69
+ <CaretRight weight='bold' className={getSize(4)} />
70
+ </Action>
66
71
  <Action
67
72
  disabled={!active}
68
73
  data-testid='devices-panel.create-invitation'
@@ -70,6 +75,11 @@ export const IdentityActionChooserImpl = ({
70
75
  classNames='plb-4'
71
76
  >
72
77
  <Plus className={getSize(6)} />
78
+ <span className='grow mli-3'>{t('choose add device label')}</span>
79
+ <CaretRight weight='bold' className={getSize(4)} />
80
+ </Action>
81
+ <Action data-testid='manage-devices' onClick={() => send?.({ type: 'chooseDevices' })} classNames='plb-4'>
82
+ <Devices className={getSize(6)} />
73
83
  <span className='grow mli-3'>{t('choose devices label')}</span>
74
84
  <CaretRight weight='bold' className={getSize(4)} />
75
85
  </Action>
@@ -79,9 +89,8 @@ export const IdentityActionChooserImpl = ({
79
89
  <CaretRight weight='bold' className={getSize(4)} />
80
90
  </Action>
81
91
  <Action
82
- disabled
83
- data-testid='sign-out'
84
- onClick={() => {} /* send({ type: 'chooseSignOut' }) */}
92
+ data-testid='devices-panel.sign-out'
93
+ onClick={() => send?.({ type: 'chooseSignOut' })}
85
94
  classNames='plb-4'
86
95
  >
87
96
  <Power className={getSize(6)} />
@@ -37,7 +37,7 @@ export const ProfileForm = (props: ProfileFormProps) => {
37
37
  };
38
38
 
39
39
  // TODO(zhenyasav): impl shouldn't need send()
40
- export const ProfileFormImpl = (props: ProfileFormImplProps) => {
40
+ const ProfileFormImpl = (props: ProfileFormImplProps) => {
41
41
  const { active, profile, send, onUpdateProfile, validationMessage } = props;
42
42
  const disabled = !active;
43
43
  const { t } = useTranslation('os');
@@ -0,0 +1,84 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Warning } from '@phosphor-icons/react';
6
+ import React from 'react';
7
+ import { type Event, type SingleOrArray } from 'xstate';
8
+
9
+ import { Message, useTranslation } from '@dxos/react-ui';
10
+ import { getSize, mx } from '@dxos/react-ui-theme';
11
+
12
+ import { Action, Actions } from '../../../components';
13
+ import { type IdentityPanelStepProps } from '../IdentityPanelProps';
14
+ import { type IdentityEvent } from '../identityMachine';
15
+
16
+ export interface SignOutChooserProps extends Omit<IdentityPanelStepProps, 'send'> {
17
+ send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
18
+ onResetDevice?: () => Promise<void>;
19
+ onJoinNewIdentity?: () => void;
20
+ }
21
+
22
+ export const SignOutChooser = (props: SignOutChooserProps) => {
23
+ const { t } = useTranslation('os');
24
+ const onResetDevice = () => {
25
+ if (window.confirm(t('reset device confirm message'))) {
26
+ void props.onResetDevice?.();
27
+ }
28
+ };
29
+ return (
30
+ <SignOutChooserImpl
31
+ disabled={!props.active}
32
+ onBack={() => props.send?.({ type: 'unchooseAction' })}
33
+ onResetDevice={onResetDevice}
34
+ onJoinNewIdentity={props.onJoinNewIdentity}
35
+ />
36
+ );
37
+ };
38
+
39
+ export type SignOutChooserImplProps = {
40
+ disabled?: boolean;
41
+ onResetDevice?: () => void;
42
+ onJoinNewIdentity?: () => void;
43
+ onBack?: () => void;
44
+ };
45
+
46
+ const SignOutChooserImpl = ({ disabled, onResetDevice, onJoinNewIdentity, onBack }: SignOutChooserImplProps) => {
47
+ const { t } = useTranslation('os');
48
+ return (
49
+ <>
50
+ <div role='none' className='grow flex flex-col justify-center'>
51
+ <Message.Root valence='error'>
52
+ <Message.Title>
53
+ <Warning className={mx(getSize(6), 'inline mie-2')} />
54
+ {t('sign out chooser title')}
55
+ </Message.Title>
56
+ <Message.Body>{t('sign out chooser message')}</Message.Body>
57
+ </Message.Root>
58
+ </div>
59
+ <Actions>
60
+ <Action disabled={disabled} onClick={onBack}>
61
+ {t('back label')}
62
+ </Action>
63
+ <Action
64
+ variant='primary'
65
+ data-testid='sign-out.join-new-identity'
66
+ classNames='!bg-error-500 hover:!bg-error-550'
67
+ disabled={disabled}
68
+ onClick={onJoinNewIdentity}
69
+ >
70
+ {t('join new identity label')}
71
+ </Action>
72
+ <Action
73
+ variant='primary'
74
+ data-testid='sign-out.reset-device'
75
+ classNames='!bg-error-500 hover:!bg-error-550'
76
+ disabled={disabled}
77
+ onClick={onResetDevice}
78
+ >
79
+ {t('reset device label')}
80
+ </Action>
81
+ </Actions>
82
+ </>
83
+ );
84
+ };
@@ -2,5 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './AgentForm';
6
+ export * from './DeviceManager';
5
7
  export * from './IdentityActionChooser';
6
8
  export * from './ProfileForm';
9
+ export * from './SignOutChooser';
@@ -0,0 +1,88 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ import { type CancellableInvitation } from '@dxos/client-protocol';
8
+ import { invariant } from '@dxos/invariant';
9
+ import { useAgentHostingProviderClient, type Client } from '@dxos/react-client';
10
+ import { type Identity } from '@dxos/react-client/halo';
11
+ import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
12
+
13
+ export const useAgentHandlers = ({
14
+ client,
15
+ invitations,
16
+ identity,
17
+ }: {
18
+ client: Client;
19
+ invitations: CancellableInvitation[];
20
+ identity: Identity | null;
21
+ }) => {
22
+ const [validationMessage, setValidationMessage] = useState('');
23
+ const [agentStatus, setAgentStatus] = useState('');
24
+ const [agentActive, setAgentActive] = useState(false);
25
+ const agentHostingProviderClient = useAgentHostingProviderClient(client.config);
26
+
27
+ useEffect(() => {
28
+ const fetchAgentStatus = async () => {
29
+ // TODO(nf): if agent exists, subscribe to updates instead of oneshot status
30
+ if (agentHostingProviderClient) {
31
+ const agentStatus = await agentHostingProviderClient?.getAgent(identity?.identityKey.truncate() ?? 'foo');
32
+ if (agentStatus) {
33
+ setAgentActive(true);
34
+ setAgentStatus(agentStatus);
35
+ } else {
36
+ setAgentActive(false);
37
+ setValidationMessage('No agent deployed via provider.');
38
+ }
39
+ } else {
40
+ setValidationMessage('No agent provider configured.');
41
+ }
42
+ };
43
+ void fetchAgentStatus();
44
+ }, []);
45
+
46
+ const handleAgentCreate = useCallback(async (invitation: Invitation) => {
47
+ const invitationCode = InvitationEncoder.encode(invitation);
48
+ if (invitation.state === Invitation.State.CONNECTING) {
49
+ console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
50
+ invariant(identity, 'Identity not found');
51
+ if (agentHostingProviderClient) {
52
+ try {
53
+ const res = await agentHostingProviderClient?.createAgent(invitationCode, identity.identityKey.truncate());
54
+ // TODO(nf): human-consumable response from API
55
+ setAgentStatus(res);
56
+ setAgentActive(true);
57
+ setValidationMessage('');
58
+ } catch (err: any) {
59
+ setValidationMessage(`error creating agent: ${err.message}`);
60
+ }
61
+ }
62
+ }
63
+ }, []);
64
+
65
+ const onAgentCreate = async () => {
66
+ invitations.forEach((invitation) => invitation.cancel());
67
+
68
+ // TODO(nf): do this work in the hosting provider client?
69
+ const invitation = client.halo.share({ type: Invitation.Type.MULTIUSE, authMethod: Invitation.AuthMethod.NONE });
70
+
71
+ invitation.subscribe(handleAgentCreate);
72
+ };
73
+
74
+ const onAgentDestroy = async () => {
75
+ const identity = client.halo.identity.get();
76
+ invariant(identity, 'Identity not found');
77
+ try {
78
+ await agentHostingProviderClient?.destroyAgent(identity.identityKey.truncate());
79
+ setValidationMessage('No agent deployed via provider.');
80
+ setAgentStatus('');
81
+ setAgentActive(false);
82
+ } catch (err: any) {
83
+ setValidationMessage(`error destroying agent: ${err.message}`);
84
+ }
85
+ };
86
+
87
+ return { onAgentCreate, onAgentDestroy, validationMessage, agentStatus, agentActive };
88
+ };
@@ -193,8 +193,6 @@ export const JoinPanel = ({
193
193
  },
194
194
  });
195
195
 
196
- console.log('[joinState]', joinState);
197
-
198
196
  useEffect(() => {
199
197
  const subscription = joinService.subscribe((state) => {
200
198
  log('[state]', state);
@@ -368,6 +366,7 @@ export const JoinPanel = ({
368
366
  identityKey: joinState.context.halo.invitation?.identityKey ?? null,
369
367
  swarmKey: joinState.context.halo.invitation?.swarmKey ?? null,
370
368
  spaceKey: joinState.context.halo.invitation?.spaceKey ?? null,
369
+ target: joinState.context.halo.invitation?.target ?? null,
371
370
  });
372
371
  }, [joinState, propsOnDone]);
373
372
 
@@ -376,6 +375,7 @@ export const JoinPanel = ({
376
375
  identityKey: joinState.context.space.invitation?.identityKey ?? null,
377
376
  swarmKey: joinState.context.space.invitation?.swarmKey ?? null,
378
377
  spaceKey: joinState.context.space.invitation?.spaceKey ?? null,
378
+ target: joinState.context.space.invitation?.target ?? null,
379
379
  });
380
380
  }, [joinState, propsOnDone]);
381
381
 
@@ -15,7 +15,7 @@ import {
15
15
  } from 'xstate';
16
16
 
17
17
  import { log } from '@dxos/log';
18
- import type { Client } from '@dxos/react-client';
18
+ import { type Client, AlreadyJoinedError } from '@dxos/react-client';
19
19
  import { type Identity } from '@dxos/react-client/halo';
20
20
  import { type AuthenticatingInvitationObservable, Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
21
21
 
@@ -133,6 +133,12 @@ const getInvitationSubscribable = (
133
133
  }
134
134
  },
135
135
  (error: any) => {
136
+ if (error instanceof AlreadyJoinedError) {
137
+ log('[already joined]', { Kind, error });
138
+ next({ type: `success${Kind}Invitation`, invitation: invitation.get() });
139
+ return complete?.();
140
+ }
141
+
136
142
  log.error('[invitation errored]', { Kind, error });
137
143
  next({ type: `fail${Kind}Invitation`, reason: 'error' });
138
144
  return onError?.(error);
@@ -397,7 +403,7 @@ const joinMachine = createMachine<JoinMachineContext, JoinEvent>(
397
403
  event.type.includes('Space') ? { ...context.space, invitation: event.invitation } : context.space,
398
404
  }),
399
405
  log: (context, event) => {
400
- log.info('[transition]', {
406
+ log('[transition]', {
401
407
  event,
402
408
  haloInvitation: context.halo.invitation,
403
409
  spaceInvitation: context.space.invitation,
@@ -437,9 +443,8 @@ const useJoinMachine = (
437
443
  ({ halo }: JoinMachineContext) => {
438
444
  if (halo.unredeemedCode) {
439
445
  try {
440
- const invitationObservable = client.halo.join(
441
- InvitationEncoder.decode(defaultCodeFromUrl('halo', halo.unredeemedCode)),
442
- );
446
+ const invitation = InvitationEncoder.decode(defaultCodeFromUrl('halo', halo.unredeemedCode));
447
+ const invitationObservable = client.halo.join(invitation);
443
448
  return {
444
449
  ...halo,
445
450
  invitationObservable,
@@ -461,20 +466,12 @@ const useJoinMachine = (
461
466
  if (space.unredeemedCode) {
462
467
  try {
463
468
  const invitation = InvitationEncoder.decode(defaultCodeFromUrl('space', space.unredeemedCode));
464
- // Don’t join with the invitation if it has already succeeded.
465
- if (space.succeededKeys?.has(invitation.spaceKey?.toHex() ?? 'never')) {
466
- return {
467
- ...space,
468
- invitation,
469
- };
470
- } else {
471
- const invitationObservable = client.spaces.join(invitation);
472
- return {
473
- ...space,
474
- invitationObservable,
475
- invitationSubscribable: getInvitationSubscribable('Space', invitationObservable),
476
- };
477
- }
469
+ const invitationObservable = client.spaces.join(invitation);
470
+ return {
471
+ ...space,
472
+ invitationObservable,
473
+ invitationSubscribable: getInvitationSubscribable('Space', invitationObservable),
474
+ };
478
475
  } catch (err) {
479
476
  log.error('Could not redeem space invitation code', err);
480
477
  return space;