@dxos/shell 0.6.14-main.2b6a0f3 → 0.6.14-main.f49f251

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 (110) hide show
  1. package/dist/bundle/shell.mjs +15116 -11401
  2. package/dist/bundle/shell.mjs.map +1 -1
  3. package/dist/bundle/shell.umd.js +134 -89
  4. package/dist/bundle/shell.umd.js.map +1 -1
  5. package/dist/bundle/style.css +1 -1
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +465 -364
  8. package/dist/lib/browser/react.mjs.map +4 -4
  9. package/dist/types/src/components/CompoundButton/CompoundButton.stories.js +1 -1
  10. package/dist/types/src/components/CompoundButton/CompoundButton.stories.js.map +1 -1
  11. package/dist/types/src/components/DeviceList/DeviceList.d.ts +1 -1
  12. package/dist/types/src/components/DeviceList/DeviceList.d.ts.map +1 -1
  13. package/dist/types/src/components/DeviceList/DeviceList.js +2 -2
  14. package/dist/types/src/components/DeviceList/DeviceList.js.map +1 -1
  15. package/dist/types/src/components/DeviceList/DeviceListItem.d.ts.map +1 -1
  16. package/dist/types/src/components/DeviceList/DeviceListItem.js +4 -1
  17. package/dist/types/src/components/DeviceList/DeviceListItem.js.map +1 -1
  18. package/dist/types/src/components/DeviceList/DeviceListProps.d.ts +2 -0
  19. package/dist/types/src/components/DeviceList/DeviceListProps.d.ts.map +1 -1
  20. package/dist/types/src/components/IdentityList/SpaceMemberList.stories.js +1 -1
  21. package/dist/types/src/components/IdentityList/SpaceMemberList.stories.js.map +1 -1
  22. package/dist/types/src/components/InvitationList/InvitationList.stories.js +1 -1
  23. package/dist/types/src/components/InvitationList/InvitationList.stories.js.map +1 -1
  24. package/dist/types/src/components/InvitationList/InvitationListContainer.d.ts.map +1 -1
  25. package/dist/types/src/components/InvitationList/InvitationListContainer.js +2 -2
  26. package/dist/types/src/components/InvitationList/InvitationListContainer.js.map +1 -1
  27. package/dist/types/src/components/Panel/Panel.stories.js +1 -1
  28. package/dist/types/src/components/Panel/Panel.stories.js.map +1 -1
  29. package/dist/types/src/components/Viewport/Viewport.stories.js +1 -1
  30. package/dist/types/src/components/Viewport/Viewport.stories.js.map +1 -1
  31. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -1
  32. package/dist/types/src/composites/Shell/Shell.js +11 -1
  33. package/dist/types/src/composites/Shell/Shell.js.map +1 -1
  34. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  35. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +43 -24
  36. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  37. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js +1 -1
  38. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js.map +1 -1
  39. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts +2 -1
  40. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts.map +1 -1
  41. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts +1 -1
  42. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts.map +1 -1
  43. package/dist/types/src/panels/IdentityPanel/identityMachine.js +2 -0
  44. package/dist/types/src/panels/IdentityPanel/identityMachine.js.map +1 -1
  45. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts +3 -5
  46. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  47. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +5 -3
  48. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  49. package/dist/types/src/panels/JoinPanel/JoinPanel.stories.js +1 -1
  50. package/dist/types/src/panels/JoinPanel/JoinPanel.stories.js.map +1 -1
  51. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +1 -1
  52. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -1
  53. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -1
  54. package/dist/types/src/panels/JoinPanel/joinMachine.js +6 -0
  55. package/dist/types/src/panels/JoinPanel/joinMachine.js.map +1 -1
  56. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.d.ts.map +1 -1
  57. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js +1 -3
  58. package/dist/types/src/panels/JoinPanel/steps/AdditionMethodChooser.js.map +1 -1
  59. package/dist/types/src/panels/JoinPanel/steps/IdentityInput.d.ts.map +1 -1
  60. package/dist/types/src/panels/JoinPanel/steps/IdentityInput.js +17 -7
  61. package/dist/types/src/panels/JoinPanel/steps/IdentityInput.js.map +1 -1
  62. package/dist/types/src/panels/Panels.stories.js +1 -1
  63. package/dist/types/src/panels/Panels.stories.js.map +1 -1
  64. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js +1 -1
  65. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js.map +1 -1
  66. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  67. package/dist/types/src/playwright/invitations-manager.js +2 -2
  68. package/dist/types/src/playwright/invitations-manager.js.map +1 -1
  69. package/dist/types/src/playwright/invitations.spec.js +0 -16
  70. package/dist/types/src/playwright/invitations.spec.js.map +1 -1
  71. package/dist/types/src/steps/ConfirmReset.d.ts +1 -1
  72. package/dist/types/src/steps/ConfirmReset.d.ts.map +1 -1
  73. package/dist/types/src/steps/ConfirmReset.js +6 -2
  74. package/dist/types/src/steps/ConfirmReset.js.map +1 -1
  75. package/dist/types/src/stories/Invitations.e2e-stories.d.ts +1 -3
  76. package/dist/types/src/stories/Invitations.e2e-stories.d.ts.map +1 -1
  77. package/dist/types/src/stories/Invitations.e2e-stories.js +14 -6
  78. package/dist/types/src/stories/Invitations.e2e-stories.js.map +1 -1
  79. package/dist/types/src/translations/locales/en-US.d.ts +2 -0
  80. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  81. package/dist/types/src/translations/locales/en-US.js +2 -0
  82. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  83. package/package.json +26 -26
  84. package/src/components/CompoundButton/CompoundButton.stories.tsx +1 -1
  85. package/src/components/DeviceList/DeviceList.tsx +2 -1
  86. package/src/components/DeviceList/DeviceListItem.tsx +8 -0
  87. package/src/components/DeviceList/DeviceListProps.ts +2 -0
  88. package/src/components/IdentityList/SpaceMemberList.stories.tsx +1 -1
  89. package/src/components/InvitationList/InvitationList.stories.tsx +1 -1
  90. package/src/components/InvitationList/InvitationListContainer.tsx +6 -3
  91. package/src/components/Panel/Panel.stories.tsx +1 -1
  92. package/src/components/Viewport/Viewport.stories.tsx +1 -1
  93. package/src/composites/Shell/Shell.tsx +12 -1
  94. package/src/panels/IdentityPanel/IdentityPanel.stories.tsx +1 -1
  95. package/src/panels/IdentityPanel/IdentityPanel.tsx +78 -44
  96. package/src/panels/IdentityPanel/IdentityPanelProps.ts +2 -0
  97. package/src/panels/IdentityPanel/identityMachine.ts +3 -1
  98. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +21 -10
  99. package/src/panels/JoinPanel/JoinPanel.stories.tsx +1 -1
  100. package/src/panels/JoinPanel/JoinPanelProps.ts +1 -1
  101. package/src/panels/JoinPanel/joinMachine.ts +7 -0
  102. package/src/panels/JoinPanel/steps/AdditionMethodChooser.tsx +0 -2
  103. package/src/panels/JoinPanel/steps/IdentityInput.tsx +22 -10
  104. package/src/panels/Panels.stories.tsx +1 -1
  105. package/src/panels/SpacePanel/SpacePanel.stories.tsx +1 -1
  106. package/src/playwright/invitations-manager.ts +3 -2
  107. package/src/playwright/invitations.spec.ts +0 -16
  108. package/src/steps/ConfirmReset.tsx +13 -5
  109. package/src/stories/Invitations.e2e-stories.tsx +17 -10
  110. package/src/translations/locales/en-US.ts +2 -0
