Skip to content

Commit e47d579

Browse files
feat(onboarding-ui): Self-hosted registration (#501)
* Add prototype of self-hosted registration * Add more details to the flow * Fix Callout icon color * Refactor form state * fix: validation * Refactor form validation * Improve AdminInfoForm validation * Update Loki references * Add more details * improve: email confirmation * Trim dummy comment Co-authored-by: dougfabris <devfabris@gmail.com>
1 parent 9a7f3d5 commit e47d579

File tree

52 files changed

+426
-116
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+426
-116
lines changed

packages/fuselage/src/components/Callout/index.d.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,7 @@ import { ComponentProps, ForwardRefExoticComponent } from 'react';
22

33
import { Box } from '../Box';
44

5-
type CalloutProps = ComponentProps<typeof Box>;
5+
type CalloutProps = Omit<ComponentProps<typeof Box>, 'type'> & {
6+
type?: 'info' | 'success' | 'warning' | 'danger';
7+
};
68
export const Callout: ForwardRefExoticComponent<CalloutProps>;

packages/fuselage/src/components/Callout/styles.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
padding-inline-start: lengths.padding(16);
1010
padding-inline-end: lengths.padding(32);
1111

12+
color: colors.foreground(default);
13+
1214
border-radius: lengths.border-radius(2);
1315

1416
&--type-info {
@@ -37,8 +39,6 @@
3739

3840
margin-inline-start: lengths.margin(8);
3941

40-
color: colors.foreground(default);
41-
4242
@include typography.use-font-scale(c1);
4343
}
4444

packages/onboarding-ui/.storybook/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
addons: ['@storybook/addon-essentials'],
3-
stories: ['../src/**/*.stories.tsx'],
3+
stories: ['../src/**/*.stories.tsx', '../src/**/stories.tsx'],
44
features: {
55
postcss: false,
66
},

packages/onboarding-ui/src/common/Form/Form.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const Form: FC<{ onSubmit: () => void }> = ({
1313
padding={40}
1414
width='full'
1515
maxWidth={576}
16+
borderRadius={4}
1617
onSubmit={onSubmit}
1718
>
1819
{children}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { action } from '@storybook/addon-actions';
2+
import { countries } from 'countries-list';
3+
import type { Validate } from 'react-hook-form';
4+
5+
export const logSubmit =
6+
<T extends (...args: any[]) => any>(onSubmit: T) =>
7+
(...args: Parameters<T>): ReturnType<T> => {
8+
action('submit')(...args);
9+
return onSubmit(...args);
10+
};
11+
12+
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
13+
14+
const simulateNetworkDelay = () => delay(3000 * Math.random());
15+
16+
const fetchMock =
17+
<T extends (...args: any[]) => any>(endpoint: string, handler: T) =>
18+
async (...args: Parameters<T>): Promise<ReturnType<T>> => {
19+
action(`fetch(${endpoint})`)(...args);
20+
await simulateNetworkDelay();
21+
return handler(...args);
22+
};
23+
24+
export const validateUsername = fetchMock(
25+
'/username/validate',
26+
(username: string) => {
27+
if (username === 'admin') {
28+
return `Username "${username}" is not available`;
29+
}
30+
31+
return true;
32+
}
33+
);
34+
35+
export const validateEmail = fetchMock('/email/validate', (email: string) => {
36+
if (email === 'admin@rocket.chat') {
37+
return `Email "${email}" is already in use`;
38+
}
39+
40+
return true;
41+
});
42+
43+
export const validatePassword: Validate<string> = (password: string) => {
44+
if (password.length < 6) {
45+
return `Password is too short`;
46+
}
47+
48+
return true;
49+
};
50+
51+
export const organizationTypes: [string, string][] = [
52+
['community', 'Community'],
53+
['enterprise', 'Enterprise'],
54+
['government', 'Government'],
55+
['nonprofit', 'Nonprofit'],
56+
];
57+
58+
export const organizationIndustryOptions: [string, string][] = [
59+
['aerospaceDefense', 'Aerospace and Defense'],
60+
['blockchain', 'Blockchain'],
61+
['consulting', 'Consulting'],
62+
['consumerGoods', 'Consumer Packaged Goods'],
63+
['contactCenter', 'Contact Center'],
64+
['education', 'Education'],
65+
['entertainment', 'Entertainment'],
66+
['financialServices', 'Financial Services'],
67+
['gaming', 'Gaming'],
68+
['healthcare', 'Healthcare'],
69+
['hospitalityBusinness', 'Hospitality Businness'],
70+
['insurance', 'Insurance'],
71+
['itSecurity', 'IT Security'],
72+
['logistics', 'Logistics'],
73+
['manufacturing', 'Manufacturing'],
74+
['media', 'Media'],
75+
['pharmaceutical', 'Pharmaceutical'],
76+
['realEstate', 'Real Estate'],
77+
['religious', 'Religious'],
78+
['retail', 'Retail'],
79+
['socialNetwork', 'Social Network'],
80+
['technologyProvider', 'Technology Provider'],
81+
['technologyServices', 'Technology Services'],
82+
['telecom', 'Telecom'],
83+
['utilities', 'Utilities'],
84+
['other', 'Other'],
85+
];
86+
87+
export const organizationSizeOptions: [string, string][] = [
88+
['0', '1-10 people'],
89+
['1', '11-50 people'],
90+
['2', '51-100 people'],
91+
['3', '101-250 people'],
92+
['4', '251-500 people'],
93+
['5', '501-1000 people'],
94+
['6', '1001-4000 people'],
95+
['7', '4000 or more people'],
96+
];
97+
98+
export const countryOptions: [string, string][] = [
99+
...Object.entries(countries).map<[string, string]>(([code, { name }]) => [
100+
code,
101+
name,
102+
]),
103+
['worldwide', 'Worldwide'],
104+
];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { Box, Callout } from '@rocket.chat/fuselage';
2+
import type { Meta, Story } from '@storybook/react';
3+
import { useState } from 'react';
4+
5+
import type { AdminInfoPayload } from '../../forms/AdminInfoForm/AdminInfoForm';
6+
import type { CloudAccountEmailPayload } from '../../forms/CloudAccountEmailForm/CloudAccountEmailForm';
7+
import type { OrganizationInfoPayload } from '../../forms/OrganizationInfoForm/OrganizationInfoForm';
8+
import type { RegisterServerPayload } from '../../forms/RegisterServerForm/RegisterServerForm';
9+
import AdminInfoPage from '../../pages/AdminInfoPage';
10+
import AwaitingConfirmationPage from '../../pages/AwaitingConfirmationPage';
11+
import CloudAccountEmailPage from '../../pages/CloudAccountEmailPage';
12+
import ConfirmationProcessPage from '../../pages/ConfirmationProcessPage';
13+
import EmailConfirmedPage from '../../pages/EmailConfirmedPage';
14+
import OrganizationInfoPage from '../../pages/OrganizationInfoPage';
15+
import RegisterServerPage from '../../pages/RegisterServerPage';
16+
import {
17+
countryOptions,
18+
logSubmit,
19+
organizationIndustryOptions,
20+
organizationSizeOptions,
21+
organizationTypes,
22+
validateEmail,
23+
validatePassword,
24+
validateUsername,
25+
} from './mocks';
26+
27+
export default {
28+
title: 'flows/Self-Hosted Registration',
29+
parameters: {
30+
layout: 'fullscreen',
31+
actions: { argTypesRegex: '^on.*' },
32+
loki: { skip: true },
33+
},
34+
} as Meta;
35+
36+
export const SelfHostedRegistration: Story = () => {
37+
const [path, navigateTo] =
38+
useState<`/${
39+
| 'admin-info'
40+
| 'org-info'
41+
| 'register-server'
42+
| 'cloud-email'
43+
| 'awaiting'
44+
| 'home'
45+
| 'email'
46+
| 'confirmation-progress'
47+
| 'email-confirmed'}`>('/admin-info');
48+
49+
const [adminInfo, setAdminInfo] =
50+
useState<Omit<AdminInfoPayload, 'password'>>();
51+
52+
const [organizationInfo, setOrganizationInfo] =
53+
useState<OrganizationInfoPayload>();
54+
55+
const [serverRegistration, setServerRegistration] = useState<{
56+
updates?: boolean;
57+
agreement?: boolean;
58+
cloudAccountEmail?: string;
59+
securityCode?: string;
60+
}>();
61+
62+
const handleAdminInfoSubmit = logSubmit((data: AdminInfoPayload) => {
63+
setAdminInfo(data);
64+
navigateTo('/org-info');
65+
});
66+
67+
const handleOrganizationInfoSubmit = logSubmit(
68+
(data: OrganizationInfoPayload) => {
69+
setOrganizationInfo(data);
70+
navigateTo('/register-server');
71+
}
72+
);
73+
74+
const handleRegisterServerSubmit = logSubmit(
75+
(data: RegisterServerPayload) => {
76+
switch (data.registerType) {
77+
case 'standalone': {
78+
navigateTo('/home');
79+
break;
80+
}
81+
82+
case 'registered': {
83+
setServerRegistration((serverRegistration) => ({
84+
...serverRegistration,
85+
updates: data.updates,
86+
agreement: data.agreement,
87+
}));
88+
navigateTo('/cloud-email');
89+
break;
90+
}
91+
}
92+
}
93+
);
94+
95+
const handleCloudAccountEmailSubmit = logSubmit(
96+
(data: CloudAccountEmailPayload) => {
97+
setServerRegistration((serverRegistration) => ({
98+
...serverRegistration,
99+
cloudAccountEmail: data.email,
100+
securityCode: 'Funny Tortoise In The Hat',
101+
}));
102+
navigateTo('/awaiting');
103+
}
104+
);
105+
106+
if (path === '/admin-info') {
107+
return (
108+
<AdminInfoPage
109+
currentStep={1}
110+
stepCount={4}
111+
passwordRulesHint=''
112+
validateUsername={validateUsername}
113+
validateEmail={validateEmail}
114+
validatePassword={validatePassword}
115+
initialValues={adminInfo}
116+
onSubmit={handleAdminInfoSubmit}
117+
/>
118+
);
119+
}
120+
121+
if (path === '/org-info') {
122+
return (
123+
<OrganizationInfoPage
124+
currentStep={2}
125+
stepCount={4}
126+
organizationTypeOptions={organizationTypes}
127+
organizationIndustryOptions={organizationIndustryOptions}
128+
organizationSizeOptions={organizationSizeOptions}
129+
countryOptions={countryOptions}
130+
initialValues={organizationInfo}
131+
onBackButtonClick={() => navigateTo('/admin-info')}
132+
onSubmit={handleOrganizationInfoSubmit}
133+
/>
134+
);
135+
}
136+
137+
if (path === '/register-server') {
138+
return (
139+
<RegisterServerPage
140+
currentStep={3}
141+
stepCount={4}
142+
initialValues={{
143+
...(serverRegistration?.updates && {
144+
updates: serverRegistration?.updates,
145+
}),
146+
...(serverRegistration?.agreement && {
147+
agreement: serverRegistration?.agreement,
148+
}),
149+
}}
150+
onBackButtonClick={() => navigateTo('/org-info')}
151+
onSubmit={handleRegisterServerSubmit}
152+
/>
153+
);
154+
}
155+
156+
if (path === '/cloud-email') {
157+
return (
158+
<CloudAccountEmailPage
159+
currentStep={4}
160+
stepCount={4}
161+
initialValues={{}}
162+
onBackButtonClick={() => navigateTo('/register-server')}
163+
onSubmit={handleCloudAccountEmailSubmit}
164+
/>
165+
);
166+
}
167+
168+
if (path === '/awaiting') {
169+
if (!serverRegistration?.cloudAccountEmail) {
170+
throw new Error('missing cloud account email');
171+
}
172+
173+
if (!serverRegistration?.securityCode) {
174+
throw new Error('missing verification code');
175+
}
176+
177+
setTimeout(() => {
178+
navigateTo('/confirmation-progress');
179+
}, 5000);
180+
181+
return (
182+
<AwaitingConfirmationPage
183+
emailAddress={serverRegistration.cloudAccountEmail}
184+
securityCode={serverRegistration.securityCode}
185+
onChangeEmailRequest={() => navigateTo('/admin-info')}
186+
onResendEmailRequest={() => undefined}
187+
/>
188+
);
189+
}
190+
191+
if (path === '/confirmation-progress') {
192+
setTimeout(() => {
193+
navigateTo('/email-confirmed');
194+
}, 3000);
195+
196+
return <ConfirmationProcessPage />;
197+
}
198+
199+
if (path === '/email-confirmed') {
200+
return <EmailConfirmedPage />;
201+
}
202+
203+
if (path === '/home') {
204+
return (
205+
<Box
206+
width='100vw'
207+
height='100vh'
208+
display='flex'
209+
justifyContent='center'
210+
alignItems='center'
211+
>
212+
<Callout type='success'>This is the home of the workspace.</Callout>
213+
</Box>
214+
);
215+
}
216+
217+
throw new Error('invalid path');
218+
};
219+
SelfHostedRegistration.storyName = 'Self-Hosted Registration';

0 commit comments

Comments
 (0)