@dxos/shell 0.3.11-main.fbbdc2a → 0.3.11-main.fc97a54

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 (137) hide show
  1. package/dist/bundle/shell.mjs +38268 -0
  2. package/dist/bundle/shell.mjs.map +1 -0
  3. package/dist/bundle/shell.umd.js +216 -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 +29 -6
  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/InvitationRescuer.js.map +1 -1
  74. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js +1 -1
  75. package/dist/types/src/panels/JoinPanel/steps/ResetIdentity.js.map +1 -1
  76. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -1
  77. package/dist/types/src/panels/SpacePanel/SpacePanel.js +5 -3
  78. package/dist/types/src/panels/SpacePanel/SpacePanel.js.map +1 -1
  79. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -1
  80. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js +2 -2
  81. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js.map +1 -1
  82. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts +1 -0
  83. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts.map +1 -1
  84. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts +1 -0
  85. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts.map +1 -1
  86. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js +3 -1
  87. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js.map +1 -1
  88. package/dist/types/src/playwright/invitations-manager.d.ts +1 -0
  89. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  90. package/dist/types/src/playwright/invitations-manager.js +4 -1
  91. package/dist/types/src/playwright/invitations-manager.js.map +1 -1
  92. package/dist/types/src/playwright/invitations.spec.js +15 -0
  93. package/dist/types/src/playwright/invitations.spec.js.map +1 -1
  94. package/dist/types/src/stories/Invitations.e2e-stories.js +3 -3
  95. package/dist/types/src/stories/Invitations.e2e-stories.js.map +1 -1
  96. package/dist/types/src/styles/invitationStatusStyles.d.ts.map +1 -1
  97. package/dist/types/src/styles/invitationStatusStyles.js.map +1 -1
  98. package/dist/types/src/testing/shell-manager.d.ts +5 -4
  99. package/dist/types/src/testing/shell-manager.d.ts.map +1 -1
  100. package/dist/types/src/testing/shell-manager.js +36 -12
  101. package/dist/types/src/testing/shell-manager.js.map +1 -1
  102. package/dist/types/src/translations/locales/en-US.d.ts +6 -0
  103. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  104. package/dist/types/src/translations/locales/en-US.js +7 -1
  105. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  106. package/package.json +22 -21
  107. package/src/components/IdentityList/DeviceList.tsx +7 -5
  108. package/src/components/IdentityList/SpaceMemberList.tsx +1 -1
  109. package/src/components/Panel/Action.tsx +4 -19
  110. package/src/composites/Shell/Shell.tsx +16 -3
  111. package/src/panels/IdentityPanel/IdentityPanel.stories.tsx +45 -3
  112. package/src/panels/IdentityPanel/IdentityPanel.tsx +35 -8
  113. package/src/panels/IdentityPanel/IdentityPanelProps.ts +27 -8
  114. package/src/panels/IdentityPanel/identityMachine.ts +13 -4
  115. package/src/panels/IdentityPanel/steps/AgentForm.tsx +61 -0
  116. package/src/panels/IdentityPanel/steps/DeviceManager.tsx +41 -0
  117. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +13 -4
  118. package/src/panels/IdentityPanel/steps/ProfileForm.tsx +1 -1
  119. package/src/panels/IdentityPanel/steps/SignOutChooser.tsx +84 -0
  120. package/src/panels/IdentityPanel/steps/index.ts +3 -0
  121. package/src/panels/IdentityPanel/useAgentHandlers.ts +88 -0
  122. package/src/panels/JoinPanel/JoinPanel.tsx +2 -2
  123. package/src/panels/JoinPanel/joinMachine.ts +16 -19
  124. package/src/panels/JoinPanel/steps/AdditionMethodChooser.tsx +3 -3
  125. package/src/panels/JoinPanel/steps/InvitationInput.tsx +7 -1
  126. package/src/panels/JoinPanel/steps/InvitationRescuer.tsx +2 -2
  127. package/src/panels/JoinPanel/steps/ResetIdentity.tsx +1 -1
  128. package/src/panels/SpacePanel/SpacePanel.stories.tsx +2 -1
  129. package/src/panels/SpacePanel/SpacePanel.tsx +4 -1
  130. package/src/panels/SpacePanel/SpacePanelProps.ts +1 -0
  131. package/src/panels/SpacePanel/steps/SpaceManager.tsx +4 -1
  132. package/src/playwright/invitations-manager.ts +5 -1
  133. package/src/playwright/invitations.spec.ts +19 -0
  134. package/src/stories/Invitations.e2e-stories.tsx +2 -2
  135. package/src/styles/invitationStatusStyles.ts +6 -6
  136. package/src/testing/shell-manager.ts +40 -13
  137. package/src/translations/locales/en-US.ts +7 -1
