@dxos/react-ui 0.1.31 → 0.1.32-next.86f5578

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 (62) hide show
  1. package/dist/lib/browser/index.mjs +1018 -870
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -1
  5. package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -1
  6. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +2 -2
  7. package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -1
  8. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +1 -1
  9. package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -1
  10. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +1 -1
  11. package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -1
  12. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts +64 -0
  13. package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -0
  14. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +2 -5
  15. package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -1
  16. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts +1 -1
  17. package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -1
  18. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts +2 -2
  19. package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -1
  20. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts +2 -2
  21. package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -1
  22. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts +2 -2
  23. package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -1
  24. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +4 -6
  25. package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -1
  26. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts +0 -1
  27. package/dist/types/src/panels/JoinPanel/view-states/index.d.ts.map +1 -1
  28. package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
  29. package/dist/types/src/translations/locales/en-US.d.ts +3 -1
  30. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  31. package/dist/types/src/util/index.d.ts +1 -0
  32. package/dist/types/src/util/index.d.ts.map +1 -1
  33. package/dist/types/src/util/toEmoji.d.ts +2 -0
  34. package/dist/types/src/util/toEmoji.d.ts.map +1 -0
  35. package/package.json +18 -14
  36. package/src/components/IdentityList/SpaceMemberListContainer.tsx +6 -2
  37. package/src/components/InvitationList/InvitationList.stories.tsx +1 -1
  38. package/src/composites/JoinDialog/JoinDialog.stories.tsx +4 -2
  39. package/src/composites/Shell/Shell.stories.tsx +1 -1
  40. package/src/composites/Shell/Shell.tsx +1 -1
  41. package/src/panels/JoinPanel/JoinHeading.tsx +42 -40
  42. package/src/panels/JoinPanel/JoinPanel.tsx +140 -218
  43. package/src/panels/JoinPanel/JoinPanelProps.ts +1 -1
  44. package/src/panels/JoinPanel/joinMachine.ts +418 -0
  45. package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +9 -21
  46. package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +6 -6
  47. package/src/panels/JoinPanel/view-states/IdentityInput.tsx +4 -4
  48. package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +22 -18
  49. package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +37 -28
  50. package/src/panels/JoinPanel/view-states/InvitationInput.tsx +21 -21
  51. package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +58 -41
  52. package/src/panels/JoinPanel/view-states/ViewState.tsx +11 -62
  53. package/src/panels/JoinPanel/view-states/index.ts +0 -1
  54. package/src/playwright/invitations-manager.ts +6 -2
  55. package/src/playwright/invitations.spec.ts +14 -5
  56. package/src/stories/Invitations.stories.tsx +2 -2
  57. package/src/translations/locales/en-US.ts +6 -4
  58. package/src/util/index.ts +1 -0
  59. package/src/util/toEmoji.ts +22 -0
  60. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +0 -7
  61. package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +0 -1
  62. package/src/panels/JoinPanel/view-states/IdentitySelector.tsx +0 -100