@@ -2,10 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Key, Plugs, PlugsConnected } from '@phosphor-icons/react';
5
+ import { FirstAidKit, Key, Plugs, PlugsConnected } from '@phosphor-icons/react';
6
6
  import React, { useEffect, useMemo, useState } from 'react';
7
7
 
8
8
  import { generateName } from '@dxos/display-name';
9
+ import { invariant } from '@dxos/invariant';
9
10
  import { log } from '@dxos/log';
10
11
  import { useClient } from '@dxos/react-client';
11
12
  import { type Identity, useIdentity, useDevices, useHaloInvitations } from '@dxos/react-client/halo';
@@ -51,6 +52,7 @@ const IdentityHeading = ({
51
52
  connectionState,
52
53
  onChangeConnectionState,
53
54
  }: IdentityPanelHeadingProps) => {
55
+ const client = useClient();
54
56
  const fallbackValue = keyToFallback(identity.identityKey);
55
57
  const { t } = useTranslation('os');
56
58
  const [displayName, setDisplayName] = useState(identity.profile?.displayName ?? '');
@@ -58,7 +60,7 @@ const IdentityHeading = ({
58
60
  const [hue, setHueDirectly] = useState<string>(getHueValue(identity));
59
61
  const { textValue, setTextValue } = useClipboardContext();
60
62
  const identityHex = identity.identityKey.toHex();
61
- const copied = textValue === identityHex;
63
+ const publicKeyCopied = textValue === identityHex;
62
64
 
63
65
  const setEmoji = (nextEmoji: string) => {
64
66
  setEmojiDirectly(nextEmoji);
@@ -70,9 +72,15 @@ const IdentityHeading = ({
70
72
  void onUpdateProfile?.({ ...identity.profile, data: { ...identity.profile?.data, hue: nextHue } });
71
73
  };
72
74
 
73
- const isConnected = connectionState === ConnectionState.ONLINE;
75
+ const handleRecoveryCode = async () => {
76
+ invariant(client.services.services.IdentityService, 'IdentityService not available');
77
+ // TODO(wittjosiah): This needs a proper api.
78
+ const { seedphrase } = await client.services.services.IdentityService.createRecoveryPhrase();
79
+ void setTextValue(seedphrase);
80
+ };
74
81
 
75
- const debug = true;
82
+ const isConnected = connectionState === ConnectionState.ONLINE;
83
+ const isProduction = client.config.values.runtime?.app?.env?.DX_ENVIRONMENT === 'production';
76
84
 
77
85
  return (
78
86
  <Heading titleId={titleId} title={title} corner={<CloseButton onDone={onDone} />}>
@@ -103,47 +111,61 @@ const IdentityHeading = ({
103
111
  <Toolbar.Root classNames='flex justify-center items-center gap-1 pt-3'>
104
112
  <EmojiPickerToolbarButton emoji={emoji} onChangeEmoji={setEmoji} classNames='bs-[--rail-action]' />
105
113
  <HuePickerToolbarButton hue={hue} onChangeHue={setHue} classNames='bs-[--rail-action]' />
106
- {debug && (
107
- <>
108
- <Tooltip.Root>
109
- <Tooltip.Trigger asChild>
110
- <Toolbar.Button
111
- classNames='bs-[--rail-action]'
112
- data-testid='update-profile-form-copy-key'
113
- onClick={() => setTextValue(identityHex)}
114
- >
115
- <span className='sr-only'>{t(copied ? 'copy success label' : 'copy self public key label')}</span>
116
- <Key className={getSize(5)} />
117
- </Toolbar.Button>
118
- </Tooltip.Trigger>
119
- <Tooltip.Portal>
120
- <Tooltip.Content side='bottom' classNames='z-50'>
121
- {t(copied ? 'copy success label' : 'copy self public key label')}
122
- <Tooltip.Arrow />
123
- </Tooltip.Content>
124
- </Tooltip.Portal>
125
- <Tooltip.Root>
126
- <Tooltip.Trigger asChild>
127
- <Toolbar.Button
128
- classNames={['bs-[--rail-action]', !isConnected && 'text-error-500']}
129
- onClick={() =>
130
- onChangeConnectionState?.(isConnected ? ConnectionState.OFFLINE : ConnectionState.ONLINE)
131
- }
132
- >
133
- <span className='sr-only'>{t(isConnected ? 'disconnect label' : 'connect label')}</span>
134
- {isConnected ? <PlugsConnected className={getSize(5)} /> : <Plugs className={getSize(5)} />}
135
- </Toolbar.Button>
136
- </Tooltip.Trigger>
137
- <Tooltip.Portal>
138
- <Tooltip.Content side='bottom' classNames='z-50'>
139
- {t(isConnected ? 'disconnect label' : 'connect label')}
140
- <Tooltip.Arrow />
141
- </Tooltip.Content>
142
- </Tooltip.Portal>
143
- </Tooltip.Root>
144
- </Tooltip.Root>
145
- </>
114
+ <Tooltip.Root>
115
+ <Tooltip.Trigger asChild>
116
+ <Toolbar.Button
117
+ classNames='bs-[--rail-action]'
118
+ data-testid='update-profile-form-copy-key'
119
+ onClick={() => setTextValue(identityHex)}
120
+ >
121
+ <span className='sr-only'>
122
+ {t(publicKeyCopied ? 'copy success label' : 'copy self public key label')}
123
+ </span>
124
+ <Key className={getSize(5)} />
125
+ </Toolbar.Button>
126
+ </Tooltip.Trigger>
127
+ <Tooltip.Portal>
128
+ <Tooltip.Content side='bottom' classNames='z-50'>
129
+ {t(publicKeyCopied ? 'copy success label' : 'copy self public key label')}
130
+ <Tooltip.Arrow />
131
+ </Tooltip.Content>
132
+ </Tooltip.Portal>
133
+ </Tooltip.Root>
134
+ {/* TODO(wittjosiah): Remove. This should have its own flow. */}
135
+ {!isProduction && (
136
+ <Tooltip.Root>
137
+ <Tooltip.Trigger asChild>
138
+ <Toolbar.Button classNames='bs-[--rail-action]' onClick={handleRecoveryCode}>
139
+ <FirstAidKit className={getSize(5)} />
140
+ </Toolbar.Button>
141
+ </Tooltip.Trigger>
142
+ <Tooltip.Portal>
143
+ <Tooltip.Content side='bottom' classNames='z-50'>
144
+ {t('create recovery code label')}
145
+ <Tooltip.Arrow />
146
+ </Tooltip.Content>
147
+ </Tooltip.Portal>
148
+ </Tooltip.Root>
146
149
  )}
150
+ <Tooltip.Root>
151
+ <Tooltip.Trigger asChild>
152
+ <Toolbar.Button
153
+ classNames={['bs-[--rail-action]', !isConnected && 'text-error-500']}
154
+ onClick={() =>
155
+ onChangeConnectionState?.(isConnected ? ConnectionState.OFFLINE : ConnectionState.ONLINE)
156
+ }
157
+ >
158
+ <span className='sr-only'>{t(isConnected ? 'disconnect label' : 'connect label')}</span>
159
+ {isConnected ? <PlugsConnected className={getSize(5)} /> : <Plugs className={getSize(5)} />}
160
+ </Toolbar.Button>
161
+ </Tooltip.Trigger>
162
+ <Tooltip.Portal>
163
+ <Tooltip.Content side='bottom' classNames='z-50'>
164
+ {t(isConnected ? 'disconnect label' : 'connect label')}
165
+ <Tooltip.Arrow />
166
+ </Tooltip.Content>
167
+ </Tooltip.Portal>
168
+ </Tooltip.Root>
147
169
  </Toolbar.Root>
148
170
  </Avatar.Root>
149
171
  </Heading>
@@ -157,6 +179,7 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
157
179
  activeView,
158
180
  onUpdateProfile,
159
181
  onResetStorage,
182
+ onRecover,
160
183
  onJoinNewIdentity,
161
184
  IdentityActionChooser: IdentityActionChooserComponent = IdentityActionChooser,
162
185
  InvitationManager: InvitationManagerComponent = InvitationManager,
@@ -207,6 +230,15 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
207
230
  onCancel={onCancelReset}
208
231
  />
209
232
  </Viewport.View>
233
+ <Viewport.View classNames={viewStyles} id='confirm recover'>
234
+ <ConfirmReset
235
+ active={activeView === 'confirm recover'}
236
+ {...rest}
237
+ mode='recover'
238
+ onConfirm={onRecover}
239
+ onCancel={onCancelReset}
240
+ />
241
+ </Viewport.View>
210
242
  <Viewport.View classNames={viewStyles} id='confirm reset storage'>
211
243
  <ConfirmReset
212
244
  active={activeView === 'confirm reset storage'}
@@ -270,6 +302,8 @@ export const IdentityPanel = ({
270
302
  return 'device invitation manager';
271
303
  case identityState.matches('confirmingResetStorage'):
272
304
  return 'confirm reset storage';
305
+ case identityState.matches('confirmingRecover'):
306
+ return 'confirm recover';
273
307
  case identityState.matches('confirmingJoinNewIdentity'):
274
308
  return 'confirm join new identity';
275
309
  default:
@@ -21,6 +21,7 @@ export type IdentityPanelImplProps = {
21
21
  | 'device invitation manager'
22
22
  | 'identity action chooser'
23
23
  | 'confirm join new identity'
24
+ | 'confirm recover'
24
25
  | 'confirm reset storage';
25
26
  initialDisposition?: IdentityPanelInitialDisposition;
26
27
  identity: Identity;
@@ -29,6 +30,7 @@ export type IdentityPanelImplProps = {
29
30
  onChangeConnectionState?: (nextState: ConnectionState) => Promise<void>;
30
31
  onUpdateProfile?: (profile: NonNullable<Identity['profile']>) => Promise<void>;
31
32
  onResetStorage?: () => Promise<void>;
33
+ onRecover?: () => Promise<void>;
32
34
  onJoinNewIdentity?: () => Promise<void>;
33
35
  createInvitationUrl: (invitationCode: string) => string;
34
36
  send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
@@ -21,7 +21,7 @@ type IdentityMachineContext = {
21
21
  };
22
22
 
23
23
  type IdentityChooseActionEvent = {
24
- type: 'chooseResetStorage' | 'chooseJoinNewIdentity' | 'unchooseAction';
24
+ type: 'chooseResetStorage' | 'chooseRecoverIdentity' | 'chooseJoinNewIdentity' | 'unchooseAction';
25
25
  };
26
26
 
27
27
  type IdentitySelectDeviceInvitationEvent = {
@@ -67,6 +67,7 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
67
67
  choosingAction: {},
68
68
  managingDeviceInvitation: {},
69
69
  confirmingResetStorage: {},
70
+ confirmingRecover: {},
70
71
  confirmingJoinNewIdentity: {},
71
72
  },
72
73
  on: {
@@ -74,6 +75,7 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
74
75
  deselectInvitation: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
75
76
  selectInvitation: { target: '.managingDeviceInvitation', actions: ['setInvitation', 'log'] },
76
77
  chooseJoinNewIdentity: { target: '.confirmingJoinNewIdentity', actions: 'log' },
78
+ chooseRecoverIdentity: { target: '.confirmingRecover', actions: 'log' },
77
79
  chooseResetStorage: { target: '.confirmingResetStorage', actions: 'log' },
78
80
  },
79
81
  },
@@ -9,7 +9,7 @@ import { useClient } from '@dxos/react-client';
9
9
  import { useHaloInvitations, DeviceType } from '@dxos/react-client/halo';
10
10
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
11
11
 
12
- import { AgentConfig, type AgentFormProps, DeviceList } from '../../../components';
12
+ import { AgentConfig, type AgentFormProps, DeviceList, type DeviceListProps } from '../../../components';
13
13
  import { type IdentityPanelStepProps } from '../IdentityPanelProps';
14
14
 
15
15
  export type IdentityActionChooserProps = IdentityPanelStepProps;
@@ -33,20 +33,30 @@ export const IdentityActionChooser = (props: IdentityPanelStepProps) => {
33
33
  }
34
34
  send?.({ type: 'selectInvitation', invitation });
35
35
  };
36
- return <IdentityActionChooserImpl {...props} onCreateInvitationClick={createInvitation} />;
36
+ const isProduction = client.config.values.runtime?.app?.env?.DX_ENVIRONMENT === 'production';
37
+ return (
38
+ <IdentityActionChooserImpl
39
+ {...props}
40
+ onClickAdd={createInvitation}
41
+ onClickJoinExisting={() => send?.({ type: 'chooseJoinNewIdentity' })}
42
+ onClickRecover={isProduction ? undefined : () => send?.({ type: 'chooseRecoverIdentity' })}
43
+ onClickReset={() => send?.({ type: 'chooseResetStorage' })}
44
+ />
45
+ );
37
46
  };
38
47
 
39
48
  export type IdentityActionChooserImplProps = IdentityActionChooserProps &
40
- Partial<AgentFormProps> & {
41
- onCreateInvitationClick?: () => void;
42
- };
49
+ Partial<AgentFormProps> &
50
+ Pick<DeviceListProps, 'onClickAdd' | 'onClickJoinExisting' | 'onClickRecover' | 'onClickReset'>;
43
51
 
44
52
  export const IdentityActionChooserImpl = ({
45
- send,
46
- onCreateInvitationClick,
47
53
  devices,
48
54
  connectionState,
49
55
  agentHostingEnabled,
56
+ onClickAdd,
57
+ onClickJoinExisting,
58
+ onClickRecover,
59
+ onClickReset,
50
60
  ...agentProps
51
61
  }: IdentityActionChooserImplProps) => {
52
62
  const managedDeviceAvailable = devices.find((device) => device.profile?.type === DeviceType.AGENT_MANAGED);
@@ -55,9 +65,10 @@ export const IdentityActionChooserImpl = ({
55
65
  <DeviceList
56
66
  devices={devices}
57
67
  connectionState={connectionState}
58
- onClickAdd={onCreateInvitationClick}
59
- onClickJoinExisting={() => send?.({ type: 'chooseJoinNewIdentity' })}
60
- onClickReset={() => send?.({ type: 'chooseResetStorage' })}
68
+ onClickAdd={onClickAdd}
69
+ onClickJoinExisting={onClickJoinExisting}
70
+ onClickRecover={onClickRecover}
71
+ onClickReset={onClickReset}
61
72
  onAgentDestroy={agentProps.onAgentDestroy!}
62
73
  />
63
74
  {!managedDeviceAvailable && agentHostingEnabled && <AgentConfig {...(agentProps as AgentFormProps)} />}
@@ -30,7 +30,7 @@ const JoinDialog = (args: Partial<JoinPanelImplProps>) => (
30
30
  );
31
31
 
32
32
  export default {
33
- title: 'sdk/react-shell/JoinPanel',
33
+ title: 'sdk/shell/JoinPanel',
34
34
  component: JoinDialog,
35
35
  decorators: [withTheme],
36
36
  parameters: { translations, chromatic: { disableSnapshot: false } },
@@ -14,7 +14,7 @@ import { type FailReason } from '../../types';
14
14
 
15
15
  export type JoinPanelMode = 'default' | 'halo-only';
16
16
 
17
- export type JoinPanelInitialDisposition = 'default' | 'accept-halo-invitation';
17
+ export type JoinPanelInitialDisposition = 'default' | 'accept-halo-invitation' | 'recover-identity';
18
18
 
19
19
  export type JoinStepProps = Omit<StepProps, 'send' | 'onDone'> & {
20
20
  send: JoinSend;
@@ -312,6 +312,11 @@ const joinMachine = createMachine<JoinMachineContext, JoinEvent>(
312
312
  target: '#join.choosingIdentity.acceptingHaloInvitation',
313
313
  actions: 'log',
314
314
  },
315
+ {
316
+ cond: 'initiallyRecoveringIdentity',
317
+ target: '#join.choosingIdentity.recoveringIdentity',
318
+ actions: 'log',
319
+ },
315
320
  { cond: 'noSelectedIdentity', target: 'choosingIdentity', actions: 'log' },
316
321
  { target: 'acceptingSpaceInvitation', actions: 'log' },
317
322
  ],
@@ -364,6 +369,8 @@ const joinMachine = createMachine<JoinMachineContext, JoinEvent>(
364
369
  noSelectedIdentity: ({ identity }, _event) => !identity,
365
370
  initiallyAcceptingHaloInvitation: ({ identity, initialDisposition }, _event) =>
366
371
  !identity && initialDisposition === 'accept-halo-invitation',
372
+ initiallyRecoveringIdentity: ({ identity, initialDisposition }, _event) =>
373
+ !identity && initialDisposition === 'recover-identity',
367
374
  hasHaloUnredeemedCode: ({ halo }, _event) => !!halo.unredeemedCode,
368
375
  noSpaceInvitation: ({ space }, _event) => !space.invitation && !space.unredeemedCode,
369
376
  },
@@ -50,8 +50,6 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
50
50
  </CompoundButton>
51
51
  <CompoundButton
52
52
  {...sharedButtonProps}
53
- // TODO(mykola): Implement recover.
54
- disabled={true}
55
53
  description={t('recover identity description')}
56
54
  before={<Textbox className={getSize(6)} />}
57
55
  onClick={() => send({ type: 'recoverIdentity' })}
@@ -28,16 +28,28 @@ export const IdentityInput = (props: IdentityInputProps) => {
28
28
  const client = useClient();
29
29
  const { t } = useTranslation('os');
30
30
  const [validationMessage, setValidationMessage] = useState('');
31
- const createIdentity = (displayName: string) => {
32
- void client.halo.createIdentity({ [isRecover ? 'seedphrase' : 'displayName']: displayName }).then(
33
- (identity) => {
34
- send?.({ type: 'selectIdentity' as const, identity });
35
- },
36
- (error) => {
37
- log.catch(error);
38
- setValidationMessage(t(isRecover ? 'failed to recover identity message' : 'failed to create identity message'));
39
- },
40
- );
31
+ const createIdentity = (value: string) => {
32
+ if (isRecover) {
33
+ void client.halo.recoverIdentity({ seedphrase: value }).then(
34
+ (identity) => {
35
+ send?.({ type: 'selectIdentity' as const, identity });
36
+ },
37
+ (error) => {
38
+ log.catch(error);
39
+ setValidationMessage(t('failed to recover identity message'));
40
+ },
41
+ );
42
+ } else {
43
+ void client.halo.createIdentity({ displayName: value }).then(
44
+ (identity) => {
45
+ send?.({ type: 'selectIdentity' as const, identity });
46
+ },
47
+ (error) => {
48
+ log.catch(error);
49
+ setValidationMessage(t('failed to create identity message'));
50
+ },
51
+ );
52
+ }
41
53
  };
42
54
  return <IdentityInputImpl {...props} onDisplayNameConfirm={createIdentity} validationMessage={validationMessage} />;
43
55
  };
@@ -53,7 +53,7 @@ export const Default = (props: any) => {
53
53
  Default.parameters = { layout: 'fullscreen' };
54
54
 
55
55
  export default {
56
- title: 'sdk/react-shell/All',
56
+ title: 'sdk/shell/All',
57
57
  decorators: [withTheme],
58
58
  component: StoryRow,
59
59
  parameters: { chromatic: { disableSnapshot: false }, translations: [osTranslations] },
@@ -28,7 +28,7 @@ const noOpProps: SpacePanelImplProps = {
28
28
  };
29
29
 
30
30
  export default {
31
- title: 'sdk/react-shell/SpacePanel',
31
+ title: 'sdk/shell/SpacePanel',
32
32
  component: SpacePanelImpl,
33
33
  decorators: [withTheme],
34
34
  parameters: { chromatic: { disableSnapshot: false } },
@@ -32,7 +32,7 @@ export class InvitationsManager extends ScopedShellManager {
32
32
  }
33
33
 
34
34
  const { page } = await setupPage(this._browser, {
35
- url: storybookUrl('invitations--default'),
35
+ url: storybookUrl('sdk-shell-invitations--default'),
36
36
  });
37
37
 
38
38
  this.page = page;
@@ -144,7 +144,8 @@ export class InvitationsManager extends ScopedShellManager {
144
144
  },
145
145
  { id, type, options },
146
146
  );
147
- return this._invitationCode.wait();
147
+
148
+ return this._invitationCode.wait({ timeout: 1_000 });
148
149
  }
149
150
 
150
151
  async acceptInvitation(id: number, type: 'device' | 'space', invitation: string) {
@@ -44,7 +44,6 @@ test.describe('Invitations', () => {
44
44
  await manager.acceptInvitation(1, 'device', invitation);
45
45
  await manager.readyToAuthenticate('device', manager.peer(1));
46
46
  await manager.authenticateInvitation('device', authCode, manager.peer(1));
47
- await manager.doneInvitation('device', manager.peer(1));
48
47
 
49
48
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
50
49
  });
@@ -56,7 +55,6 @@ test.describe('Invitations', () => {
56
55
 
57
56
  await manager.openPanel(1, 'identity');
58
57
  await manager.acceptInvitation(1, 'device', invitation);
59
- await manager.doneInvitation('device', manager.peer(1));
60
58
 
61
59
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
62
60
  });
@@ -72,7 +70,6 @@ test.describe('Invitations', () => {
72
70
  await manager.authenticateInvitation('device', '000000', manager.peer(1));
73
71
  await manager.clearAuthCode('device', manager.peer(1));
74
72
  await manager.authenticateInvitation('device', authCode, manager.peer(1));
75
- await manager.doneInvitation('device', manager.peer(1));
76
73
 
77
74
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
78
75
  });
@@ -99,7 +96,6 @@ test.describe('Invitations', () => {
99
96
  await manager.invitationInputContinue('device', manager.peer(1));
100
97
  await manager.clearAuthCode('device', manager.peer(1));
101
98
  await manager.authenticateInvitation('device', authCode, manager.peer(1));
102
- await manager.doneInvitation('device', manager.peer(1));
103
99
 
104
100
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
105
101
  });
@@ -144,7 +140,6 @@ test.describe('Invitations', () => {
144
140
  await manager.invitationInputContinue('device', manager.peer(1));
145
141
  await manager.clearAuthCode('device', manager.peer(1));
146
142
  await manager.authenticateInvitation('device', authCode, manager.peer(1));
147
- await manager.doneInvitation('device', manager.peer(1));
148
143
 
149
144
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
150
145
  });
@@ -167,7 +162,6 @@ test.describe('Invitations', () => {
167
162
  await manager.invitationInputContinue('device', manager.peer(1));
168
163
  await manager.clearAuthCode('device', manager.peer(1));
169
164
  await manager.authenticateInvitation('device', authCode, manager.peer(1));
170
- await manager.doneInvitation('device', manager.peer(1));
171
165
 
172
166
  expect(await manager.getDisplayName(0)).toEqual(await manager.getDisplayName(1));
173
167
  });
@@ -186,7 +180,6 @@ test.describe('Invitations', () => {
186
180
  await manager.acceptInvitation(1, 'space', invitation);
187
181
  await manager.readyToAuthenticate('space', manager.peer(1));
188
182
  await manager.authenticateInvitation('space', authCode, manager.peer(1));
189
- await manager.doneInvitation('space', manager.peer(1));
190
183
 
191
184
  await manager.openPanel(0, 'spaces');
192
185
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -202,11 +195,9 @@ test.describe('Invitations', () => {
202
195
  await manager.createIdentity(1);
203
196
  await manager.openPanel(1, 'join');
204
197
  await manager.acceptInvitation(1, 'space', invitation1);
205
- await manager.doneInvitation('space', manager.peer(1));
206
198
 
207
199
  await manager.openPanel(1, 'join');
208
200
  await manager.acceptInvitation(1, 'space', invitation2);
209
- await manager.doneInvitation('space', manager.peer(1));
210
201
 
211
202
  expect(await manager.getSpaceMembersCount(0)).toEqual(2);
212
203
  });
@@ -220,7 +211,6 @@ test.describe('Invitations', () => {
220
211
  await manager.createIdentity(1);
221
212
  await manager.openPanel(1, 'join');
222
213
  await manager.acceptInvitation(1, 'space', invitation);
223
- await manager.doneInvitation('space', manager.peer(1));
224
214
 
225
215
  await manager.openPanel(0, 'spaces');
226
216
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -242,7 +232,6 @@ test.describe('Invitations', () => {
242
232
 
243
233
  await manager.clearAuthCode('space', manager.peer(1));
244
234
  await manager.authenticateInvitation('space', authCode, manager.peer(1));
245
- await manager.doneInvitation('space', manager.peer(1));
246
235
 
247
236
  await manager.openPanel(0, 'spaces');
248
237
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -272,7 +261,6 @@ test.describe('Invitations', () => {
272
261
  await manager.invitationInputContinue('space', manager.peer(1));
273
262
  await manager.clearAuthCode('space', manager.peer(1));
274
263
  await manager.authenticateInvitation('space', authCode, manager.peer(1));
275
- await manager.doneInvitation('space', manager.peer(1));
276
264
 
277
265
  await manager.openPanel(0, 'spaces');
278
266
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -323,7 +311,6 @@ test.describe('Invitations', () => {
323
311
  await manager.invitationInputContinue('space', manager.peer(1));
324
312
  await manager.clearAuthCode('space', manager.peer(1));
325
313
  await manager.authenticateInvitation('space', authCode, manager.peer(1));
326
- await manager.doneInvitation('space', manager.peer(1));
327
314
 
328
315
  await manager.openPanel(0, 'spaces');
329
316
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -349,7 +336,6 @@ test.describe('Invitations', () => {
349
336
  await manager.invitationInputContinue('space', manager.peer(1));
350
337
  await manager.clearAuthCode('space', manager.peer(1));
351
338
  await manager.authenticateInvitation('space', authCode, manager.peer(1));
352
- await manager.doneInvitation('space', manager.peer(1));
353
339
 
354
340
  await manager.openPanel(0, 'spaces');
355
341
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -380,8 +366,6 @@ test.describe('Invitations', () => {
380
366
  // Helps to ensure both auth codes are fully input (especially in webkit).
381
367
  await sleep(100);
382
368
  await manager.authenticateInvitation('space', authCode2, manager.peer(2));
383
- await manager.doneInvitation('space', manager.peer(1));
384
- await manager.doneInvitation('space', manager.peer(2));
385
369
 
386
370
  await manager.openPanel(0, 'spaces');
387
371
  expect(await manager.getSpaceName(0, 1)).toEqual(await manager.getSpaceName(1, 1));
@@ -15,7 +15,7 @@ import { Action, Actions, StepHeading, Input } from '../components';
15
15
  type ConfirmResetOptions = Partial<{
16
16
  onCancel: () => void;
17
17
  onConfirm: () => Promise<void>;
18
- mode: 'join new identity' | 'reset storage';
18
+ mode: 'join new identity' | 'recover' | 'reset storage';
19
19
  }>;
20
20
 
21
21
  export type ConfirmResetProps = StepProps & ConfirmResetOptions;
@@ -64,7 +64,8 @@ export const ConfirmResetImpl = ({
64
64
  const { t } = useTranslation('os');
65
65
  const confirmationValue = t('confirmation value');
66
66
  const [inputValue, setInputValue] = useState('');
67
- const testIdAffix = mode === 'join new identity' ? 'join-new-identity' : 'reset-storage';
67
+ const testIdAffix =
68
+ mode === 'join new identity' ? 'join-new-identity' : mode === 'recover' ? 'recover' : 'reset-storage';
68
69
  return (
69
70
  <>
70
71
  <div role='none' className='grow flex flex-col gap-2 justify-center'>
@@ -79,9 +80,16 @@ export const ConfirmResetImpl = ({
79
80
  {...{ validationMessage }}
80
81
  label={
81
82
  <StepHeading className='text-start mlb-2'>
82
- {t(mode === 'join new identity' ? 'join new identity input label' : 'reset storage input label', {
83
- confirmationValue,
84
- })}
83
+ {t(
84
+ mode === 'join new identity'
85
+ ? 'join new identity input label'
86
+ : mode === 'recover'
87
+ ? 'recover reset input label'
88
+ : 'reset storage input label',
89
+ {
90
+ confirmationValue,
91
+ },
92
+ )}
85
93
  </StepHeading>
86
94
  }
87
95
  disabled={disabled}
@@ -14,7 +14,7 @@ import { type Space, type SpaceMember, useSpaces } from '@dxos/react-client/echo
14
14
  import { useIdentity } from '@dxos/react-client/halo';
15
15
  import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
16
16
  import { ConnectionState, useNetworkStatus } from '@dxos/react-client/mesh';
17
- import { ClientRepeater } from '@dxos/react-client/testing';
17
+ import { useMultiClient, withMultiClientProvider } from '@dxos/react-client/testing';
18
18
  import { Button, ButtonGroup, List } from '@dxos/react-ui';
19
19
  import { getSize, groupSurface } from '@dxos/react-ui-theme';
20
20
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -24,7 +24,7 @@ import { IdentityPanel, JoinPanel, SpacePanel } from '../panels';
24
24
  import { osTranslations } from '../translations';
25
25
 
26
26
  export default {
27
- title: 'Invitations',
27
+ title: 'sdk/shell/Invitations',
28
28
  };
29
29
 
30
30
  export type PanelType = Space | 'identity' | 'devices' | 'join';
@@ -113,8 +113,8 @@ const Panel = ({ id, panel, setPanel }: { id: number; panel?: PanelType; setPane
113
113
  }
114
114
  };
115
115
 
116
- const Invitations = (args: { id: number; count: number }) => {
117
- const { id }: { id: number } = args;
116
+ const Invitations = () => {
117
+ const { id } = useMultiClient();
118
118
  const client = useClient();
119
119
  const networkStatus = useNetworkStatus().swarm;
120
120
  const identity = useIdentity();
@@ -220,12 +220,19 @@ const Invitations = (args: { id: number; count: number }) => {
220
220
  // The issue appears to be related to dynamic imports during client initialization.
221
221
  // This does not seem to be a problem in other browsers nor in Safari in the app.
222
222
  export const Default = {
223
- render: (args: { id: number }) => (
224
- <ClipboardProvider>
225
- <ClientRepeater component={Invitations} count={3} />
226
- </ClipboardProvider>
227
- ),
228
- decorators: [withTheme, withLayout({ tooltips: true })],
223
+ render: () => {
224
+ return (
225
+ // TODO(wittjosiah): Factor out ClipboardProvider to react-ui and include in layout decorator.
226
+ <ClipboardProvider>
227
+ <Invitations />
228
+ </ClipboardProvider>
229
+ );
230
+ },
231
+ decorators: [
232
+ withMultiClientProvider({ numClients: 3 }),
233
+ withLayout({ tooltips: true, classNames: 'grid grid-cols-3' }),
234
+ withTheme,
235
+ ],
229
236
  parameters: {
230
237
  chromatic: { disableSnapshot: true },
231
238
  translations: [osTranslations],
@@ -153,4 +153,6 @@ export const os = {
153
153
  'agent requested description':
154
154
  'The agent service is creating an agent for you. Once the agent connects, it will display in the devices list.',
155
155
  'authenticating label': 'Authenticating…',
156
+ 'create recovery code label': 'Create recovery code',
157
+ 'choose recover identity label': 'Recover identity',
156
158
  };