@@ -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;
@@ -35,7 +35,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
35
35
  before={<Plus className={getSize(6)} />}
36
36
  onClick={() => send({ type: 'createIdentity' })}
37
37
  data-autofocus='choosingAuthMethod'
38
- data-testid='create-identity'
38
+ data-testid='identity-chooser.create-identity'
39
39
  >
40
40
  {t('create identity label')}
41
41
  </CompoundButton>
@@ -44,7 +44,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
44
44
  description={t('join identity description')}
45
45
  before={<QrCode className={getSize(6)} />}
46
46
  onClick={() => send({ type: 'acceptHaloInvitation' })}
47
- data-testid='join-identity'
47
+ data-testid='identity-chooser.join-identity'
48
48
  >
49
49
  {t('join identity label')}
50
50
  </CompoundButton>
@@ -55,7 +55,7 @@ export const AdditionMethodChooser = (viewStateProps: AdditionMethodChooserProps
55
55
  description={t('recover identity description')}
56
56
  before={<Textbox className={getSize(6)} />}
57
57
  onClick={() => send({ type: 'recoverIdentity' })}
58
- data-testid='recover-identity'
58
+ data-testid='identity-chooser.recover-identity'
59
59
  >
60
60
  {t('recover identity label')}
61
61
  </CompoundButton>
@@ -39,12 +39,17 @@ export const InvitationInput = (props: InvitationInputProps) => {
39
39
  unredeemedCode && setInputValue(unredeemedCode ?? '');
40
40
  }, [unredeemedCode]);
41
41
 
42
- const handleNext = () =>
42
+ const handleNext = () => {
43
+ if (inputValue.trim().length === 0) {
44
+ return;
45
+ }
46
+
43
47
  send({
44
48
  type: `set${Kind}InvitationCode`,
45
49
  code: invitationCodeFromUrl(inputValue),
46
50
  ...(Kind === 'Space' && { succeededKeys }),
47
51
  });
52
+ };
48
53
 