@@ -0,0 +1,418 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { useMachine } from '@xstate/react';
6
+ import { useCallback } from 'react';
7
+ import { assign, createMachine, InterpreterFrom, StateFrom } from 'xstate';
8
+ import type { StateNodeConfig, Subscribable, Subscription } from 'xstate';
9
+
10
+ import type { Identity, AuthenticatingInvitationObservable, Client } from '@dxos/client';
11
+ import { Invitation, InvitationEncoder } from '@dxos/client';
12
+ import { log } from '@dxos/log';
13
+
14
+ import { JoinPanelMode } from './JoinPanelProps';
15
+
16
+ type FailReason = 'error' | 'timeout' | 'cancelled' | 'badVerificationCode';
17
+
18
+ type InvitationDomainContext = Partial<{
19
+ failReason: FailReason | null;
20
+ unredeemedCode: string;
21
+ invitationObservable: AuthenticatingInvitationObservable;
22
+ invitation: Invitation;
23
+ invitationSubscribable: Subscribable<InvitationEvent>;
24
+ }>;
25
+
26
+ type JoinMachineContext = {
27
+ mode: JoinPanelMode;
28
+ identity: Identity | null;
29
+ halo: InvitationDomainContext;
30
+ space: InvitationDomainContext;
31
+ };
32
+
33
+ type SelectIdentityEvent = {
34
+ type: 'selectIdentity';
35
+ identity: Identity;
36
+ };
37
+
38
+ type SetInvitationCodeEvent = {
39
+ type: 'setHaloInvitationCode' | 'setSpaceInvitationCode';
40
+ code: string;
41
+ };
42
+
43
+ type SetInvitationEvent = {
44
+ type:
45
+ | 'authenticateHaloInvitation'
46
+ | 'authenticateSpaceInvitation'
47
+ | 'connectHaloInvitation'
48
+ | 'connectSpaceInvitation'
49
+ | 'connectionSuccessHaloInvitation'
50
+ | 'connectionSuccessSpaceInvitation';
51
+ invitation: Invitation;
52
+ };
53
+
54
+ type FailInvitationEvent = {
55
+ type: 'failHaloInvitation' | 'failSpaceInvitation';
56
+ reason: FailReason;
57
+ };
58
+
59
+ type EmptyInvitationEvent = {
60
+ type:
61
+ | 'authenticateHaloVerificationCode'
62
+ | 'authenticateSpaceVerificationCode'
63
+ | 'resetHaloInvitation'
64
+ | 'resetSpaceInvitation'
65
+ | 'succeedHaloInvitation'
66
+ | 'succeedSpaceInvitation';
67
+ };
68
+
69
+ type InvitationEvent = FailInvitationEvent | SetInvitationCodeEvent | SetInvitationEvent | EmptyInvitationEvent;
70
+
71
+ const getInvitationSubscribable = (
72
+ Domain: 'Space' | 'Halo',
73
+ invitation: AuthenticatingInvitationObservable
74
+ ): Subscribable<InvitationEvent> => {
75
+ log('[subscribing to invitation]', invitation);
76
+ return {
77
+ subscribe: (
78
+ next: (value: InvitationEvent) => void,
79
+ onError?: (error: any) => void,
80
+ complete?: () => void
81
+ ): Subscription => {
82
+ const unsubscribe = invitation.subscribe({
83
+ onAuthenticating: (invitation: Invitation) => {
84
+ log('[invitation authenticating]', { Domain, invitation });
85
+ return next({ type: `authenticate${Domain}Invitation`, invitation });
86
+ },
87
+ onCancelled: () => {
88
+ log.warn('[invitation cancelled]', { Domain });
89
+ return next({ type: `fail${Domain}Invitation`, reason: 'cancelled' } as FailInvitationEvent);
90
+ },
91
+ onTimeout: () => {
92
+ log.error('[invitation timeout]', { Domain });
93
+ return next({ type: `fail${Domain}Invitation`, reason: 'timeout' } as FailInvitationEvent);
94
+ },
95
+ onConnecting: (invitation: Invitation) => {
96
+ log('[invitation connecting]', { Domain, invitation });
97
+ return next({ type: `connect${Domain}Invitation`, invitation });
98
+ },
99
+ onConnected: (invitation: Invitation) => {
100
+ log('[invitation connected]', { Domain, invitation });
101
+ return next({ type: `connectionSuccess${Domain}Invitation`, invitation });
102
+ },
103
+ onSuccess: () => {
104
+ log('[invitation success]', { Domain });
105
+ next({ type: `succeed${Domain}Invitation` });
106
+ return complete?.();
107
+ },
108
+ onError: (error: any) => {
109
+ log.error('[invitation errored]', { Domain, error });
110
+ next({ type: `fail${Domain}Invitation`, reason: 'error' });
111
+ return onError?.(error);
112
+ }
113
+ });
114
+ return { unsubscribe };
115
+ }
116
+ } as Subscribable<InvitationEvent>;
117
+ };
118
+
119
+ const acceptingInvitationTemplate = (Domain: 'Space' | 'Halo', successTarget: string) => {
120
+ const config: StateNodeConfig<JoinMachineContext, any, InvitationEvent> = {
121
+ initial: `unknown${Domain}`,
122
+ states: {
123
+ [`unknown${Domain}`]: {
124
+ always: [
125
+ {
126
+ cond: ({ mode }) => mode === 'halo-only' && Domain === 'Space',
127
+ target: '#join.finishingJoiningHalo'
128
+ },
129
+ {
130
+ // cond: `no${Domain}Invitation`,
131
+ target: `inputting${Domain}InvitationCode`,
132
+ actions: 'log'
133
+ }
134
+ // todo(thure): Restore this transition that redeems the invitation code on init.
135
+ // {
136
+ // target: `acceptingRedeemed${Domain}Invitation`,
137
+ // actions: [`redeem${Domain}InvitationCode`, 'log']
138
+ // }
139
+ ]
140
+ },
141
+ [`inputting${Domain}InvitationCode`]: {},
142
+ [`acceptingRedeemed${Domain}Invitation`]: {
143
+ invoke: {
144
+ src: (context) => context[Domain.toLowerCase() as 'space' | 'halo'].invitationSubscribable!
145
+ },
146
+ initial: `unknown${Domain}Invitation`,
147
+ states: {
148
+ [`unknown${Domain}Invitation`]: {
149
+ always: [
150
+ {
151
+ cond: (context) => {
152
+ const invitation = context[Domain.toLowerCase() as 'space' | 'halo'].invitation;
153
+ return !invitation || invitation?.state === Invitation.State.CONNECTING;
154
+ },
155
+ target: `connecting${Domain}Invitation`,
156
+ actions: 'log'
157
+ },
158
+ {
159
+ target: `inputting${Domain}VerificationCode`,
160
+ actions: 'log'
161
+ }
162
+ ]
163
+ },
164
+ [`connecting${Domain}Invitation`]: {},
165
+ [`inputting${Domain}VerificationCode`]: {
166
+ on: {
167
+ [`authenticate${Domain}VerificationCode`]: {
168
+ target: `authenticating${Domain}VerificationCode`,
169
+ actions: 'log'
170
+ }
171
+ }
172
+ },
173
+ [`authenticating${Domain}VerificationCode`]: {
174
+ on: {
175
+ [`authenticate${Domain}Invitation`]: {
176
+ target: `authenticationFailing${Domain}VerificationCode`,
177
+ actions: 'log'
178
+ }
179
+ }
180
+ },
181
+ [`authenticationFailing${Domain}VerificationCode`]: {},
182
+ [`failing${Domain}Invitation`]: {},
183
+ [`success${Domain}Invitation`]: {}
184
+ },
185
+ on: {
186
+ [`reset${Domain}Invitation`]: {
187
+ target: `#join${
188
+ Domain === 'Halo' ? '.choosingIdentity' : ''
189
+ }.accepting${Domain}Invitation.unknown${Domain}`,
190
+ actions: ['resetInvitation', 'log']
191
+ },
192
+ [`connect${Domain}Invitation`]: {
193
+ target: `.connecting${Domain}Invitation`,
194
+ actions: ['setInvitation', 'log']
195
+ },
196
+ [`connectionSuccess${Domain}Invitation`]: {
197
+ target: `.inputting${Domain}VerificationCode`,
198
+ actions: ['setInvitation', 'log']
199
+ },
200
+ [`succeed${Domain}Invitation`]: { target: successTarget, actions: 'log' },
201
+ [`fail${Domain}Invitation`]: {
202
+ target: `.failing${Domain}Invitation`,
203
+ actions: [
204
+ assign({
205
+ [Domain.toLowerCase() as 'space' | 'halo']: (
206
+ context: JoinMachineContext,
207
+ event: FailInvitationEvent
208
+ ) => ({
209
+ ...context[Domain.toLowerCase() as 'space' | 'halo'],
210
+ failReason: event.reason
211
+ })
212
+ }),
213
+ 'log'
214
+ ]
215
+ }
216
+ }
217
+ }
218
+ },
219
+ on: {
220
+ [`set${Domain}InvitationCode`]: {
221
+ target: `.acceptingRedeemed${Domain}Invitation`,
222
+ actions: [
223
+ assign({
224
+ [Domain.toLowerCase() as 'space' | 'halo']: (
225
+ context: JoinMachineContext,
226
+ event: SetInvitationCodeEvent
227
+ ) => ({
228
+ ...context[Domain.toLowerCase() as 'space' | 'halo'],
229
+ unredeemedCode: event.code
230
+ })
231
+ }),
232
+ `redeem${Domain}InvitationCode`,
233
+ 'log'
234
+ ]
235
+ }
236
+ }
237
+ };
238
+ return config as StateNodeConfig<JoinMachineContext, typeof config, JoinEvent>;
239
+ };
240
+
241
+ type EmptyJoinEvent = {
242
+ type:
243
+ | 'recoverIdentity'
244
+ | 'createIdentity'
245
+ | 'acceptHaloInvitation'
246
+ | 'addIdentity'
247
+ | 'selectIdentity'
248
+ | 'deselectAuthMethod';
249
+ };
250
+
251
+ type JoinEvent = InvitationEvent | SelectIdentityEvent | EmptyJoinEvent;
252
+
253
+ const joinMachine = createMachine<JoinMachineContext, JoinEvent>(
254
+ {
255
+ id: 'join',
256
+ predictableActionArguments: true,
257
+ context: {
258
+ mode: 'default',
259
+ identity: null,
260
+ halo: {},
261
+ space: {}
262
+ },
263
+ initial: 'unknown',
264
+ states: {
265
+ unknown: {
266
+ always: [
267
+ { cond: 'noSelectedIdentity', target: 'choosingIdentity', actions: 'log' },
268
+ { target: 'acceptingSpaceInvitation', actions: 'log' }
269
+ ]
270
+ },
271
+ choosingIdentity: {
272
+ initial: 'choosingAuthMethod',
273
+ states: {
274
+ choosingAuthMethod: {},
275
+ recoveringIdentity: {},
276
+ creatingIdentity: {},
277
+ acceptingHaloInvitation: acceptingInvitationTemplate('Halo', '#join.acceptingSpaceInvitation'),
278
+ confirmingAddedIdentity: {}
279
+ },
280
+ on: {
281
+ recoverIdentity: { target: '.recoveringIdentity', actions: 'log' },
282
+ createIdentity: { target: '.creatingIdentity', actions: 'log' },
283
+ acceptHaloInvitation: { target: '.acceptingHaloInvitation', actions: 'log' },
284
+ addIdentity: { target: '.confirmingAddedIdentity', actions: 'log' },
285
+ selectIdentity: {
286
+ target: 'acceptingSpaceInvitation',
287
+ actions: ['setIdentity', 'log']
288
+ },
289
+ deselectAuthMethod: { target: '.choosingAuthMethod', actions: 'log' }
290
+ }
291
+ },
292
+ acceptingSpaceInvitation: acceptingInvitationTemplate('Space', '#join.finishingJoiningSpace'),
293
+ finishingJoiningSpace: {
294
+ type: 'final'
295
+ },
296
+ finishingJoiningHalo: {
297
+ type: 'final'
298
+ }
299
+ }
300
+ },
301
+ {
302
+ guards: {
303
+ noSelectedIdentity: ({ identity }, _event) => !identity,
304
+ noHaloInvitation: ({ halo }, _event) => !halo.invitation && !halo.unredeemedCode,
305
+ noSpaceInvitation: ({ space }, _event) => !space.invitation && !space.unredeemedCode
306
+ },
307
+ actions: {
308
+ setIdentity: assign<JoinMachineContext, SelectIdentityEvent>({
309
+ identity: (context, event) => event.identity
310
+ }),
311
+ unsetIdentity: assign<JoinMachineContext>({
312
+ identity: () => null
313
+ }),
314
+ resetInvitation: assign<JoinMachineContext, EmptyInvitationEvent>({
315
+ halo: (context, event) =>
316
+ event.type === 'resetHaloInvitation'
317
+ ? {
318
+ ...context.halo,
319
+ invitation: undefined,
320
+ invitationObservable: undefined,
321
+ invitationSubscribable: undefined
322
+ }
323
+ : context.halo,
324
+ space: (context, event) =>
325
+ event.type === 'resetSpaceInvitation'
326
+ ? {
327
+ ...context.space,
328
+ invitation: undefined,
329
+ invitationObservable: undefined,
330
+ invitationSubscribable: undefined
331
+ }
332
+ : context.space
333
+ }),
334
+ setInvitation: assign<JoinMachineContext, SetInvitationEvent>({
335
+ halo: (context, event) =>
336
+ event.type.includes('Halo') ? { ...context.halo, invitation: event.invitation } : context.halo,
337
+ space: (context, event) =>
338
+ event.type.includes('Space') ? { ...context.space, invitation: event.invitation } : context.space
339
+ }),
340
+ log: (context, event) => {
341
+ log('[transition]', {
342
+ event,
343
+ haloInvitation: context.halo.invitation,
344
+ spaceInvitation: context.space.invitation
345
+ });
346
+ }
347
+ }
348
+ }
349
+ );
350
+
351
+ type JoinMachine = typeof joinMachine;
352
+
353
+ type JoinState = StateFrom<JoinMachine>;
354
+ type JoinSend = InterpreterFrom<JoinMachine>['send'];
355
+
356
+ const defaultCodeFromUrl = (invitationType: 'halo' | 'space', text: string) => {
357
+ try {
358
+ const searchParams = new URLSearchParams(text.substring(text.lastIndexOf('?')));
359
+ return (
360
+ searchParams.get(`${invitationType}InvitationCode`) ??
361
+ searchParams.get(`${invitationType}Invitation`) ??
362
+ searchParams.get('invitationCode') ??
363
+ searchParams.get('invitation') ??
364
+ text
365
+ );
366
+ } catch (err) {
367
+ log.catch(err);
368
+ return text;
369
+ }
370
+ };
371
+
372
+ const useJoinMachine = (client: Client, options?: Parameters<typeof useMachine<JoinMachine>>[1]) => {
373
+ const redeemHaloInvitationCode = useCallback(
374
+ ({ halo }: JoinMachineContext) => {
375
+ if (halo.unredeemedCode) {
376
+ const invitationObservable = client.halo.acceptInvitation(
377
+ InvitationEncoder.decode(defaultCodeFromUrl('halo', halo.unredeemedCode))
378
+ );
379
+ return {
380
+ ...halo,
381
+ invitationObservable,
382
+ invitationSubscribable: getInvitationSubscribable('Halo', invitationObservable)
383
+ };
384
+ } else {
385
+ return halo;
386
+ }
387
+ },
388
+ [client]
389
+ );
390
+ const redeemSpaceInvitationCode = useCallback(
391
+ ({ space }: JoinMachineContext) => {
392
+ if (space.unredeemedCode) {
393
+ const invitationObservable = client.acceptInvitation(
394
+ InvitationEncoder.decode(defaultCodeFromUrl('space', space.unredeemedCode))
395
+ );
396
+ return {
397
+ ...space,
398
+ invitationObservable,
399
+ invitationSubscribable: getInvitationSubscribable('Space', invitationObservable)
400
+ };
401
+ } else {
402
+ return space;
403
+ }
404
+ },
405
+ [client]
406
+ );
407
+ return useMachine(joinMachine, {
408
+ ...options,
409
+ actions: {
410
+ ...options?.actions,
411
+ redeemHaloInvitationCode: assign<JoinMachineContext>({ halo: redeemHaloInvitationCode }),
412
+ redeemSpaceInvitationCode: assign<JoinMachineContext>({ space: redeemSpaceInvitationCode })
413
+ }
414
+ });
415
+ };
416
+
417
+ export type { JoinMachine, JoinState, JoinSend, JoinEvent, JoinMachineContext };
418
+ export { joinMachine, useJoinMachine };
@@ -2,21 +2,18 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CaretLeft, CaretRight, Plus, QrCode, Textbox } from '@phosphor-icons/react';
5
+ import { CaretRight, Plus, QrCode, Textbox } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
- import type { Identity } from '@dxos/client';
9
- import { Button, CompoundButton, getSize, mx, useTranslation } from '@dxos/react-components';
8
+ import { CompoundButton, getSize, useTranslation } from '@dxos/react-components';
10
9
 
