Skip to content

Commit

Permalink
feat(onboarding-ui): Self-hosted registration (#501)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
tassoevan and dougfabris authored Jul 22, 2021
1 parent 9a7f3d5 commit e47d579
Show file tree
Hide file tree
Showing 52 changed files with 426 additions and 116 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion packages/fuselage/src/components/Callout/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ import { ComponentProps, ForwardRefExoticComponent } from 'react';

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

type CalloutProps = ComponentProps<typeof Box>;
type CalloutProps = Omit<ComponentProps<typeof Box>, 'type'> & {
type?: 'info' | 'success' | 'warning' | 'danger';
};
export const Callout: ForwardRefExoticComponent<CalloutProps>;
4 changes: 2 additions & 2 deletions packages/fuselage/src/components/Callout/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
padding-inline-start: lengths.padding(16);
padding-inline-end: lengths.padding(32);

color: colors.foreground(default);

border-radius: lengths.border-radius(2);

&--type-info {
Expand Down Expand Up @@ -37,8 +39,6 @@

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

color: colors.foreground(default);

@include typography.use-font-scale(c1);
}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Diff not rendered.
Diff not rendered.
2 changes: 1 addition & 1 deletion packages/onboarding-ui/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
addons: ['@storybook/addon-essentials'],
stories: ['../src/**/*.stories.tsx'],
stories: ['../src/**/*.stories.tsx', '../src/**/stories.tsx'],
features: {
postcss: false,
},
Expand Down
1 change: 1 addition & 0 deletions packages/onboarding-ui/src/common/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const Form: FC<{ onSubmit: () => void }> = ({
padding={40}
width='full'
maxWidth={576}
borderRadius={4}
onSubmit={onSubmit}
>
{children}
Expand Down
104 changes: 104 additions & 0 deletions packages/onboarding-ui/src/flows/SelfHostedRegistration/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { action } from '@storybook/addon-actions';
import { countries } from 'countries-list';
import type { Validate } from 'react-hook-form';

export const logSubmit =
<T extends (...args: any[]) => any>(onSubmit: T) =>
(...args: Parameters<T>): ReturnType<T> => {
action('submit')(...args);
return onSubmit(...args);
};

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const simulateNetworkDelay = () => delay(3000 * Math.random());

const fetchMock =
<T extends (...args: any[]) => any>(endpoint: string, handler: T) =>
async (...args: Parameters<T>): Promise<ReturnType<T>> => {
action(`fetch(${endpoint})`)(...args);
await simulateNetworkDelay();
return handler(...args);
};

export const validateUsername = fetchMock(
'/username/validate',
(username: string) => {
if (username === 'admin') {
return `Username "${username}" is not available`;
}

return true;
}
);

export const validateEmail = fetchMock('/email/validate', (email: string) => {
if (email === 'admin@rocket.chat') {
return `Email "${email}" is already in use`;
}

return true;
});

export const validatePassword: Validate<string> = (password: string) => {
if (password.length < 6) {
return `Password is too short`;
}

return true;
};

export const organizationTypes: [string, string][] = [
['community', 'Community'],
['enterprise', 'Enterprise'],
['government', 'Government'],
['nonprofit', 'Nonprofit'],
];

export const organizationIndustryOptions: [string, string][] = [
['aerospaceDefense', 'Aerospace and Defense'],
['blockchain', 'Blockchain'],
['consulting', 'Consulting'],
['consumerGoods', 'Consumer Packaged Goods'],
['contactCenter', 'Contact Center'],
['education', 'Education'],
['entertainment', 'Entertainment'],
['financialServices', 'Financial Services'],
['gaming', 'Gaming'],
['healthcare', 'Healthcare'],
['hospitalityBusinness', 'Hospitality Businness'],
['insurance', 'Insurance'],
['itSecurity', 'IT Security'],
['logistics', 'Logistics'],
['manufacturing', 'Manufacturing'],
['media', 'Media'],
['pharmaceutical', 'Pharmaceutical'],
['realEstate', 'Real Estate'],
['religious', 'Religious'],
['retail', 'Retail'],
['socialNetwork', 'Social Network'],
['technologyProvider', 'Technology Provider'],
['technologyServices', 'Technology Services'],
['telecom', 'Telecom'],
['utilities', 'Utilities'],
['other', 'Other'],
];

export const organizationSizeOptions: [string, string][] = [
['0', '1-10 people'],
['1', '11-50 people'],
['2', '51-100 people'],
['3', '101-250 people'],
['4', '251-500 people'],
['5', '501-1000 people'],
['6', '1001-4000 people'],
['7', '4000 or more people'],
];

export const countryOptions: [string, string][] = [
...Object.entries(countries).map<[string, string]>(([code, { name }]) => [
code,
name,
]),
['worldwide', 'Worldwide'],
];
219 changes: 219 additions & 0 deletions packages/onboarding-ui/src/flows/SelfHostedRegistration/stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { Box, Callout } from '@rocket.chat/fuselage';
import type { Meta, Story } from '@storybook/react';
import { useState } from 'react';

import type { AdminInfoPayload } from '../../forms/AdminInfoForm/AdminInfoForm';
import type { CloudAccountEmailPayload } from '../../forms/CloudAccountEmailForm/CloudAccountEmailForm';
import type { OrganizationInfoPayload } from '../../forms/OrganizationInfoForm/OrganizationInfoForm';
import type { RegisterServerPayload } from '../../forms/RegisterServerForm/RegisterServerForm';
import AdminInfoPage from '../../pages/AdminInfoPage';
import AwaitingConfirmationPage from '../../pages/AwaitingConfirmationPage';
import CloudAccountEmailPage from '../../pages/CloudAccountEmailPage';
import ConfirmationProcessPage from '../../pages/ConfirmationProcessPage';
import EmailConfirmedPage from '../../pages/EmailConfirmedPage';
import OrganizationInfoPage from '../../pages/OrganizationInfoPage';
import RegisterServerPage from '../../pages/RegisterServerPage';
import {
countryOptions,
logSubmit,
organizationIndustryOptions,
organizationSizeOptions,
organizationTypes,
validateEmail,
validatePassword,
validateUsername,
} from './mocks';

export default {
title: 'flows/Self-Hosted Registration',
parameters: {
layout: 'fullscreen',
actions: { argTypesRegex: '^on.*' },
loki: { skip: true },
},
} as Meta;

export const SelfHostedRegistration: Story = () => {
const [path, navigateTo] =
useState<`/${
| 'admin-info'
| 'org-info'
| 'register-server'
| 'cloud-email'
| 'awaiting'
| 'home'
| 'email'
| 'confirmation-progress'
| 'email-confirmed'}`>('/admin-info');

const [adminInfo, setAdminInfo] =
useState<Omit<AdminInfoPayload, 'password'>>();

const [organizationInfo, setOrganizationInfo] =
useState<OrganizationInfoPayload>();

const [serverRegistration, setServerRegistration] = useState<{
updates?: boolean;
agreement?: boolean;
cloudAccountEmail?: string;
securityCode?: string;
}>();

const handleAdminInfoSubmit = logSubmit((data: AdminInfoPayload) => {
setAdminInfo(data);
navigateTo('/org-info');
});

const handleOrganizationInfoSubmit = logSubmit(
(data: OrganizationInfoPayload) => {
setOrganizationInfo(data);
navigateTo('/register-server');
}
);

const handleRegisterServerSubmit = logSubmit(
(data: RegisterServerPayload) => {
switch (data.registerType) {
case 'standalone': {
navigateTo('/home');
break;
}

case 'registered': {
setServerRegistration((serverRegistration) => ({
...serverRegistration,
updates: data.updates,
agreement: data.agreement,
}));
navigateTo('/cloud-email');
break;
}
}
}
);

const handleCloudAccountEmailSubmit = logSubmit(
(data: CloudAccountEmailPayload) => {
setServerRegistration((serverRegistration) => ({
...serverRegistration,
cloudAccountEmail: data.email,
securityCode: 'Funny Tortoise In The Hat',
}));
navigateTo('/awaiting');
}
);

if (path === '/admin-info') {
return (
<AdminInfoPage
currentStep={1}
stepCount={4}
passwordRulesHint=''
validateUsername={validateUsername}
validateEmail={validateEmail}
validatePassword={validatePassword}
initialValues={adminInfo}
onSubmit={handleAdminInfoSubmit}
/>
);
}

if (path === '/org-info') {
return (
<OrganizationInfoPage
currentStep={2}
stepCount={4}
organizationTypeOptions={organizationTypes}
organizationIndustryOptions={organizationIndustryOptions}
organizationSizeOptions={organizationSizeOptions}
countryOptions={countryOptions}
initialValues={organizationInfo}
onBackButtonClick={() => navigateTo('/admin-info')}
onSubmit={handleOrganizationInfoSubmit}
/>
);
}

if (path === '/register-server') {
return (
<RegisterServerPage
currentStep={3}
stepCount={4}
initialValues={{
...(serverRegistration?.updates && {
updates: serverRegistration?.updates,
}),
...(serverRegistration?.agreement && {
agreement: serverRegistration?.agreement,
}),
}}
onBackButtonClick={() => navigateTo('/org-info')}
onSubmit={handleRegisterServerSubmit}
/>
);
}

if (path === '/cloud-email') {
return (
<CloudAccountEmailPage
currentStep={4}
stepCount={4}
initialValues={{}}
onBackButtonClick={() => navigateTo('/register-server')}
onSubmit={handleCloudAccountEmailSubmit}
/>
);
}

if (path === '/awaiting') {
if (!serverRegistration?.cloudAccountEmail) {
throw new Error('missing cloud account email');
}

if (!serverRegistration?.securityCode) {
throw new Error('missing verification code');
}

setTimeout(() => {
navigateTo('/confirmation-progress');
}, 5000);

return (
<AwaitingConfirmationPage
emailAddress={serverRegistration.cloudAccountEmail}
securityCode={serverRegistration.securityCode}
onChangeEmailRequest={() => navigateTo('/admin-info')}
onResendEmailRequest={() => undefined}
/>
);
}

if (path === '/confirmation-progress') {
setTimeout(() => {
navigateTo('/email-confirmed');
}, 3000);

return <ConfirmationProcessPage />;
}

if (path === '/email-confirmed') {
return <EmailConfirmedPage />;
}

if (path === '/home') {
return (
<Box
width='100vw'
height='100vh'
display='flex'
justifyContent='center'
alignItems='center'
>
<Callout type='success'>This is the home of the workspace.</Callout>
</Box>
);
}

throw new Error('invalid path');
};
SelfHostedRegistration.storyName = 'Self-Hosted Registration';
Loading

0 comments on commit e47d579

Please sign in to comment.