49
54
  const exitAction = (
50
55
  <Action
@@ -62,6 +67,7 @@ export const InvitationInput = (props: InvitationInputProps) => {
62
67
  <div role='none' className='grow flex flex-col justify-center'>
63
68
  <Input
64
69
  label={<StepHeading>{t('invitation input label')}</StepHeading>}
70
+ // TODO(burdon): Translation.
65
71
  placeholder='Type an invitation code'
66
72
  disabled={disabled}
67
73
  value={inputValue}
@@ -39,8 +39,8 @@ const InvitationActions = ({
39
39
  failReason === 'timeout'
40
40
  ? 'timeout status label'
41
41
  : failReason === 'cancelled'
42
- ? 'cancelled status label'
43
- : 'error status label',
42
+ ? 'cancelled status label'
43
+ : 'error status label',
44
44
  )}
45
45
  </StepHeading>
46
46
  <div role='none' className='grow' />
@@ -50,7 +50,7 @@ export const ResetIdentityImpl = ({ disabled, validationMessage, onConfirm }: Re
50
50
  {...{ validationMessage }}
51
51
  label={<StepHeading>{t('reset identity input label', { confirmationValue })}</StepHeading>}
52
52
  disabled={disabled}
53
- data-testid='identity-input'
53
+ data-testid='reset-identity-input'
54
54
  placeholder={confirmationValue}
55
55
  onChange={({ target: { value } }) => setInputValue(value)}
56
56
  />
@@ -129,6 +129,7 @@ export const SpaceManagerWithEvenMoreInvites = () => {
129
129
  identityKey = PublicKey.random(),
130
130
  swarmKey,
131
131
  spaceKey = PublicKey.random(),
132
+ target = null,
132
133
  } = invitation;
133
134
  return (
134
135
  <InvitationListItemImpl
@@ -140,7 +141,7 @@ export const SpaceManagerWithEvenMoreInvites = () => {
140
141
  invitationCode: id,
141
142
  authCode: state === Invitation.State.READY_FOR_AUTHENTICATION ? '123414' : undefined,
142
143
  id,
143
- result: { identityKey, swarmKey, spaceKey },
144
+ result: { identityKey, swarmKey, spaceKey, target },
144
145
  cancel: () => {},
145
146
  authenticate: async (authCode: string) => {},
146
147
  connect: () => {},
@@ -39,17 +39,19 @@ export const SpacePanelImpl = (props: SpacePanelImplProps) => {
39
39
  titleId,
40
40
  activeView,
41
41
  space,
42
+ target,
42
43
  SpaceManager: SpaceManagerComponent = SpaceManager,
43
44
  InvitationManager: InvitationManagerComponent = InvitationManager,
44
45
  ...rest
45
46
  } = props;
47
+
46
48
  return (
47
49
  <DensityProvider density='fine'>
48
50
  <SpacePanelHeading {...rest} {...{ titleId, space }} />
49
51
  <Viewport.Root activeView={activeView}>
50
52
  <Viewport.Views>
51
53
  <Viewport.View id='space manager' classNames={stepStyles}>
52
- <SpaceManagerComponent active={activeView === 'space manager'} space={space} {...rest} />
54
+ <SpaceManagerComponent active={activeView === 'space manager'} space={space} target={target} {...rest} />
53
55
  </Viewport.View>
54
56
  <Viewport.View id='space invitation manager' classNames={stepStyles}>
55
57
  <InvitationManagerComponent
@@ -115,6 +117,7 @@ export const SpacePanel = ({
115
117
  };
116
118
 
117
119
  return spaceState.context.invitation ? (
120
+ // TODO(wittjosiah): Unused? Remove?
118
121
  <SpacePanelWithInvitationImpl {...implProps} invitation={spaceState.context.invitation} />
119
122
  ) : (
120
123
  <SpacePanelImpl {...implProps} />
@@ -16,6 +16,7 @@ export type SpacePanelImplProps = {
16
16
  titleId: string;
17
17
  activeView: string;
18
18
  send: (event: SingleOrArray<Event<any>>) => void;
19
+ target?: string;
19
20
  createInvitationUrl: (invitationCode: string) => string;
20
21
  space: ErsatzSpace | Space;
21
22
  invitationUrl?: string;
@@ -23,6 +23,7 @@ import {
23
23
  import { type SpacePanelStepProps } from '../SpacePanelProps';
24
24
 
25
25
  export type SpaceManagerImplProps = SpacePanelStepProps & {
26
+ target?: string;
26
27
  invitations?: CancellableInvitationObservable[];
27
28
  showInactiveInvitations?: boolean;
28
29
  inviteActions?: Record<string, ActionMenuItem>;
@@ -35,7 +36,7 @@ const activeActionKey = 'dxos:react-shell/space-manager/active-action';
35
36
  export type SpaceManagerProps = SpaceManagerImplProps & {};
36
37
 
37
38
  export const SpaceManager = (props: SpaceManagerProps) => {
38
- const { space } = props;
39
+ const { space, target } = props;
39
40
  const { t } = useTranslation('os');
40
41
  const config = useConfig();
41
42
 
@@ -57,6 +58,7 @@ export const SpaceManager = (props: SpaceManagerProps) => {
57
58
  const invitation = space.share?.({
58
59
  type: Invitation.Type.INTERACTIVE,
59
60
  authMethod: Invitation.AuthMethod.SHARED_SECRET,
61
+ target,
60
62
  });
61
63
  if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {
62
64
  invitation.subscribe(onInvitationEvent);
@@ -71,6 +73,7 @@ export const SpaceManager = (props: SpaceManagerProps) => {
71
73
  const invitation = space.share?.({
72
74
  type: Invitation.Type.MULTIUSE,
73
75
  authMethod: Invitation.AuthMethod.NONE,
76
+ target,
74
77
  });
75
78
  if (invitation && config.values.runtime?.app?.env?.DX_ENVIRONMENT !== 'production') {
76
79
  invitation.subscribe(onInvitationEvent);
@@ -70,6 +70,10 @@ export class InvitationsManager extends ScopedShellManager {
70
70
  return this.peer(id).getByTestId('space-list-item').nth(nth).textContent();
71
71
  }
72
72
 
73
+ async getSpaceMembersCount(id: number) {
74
+ return this.peer(id).getByTestId('space-members-list').locator('li').count();
75
+ }
76
+
73
77
  async getAuthCode(): Promise<string> {
74
78
  return await this._authCode.wait();
75
79
  }
@@ -147,7 +151,7 @@ export class InvitationsManager extends ScopedShellManager {
147
151
  const peer = this.peer(id);
148
152
  // TODO(wittjosiah): Update ids.
149
153
  if (type === 'device') {
150
- await peer.getByTestId('join-identity').click();
154
+ await peer.getByTestId('identity-chooser.join-identity').click();
151
155
  }
152
156
  await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input`).waitFor({ timeout: 500 });
153
157
  await peer.getByTestId(`${type === 'device' ? 'halo' : 'space'}-invitation-input`).fill(invitation);
@@ -185,6 +185,25 @@ test.describe('Invitations', () => {
185
185
  expect(await manager.getSpaceName(0, 1)).to.equal(await manager.getSpaceName(1, 1));
186
186
  });
187
187
 
188
+ test('already joined', async () => {
189
+ await manager.createIdentity(0);
190
+ await manager.createSpace(0);
191
+ await manager.openPanel(0, 1);
192
+ const invitation1 = await manager.createInvitation(0, 'space', { authMethod: Invitation.AuthMethod.NONE });
193
+ const invitation2 = await manager.createInvitation(0, 'space');
194
+
195
+ await manager.createIdentity(1);
196
+ await manager.openPanel(1, 'join');
197
+ await manager.acceptInvitation(1, 'space', invitation1);
198
+ await manager.doneInvitation('space', manager.peer(1));
199
+
200
+ await manager.openPanel(1, 'join');
201
+ await manager.acceptInvitation(1, 'space', invitation2);
202
+ await manager.doneInvitation('space', manager.peer(1));
203
+
204
+ expect(await manager.getSpaceMembersCount(0)).to.equal(2);
205
+ });
206
+
188
207
  test('no auth method', async () => {
189
208
  await manager.createIdentity(0);
190
209
  await manager.createSpace(0);
@@ -4,10 +4,10 @@
4
4
 
5
5
  import '@dxosTheme';
6
6
 
7
- import { faker } from '@faker-js/faker';
8
7
  import { Laptop, Planet, Plus, PlusCircle, SignIn, QrCode, WifiHigh, WifiSlash } from '@phosphor-icons/react';
9
8
  import React, { useMemo, useState } from 'react';
10
9
 
10
+ import { faker } from '@dxos/random';
11
11
  import { useClient } from '@dxos/react-client';
12
12
  import { type Space, type SpaceMember, SpaceProxy, useSpaces } from '@dxos/react-client/echo';
13
13
  import { useIdentity } from '@dxos/react-client/halo';
@@ -72,7 +72,7 @@ const Panel = ({ id, panel, setPanel }: { id: number; panel?: PanelType; setPane
72
72
  <ButtonGroup classNames='mbe-4'>
73
73
  {/* <Tooltip content='Create Space'> */}
74
74
  <Button
75
- onClick={() => client.spaces.create({ name: faker.animal.bird() })}
75
+ onClick={() => client.spaces.create({ name: faker.commerce.productName() })}
76
76
  data-testid='invitations.create-space'
77
77
  >
78
78
  <PlusCircle className={getSize(6)} />
@@ -26,17 +26,17 @@ export const resolvedStrokeColor = (status: Invitation.State) =>
26
26
  status === Invitation.State.ERROR
27
27
  ? errorStrokeColor
28
28
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
29
- ? cancelledStrokeColor
30
- : successStrokeColor;
29
+ ? cancelledStrokeColor
30
+ : successStrokeColor;
31
31
  export const resolvedBgColor = (status: Invitation.State) =>
32
32
  status === Invitation.State.ERROR
33
33
  ? errorBgColor
34
34
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
35
- ? cancelledBgColor
36
- : successBgColor;
35
+ ? cancelledBgColor
36
+ : successBgColor;
37
37
  export const resolvedTextColor = (status: Invitation.State) =>
38
38
  status === Invitation.State.ERROR
39
39
  ? errorTextColor
40
40
  : status === Invitation.State.CANCELLED || status === Invitation.State.TIMEOUT
41
- ? cancelledTextColor
42
- : successTextColor;
41
+ ? cancelledTextColor
42
+ : successTextColor;
@@ -2,12 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import type { ConsoleMessage, Page } from '@playwright/test';
5
+ import type { ConsoleMessage, Dialog, Page } from '@playwright/test';
6
6
 
7
7
  import { sleep, Trigger } from '@dxos/async';
8
8
 
9
9
  import { ScopedShellManager } from './scoped-shell-manager';
10
10
 
11
+ // TODO(wittjosiah): Normalize data-testids between snake and camel case.
12
+
11
13
  export class ShellManager extends ScopedShellManager {
12
14
  private _invitationCode = new Trigger<string>();
13
15
  private _authCode = new Trigger<string>();
@@ -27,6 +29,10 @@ export class ShellManager extends ScopedShellManager {
27
29
 
28
30
  // Getters
29
31
 
32
+ async getDisplayName(): Promise<string | null> {
33
+ return this.shell.getByTestId('identityHeading.displayName').textContent();
34
+ }
35
+
30
36
  async isCurrentSpaceViewVisible(): Promise<boolean> {
31
37
  return await this.shell.getByTestId('current-space-view').isVisible();
32
38
  }
@@ -44,29 +50,50 @@ export class ShellManager extends ScopedShellManager {
44
50
  await this.shell.getByTestId('halo-invitation-accepted-done').click();
45
51
  }
46
52
 
47
- async closeShell() {
48
- await this.page.keyboard.press('Escape');
53
+ async createDeviceInvitation(): Promise<string> {
54
+ this._invitationCode = new Trigger<string>();
55
+ this._authCode = new Trigger<string>();
56
+ await this.shell.getByTestId('devices-panel.create-invitation').click();
57
+ return await this._invitationCode.wait();
49
58
  }
50
59
 
51
- // TODO(wittjosiah): These shortcuts are no longer within the SDK so these commands should live elsewhere.
52
- async openCurrentSpace() {
53
- await this.page.keyboard.press('Meta+.');
60
+ async resetDevice() {
61
+ const handleDialog = async (dialog: Dialog) => {
62
+ if (dialog.type() === 'confirm') {
63
+ await dialog.accept();
64
+ }
65
+ };
66
+
67
+ this.page.on('dialog', handleDialog);
68
+ await this.shell.getByTestId('devices-panel.sign-out').click();
69
+ await this.shell.getByTestId('sign-out.reset-device').click();
70
+ this.page.off('dialog', handleDialog);
54
71
  }
55
72
 
56
- async openSpaceList() {
57
- await this.page.keyboard.press('Meta+Shift+>');
73
+ async joinNewIdentity(invitationCode: string) {
74
+ await this.shell.getByTestId('devices-panel.sign-out').click();
75
+ await this.shell.getByTestId('sign-out.join-new-identity').click();
76
+ await this.shell.getByTestId('reset-identity-input').fill('CONFIRM');
77
+ await this.shell.getByTestId('reset-identity-input-confirm').click();
78
+ await this.shell.getByTestId('identity-chooser.join-identity').click();
79
+ await this.inputInvitation('device', invitationCode, this.shell);
58
80
  }
59
81
 
60
- async showAllSpaces() {
61
- await this.shell.getByTestId('show-all-spaces').click();
82
+ async authenticateDevice(authCode: string, skipDone = true) {
83
+ // Wait for focus to shift before typing.
84
+ await sleep(1500);
85
+ await this.authenticateInvitation('device', authCode, this.shell);
86
+ // TODO(wittjosiah): When "signing out" and joinging a new identity, the done step seems to be skipped.
87
+ if (!skipDone) {
88
+ await this.doneInvitation('device', this.shell);
89
+ }
62
90
  }
63
91
 
64
- async showCurrentSpace() {
65
- await this.shell.getByTestId('show-current-space').click();
92
+ async closeShell() {
93
+ await this.page.keyboard.press('Escape');
66
94
  }
67
95
 
68
96
  async createSpaceInvitation(): Promise<string> {
69
- await this.openCurrentSpace();
70
97
  this._invitationCode = new Trigger<string>();
71
98
  this._authCode = new Trigger<string>();
72
99
  await this.shell.getByTestId('spaces-panel.create-invitation').click();
@@ -69,7 +69,8 @@ export const os = {
69
69
  'selecting identity heading': 'Selecting identity',
70
70
  'devices heading': 'Devices',
71
71
  'identity heading': 'Profile settings',
72
- 'choose devices label': 'Add device',
72
+ 'choose add device label': 'Add device',
73
+ 'choose devices label': 'Manage devices',
73
74
  'choose profile label': 'Edit profile',
74
75
  'choose sign out label': 'Sign out',
75
76
  'empty space members message': 'There is nobody in this space yet.',
@@ -98,4 +99,9 @@ export const os = {
98
99
  'This will clear this device and all associated data. Type {{confirmationValue}} and confirm to continue.',
99
100
  'failed to reset identity message': 'Failed to reset identity.',
100
101
  'confirmation value': 'CONFIRM',
102
+ 'sign out chooser title': 'DANGER ZONE',
103
+ 'sign out chooser message': 'Proceeding with either action below will erase all data on this device.',
104
+ 'join new identity label': 'Join a new identity',
105
+ 'reset device label': 'Reset device',
106
+ 'reset device confirm message': 'Are you sure you want to reset your device? All data will be lost.',
101
107
  };