@dxos/react-ui 0.1.31 → 0.1.32-next.77d513e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/index.mjs +1035 -876
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/IdentityList/SpaceMemberListContainer.d.ts.map +1 -1
- package/dist/types/src/composites/DevicesDialog/DevicesDialog.d.ts.map +1 -1
- package/dist/types/src/composites/JoinDialog/JoinDialog.d.ts.map +1 -1
- package/dist/types/src/composites/JoinDialog/JoinDialog.stories.d.ts.map +1 -1
- package/dist/types/src/composites/SpaceDialog/SpaceDialog.d.ts.map +1 -1
- package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts +2 -2
- package/dist/types/src/panels/JoinPanel/JoinHeading.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts +1 -1
- package/dist/types/src/panels/JoinPanel/JoinPanel.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts +1 -1
- package/dist/types/src/panels/JoinPanel/JoinPanelProps.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/joinMachine.d.ts +64 -0
- package/dist/types/src/panels/JoinPanel/joinMachine.d.ts.map +1 -0
- package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts +2 -5
- package/dist/types/src/panels/JoinPanel/view-states/AdditionMethodSelector.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAccepted.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts +2 -2
- package/dist/types/src/panels/JoinPanel/view-states/InvitationAuthenticator.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts +2 -2
- package/dist/types/src/panels/JoinPanel/view-states/InvitationInput.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts +2 -2
- package/dist/types/src/panels/JoinPanel/view-states/InvitationRescuer.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts +4 -6
- package/dist/types/src/panels/JoinPanel/view-states/ViewState.d.ts.map +1 -1
- package/dist/types/src/panels/JoinPanel/view-states/index.d.ts +0 -1
- package/dist/types/src/panels/JoinPanel/view-states/index.d.ts.map +1 -1
- package/dist/types/src/playwright/invitations-manager.d.ts.map +1 -1
- package/dist/types/src/translations/locales/en-US.d.ts +3 -1
- package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +1 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/toEmoji.d.ts +2 -0
- package/dist/types/src/util/toEmoji.d.ts.map +1 -0
- package/package.json +18 -14
- package/src/components/IdentityList/SpaceMemberListContainer.tsx +6 -2
- package/src/components/InvitationList/InvitationList.stories.tsx +1 -1
- package/src/composites/DevicesDialog/DevicesDialog.tsx +3 -2
- package/src/composites/JoinDialog/JoinDialog.stories.tsx +4 -2
- package/src/composites/JoinDialog/JoinDialog.tsx +4 -2
- package/src/composites/Shell/Shell.stories.tsx +1 -1
- package/src/composites/Shell/Shell.tsx +1 -1
- package/src/composites/SpaceDialog/SpaceDialog.tsx +3 -2
- package/src/panels/IdentityPanel/IdentityPanel.tsx +4 -2
- package/src/panels/JoinPanel/JoinHeading.tsx +42 -40
- package/src/panels/JoinPanel/JoinPanel.tsx +143 -220
- package/src/panels/JoinPanel/JoinPanelProps.ts +1 -1
- package/src/panels/JoinPanel/joinMachine.ts +418 -0
- package/src/panels/JoinPanel/view-states/AdditionMethodSelector.tsx +9 -21
- package/src/panels/JoinPanel/view-states/IdentityAdded.tsx +6 -6
- package/src/panels/JoinPanel/view-states/IdentityInput.tsx +4 -4
- package/src/panels/JoinPanel/view-states/InvitationAccepted.tsx +22 -18
- package/src/panels/JoinPanel/view-states/InvitationAuthenticator.tsx +38 -28
- package/src/panels/JoinPanel/view-states/InvitationInput.tsx +21 -21
- package/src/panels/JoinPanel/view-states/InvitationRescuer.tsx +58 -41
- package/src/panels/JoinPanel/view-states/ViewState.tsx +11 -62
- package/src/panels/JoinPanel/view-states/index.ts +0 -1
- package/src/playwright/invitations-manager.ts +6 -2
- package/src/playwright/invitations.spec.ts +14 -5
- package/src/stories/Invitations.stories.tsx +2 -2
- package/src/translations/locales/en-US.ts +6 -4
- package/src/util/index.ts +1 -0
- package/src/util/toEmoji.ts +22 -0
- package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts +0 -7
- package/dist/types/src/panels/JoinPanel/view-states/IdentitySelector.d.ts.map +0 -1
- 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 {
|
|
5
|
+
import { CaretRight, Plus, QrCode, Textbox } from '@phosphor-icons/react';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
-
import
|
|
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
|
|
14
|
-
availableIdentities: Identity[];
|
|
15
|
-
}
|
|
12
|
+
export type AdditionMethodSelectorProps = ViewStateProps;
|
|
16
13
|
|
|
17
|
-
export const AdditionMethodSelector = (
|
|
14
|
+
export const AdditionMethodSelector = (viewStateProps: AdditionMethodSelectorProps) => {
|
|
18
15
|
const disabled = !viewStateProps.active;
|
|
19
|
-
const {
|
|
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={() =>
|
|
38
|
-
data-autofocus='
|
|
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={() =>
|
|
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={() =>
|
|
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='
|
|
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
|
|
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 {
|
|
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 &&
|
|
81
|
-
data-autofocus='
|
|
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={() =>
|
|
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 {
|
|
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
|
-
|
|
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 ? '
|
|
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={() =>
|
|
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
|
-
|
|
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
|
-
|
|
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='
|
|
35
|
-
data-autofocus={
|
|
36
|
-
data-testid={`${
|
|
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
|
|
40
|
+
<Check className={getSize(4)} />
|
|
41
41
|
</Button>
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
-
return
|
|
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 = (
|
|
48
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
|
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
|
);
|