11
10
  import { ViewState, ViewStateHeading, ViewStateProps } from './ViewState';
12
11
 
13
- export interface AdditionMethodSelectorProps extends ViewStateProps {
14
- availableIdentities: Identity[];
15
- }
12
+ export type AdditionMethodSelectorProps = ViewStateProps;
16
13
 
17
- export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps }: AdditionMethodSelectorProps) => {
14
+ export const AdditionMethodSelector = (viewStateProps: AdditionMethodSelectorProps) => {
18
15
  const disabled = !viewStateProps.active;
19
- const { dispatch } = viewStateProps;
16
+ const { joinSend } = viewStateProps;
20
17
 
21
18
  const { t } = useTranslation('os');
22
19
 
@@ -34,8 +31,8 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
34
31
  {...sharedButtonProps}
35
32
  description={t('create identity description')}
36
33
  before={<Plus className={getSize(6)} />}
37
- onClick={() => dispatch({ type: 'select addition method', method: 'create identity' })}
38
- data-autofocus='addition method selector'
34
+ onClick={() => joinSend({ type: 'createIdentity' })}
35
+ data-autofocus='choosingAuthMethod'
39
36
  data-testid='create-identity'
40
37
  >
41
38
  {t('create identity label')}
@@ -44,7 +41,7 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
44
41
  {...sharedButtonProps}
45
42
  description={t('join identity description')}
46
43
  before={<QrCode className={getSize(6)} />}
47
- onClick={() => dispatch({ type: 'select addition method', method: 'accept device invitation' })}
44
+ onClick={() => joinSend({ type: 'acceptHaloInvitation' })}
48
45
  data-testid='join-identity'
49
46
  >
50
47
  {t('join identity label')}
@@ -55,21 +52,12 @@ export const AdditionMethodSelector = ({ availableIdentities, ...viewStateProps
55
52
  disabled={true}
56
53
  description={t('recover identity description')}
57
54
  before={<Textbox className={getSize(6)} />}
58
- onClick={() => dispatch({ type: 'select addition method', method: 'recover identity' })}
55
+ onClick={() => joinSend({ type: 'recoverIdentity' })}
59
56
  data-testid='recover-identity'
60
57
  >
61
58
  {t('recover identity label')}
62
59
  </CompoundButton>
63
60
  </div>
64
- <Button
65
- disabled={disabled || availableIdentities.length < 1}
66
- onClick={() => dispatch({ type: 'deselect identity' })}
67
- data-testid='deselect-identity'
68
- >
69
- <CaretLeft className={getSize(4)} weight='bold' />
70
- <span className='grow'>{t('deselect identity label')}</span>
71
- <CaretRight className={mx(getSize(4), 'invisible')} weight='bold' />
72
- </Button>
73
61
  </ViewState>
74
62
  );
75
63
  };
@@ -31,12 +31,12 @@ const Done = ({ onDone, doneActionParent, active }: DoneProps) => {
31
31
  {...(onDone && { onClick: () => onDone(null) })}
32
32
  disabled={disabled}
33
33
  className='grow flex items-center gap-2 pli-2'
34
- data-autofocus='identity added'
34
+ data-autofocus='confirmingAddedIdentity'
35
35
  data-testid='identity-added-done'
36
36
  >
37
37
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
38
38
  <span className='grow'>{t('done label')}</span>
39
- <Check weight='bold' className={getSize(4)} />
39
+ <Check className={getSize(4)} />
40
40
  </Button>
41
41
  );
42
42
 
@@ -51,7 +51,7 @@ export const IdentityAdded = ({
51
51
  ...viewStateProps
52
52
  }: IdentityAddedProps) => {
53
53
  const disabled = !viewStateProps.active;
54
- const { dispatch } = viewStateProps;
54
+ const { joinSend } = viewStateProps;
55
55
  const { t } = useTranslation('os');
56
56
 
57
57
  return (
@@ -77,8 +77,8 @@ export const IdentityAdded = ({
77
77
  <Button
78
78
  disabled={disabled || !addedIdentity}
79
79
  className='grow flex items-center gap-2 pli-2 order-2'
80
- onClick={() => addedIdentity && dispatch({ type: 'select identity', identity: addedIdentity })}
81
- data-autofocus='identity added'
80
+ onClick={() => addedIdentity && joinSend({ type: 'selectIdentity', identity: addedIdentity })}
81
+ data-autofocus='confirmingAddedIdentity'
82
82
  >
83
83
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
84
84
  <span className='grow'>{t('continue label')}</span>
@@ -86,7 +86,7 @@ export const IdentityAdded = ({
86
86
  </Button>
87
87
  <Button
88
88
  disabled={disabled}
89
- onClick={() => dispatch({ type: 'deselect identity' })}
89
+ onClick={() => joinSend({ type: 'deselectAuthMethod' })}
90
90
  className='flex items-center gap-2 pis-2 pie-4'
91
91
  >
92
92
  <CaretLeft weight='bold' className={getSize(4)} />
@@ -16,7 +16,7 @@ export interface IdentityCreatorProps extends ViewStateProps {
16
16
 
17
17
  export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProps) => {
18
18
  const disabled = !viewStateProps.active;
19
- const { dispatch } = viewStateProps;
19
+ const { joinSend } = viewStateProps;
20
20
  const { t } = useTranslation('os');
21
21
  const [inputValue, setInputValue] = useState('');
22
22
  const client = useClient();
@@ -25,7 +25,7 @@ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProp
25
25
  const handleNext = () => {
26
26
  void client.halo.createIdentity({ [isRecover ? 'seedphrase' : 'displayName']: inputValue }).then(
27
27
  (identity) => {
28
- dispatch({ type: 'added identity', identity });
28
+ joinSend({ type: 'selectIdentity', identity });
29
29
  },
30
30
  (_error) => {
31
31
  setValidationMessage(t(isRecover ? 'failed to recover identity message' : 'failed to create identity message'));
@@ -45,7 +45,7 @@ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProp
45
45
  slots={{
46
46
  root: { className: 'm-0' },
47
47
  input: {
48
- 'data-autofocus': isRecover ? 'identity input; recover identity' : 'identity input; create identity',
48
+ 'data-autofocus': isRecover ? 'recoveringIdentity' : 'creatingIdentity',
49
49
  onKeyUp: ({ key }) => key === 'Enter' && handleNext()
50
50
  } as ComponentPropsWithoutRef<'input'>
51
51
  }}
@@ -66,7 +66,7 @@ export const IdentityInput = ({ method, ...viewStateProps }: IdentityCreatorProp
66
66
  </Button>
67
67
  <Button
68
68
  disabled={disabled}
69
- onClick={() => dispatch({ type: 'add identity' })}
69
+ onClick={() => joinSend({ type: 'deselectAuthMethod' })}
70
70
  className='flex items-center gap-2 pis-2 pie-4'
71
71
  data-testid={`${method === 'recover identity' ? 'recover' : 'create'}-identity-input-back`}
72
72
  >
@@ -12,7 +12,7 @@ import { Button, getSize, mx, useTranslation } from '@dxos/react-components';
12
12
  import { ViewState, ViewStateProps } from './ViewState';
13
13
 
14
14
  export interface InvitationAcceptedProps extends ViewStateProps {
15
- invitationType: 'space' | 'halo';
15
+ Domain: 'Space' | 'Halo';
16
16
  doneActionParent?: Parameters<typeof cloneElement>[0];
17
17
  onDone?: (result: InvitationResult | null) => void;
18
18
  }
@@ -20,7 +20,7 @@ export interface InvitationAcceptedProps extends ViewStateProps {
20
20
  const PureInvitationAcceptedContent = ({
21
21
  onDone,
22
22
  result,
23
- invitationType,
23
+ Domain,
24
24
  doneActionParent,
25
25
  active
26
26
  }: InvitationAcceptedProps & { result: InvitationResult | null }) => {
@@ -31,39 +31,43 @@ const PureInvitationAcceptedContent = ({
31
31
  <Button
32
32
  {...(onDone && { onClick: () => onDone(result) })}
33
33
  disabled={disabled}
34
- className='grow flex items-center gap-2 pli-2'
35
- data-autofocus={`${invitationType} invitation acceptor; invitation accepted`}
36
- data-testid={`${invitationType}-invitation-accepted-done`}
34
+ className='flex items-center gap-2 pli-2'
35
+ data-autofocus={`success${Domain}Invitation finishingJoining${Domain}`}
36
+ data-testid={`${Domain.toLowerCase()}-invitation-accepted-done`}
37
37
  >
38
38
  <CaretLeft weight='bold' className={mx(getSize(2), 'invisible')} />
39
39
  <span className='grow'>{t('done label')}</span>
40
- <Check weight='bold' className={getSize(4)} />
40
+ <Check className={getSize(4)} />
41
41
  </Button>
42
42
  );
43
43
 
44
- return doneActionParent ? cloneElement(doneActionParent, {}, doneButton) : doneButton;
44
+ return (
45
+ <>
46
+ <p className='text-center text-sm font-system-normal'>{t('welcome message')}</p>
47
+ <div role='none' className='grow' />
48
+ {doneActionParent ? cloneElement(doneActionParent, {}, doneButton) : doneButton}
49
+ </>
50
+ );
45
51
  };
46
52
 
47
- const InvitationAcceptedContent = (props: InvitationAcceptedProps) => {
48
- const { result } = useInvitationStatus(props.activeInvitation as AuthenticatingInvitationObservable);
53
+ const InvitationAcceptedContent = (
54
+ props: InvitationAcceptedProps & { activeInvitation: AuthenticatingInvitationObservable }
55
+ ) => {
56
+ const { result } = useInvitationStatus(props.activeInvitation);
49
57
  return <PureInvitationAcceptedContent {...props} result={result} />;
50
58
  };
51
59
 
52
60
  export const InvitationAccepted = (props: InvitationAcceptedProps) => {
53
- const {
54
- invitationType: _invitationType,
55
- doneActionParent: _doneActionParent,
56
- onDone: _onDone,
57
- ...viewStateProps
58
- } = props;
59
- const { activeInvitation } = viewStateProps;
61
+ const { Domain, doneActionParent: _doneActionParent, onDone: _onDone, ...viewStateProps } = props;
62
+ const activeInvitation =
63
+ viewStateProps.joinState?.context[Domain.toLowerCase() as 'halo' | 'space'].invitationObservable;
60
64
 
61
65
  return (
62
66
  <ViewState {...viewStateProps}>
63
- {!activeInvitation || activeInvitation === true ? (
67
+ {!activeInvitation ? (
64
68
  <PureInvitationAcceptedContent {...props} result={null} />
65
69
  ) : (
66
- <InvitationAcceptedContent {...props} />
70
+ <InvitationAcceptedContent {...props} activeInvitation={activeInvitation} />
67
71
  )}
68
72
  </ViewState>
69
73
  );