add buzzer code identification to onboarding flow
This commit is contained in:
parent
1bca8cf9dc
commit
975c47b174
@ -5,9 +5,12 @@ export interface ICountdownBarProps {
|
||||
timeSeconds: number;
|
||||
onCancel?: () => void;
|
||||
onExpiry?: () => void;
|
||||
label?: string;
|
||||
description?: string;
|
||||
additionalInfo?: string;
|
||||
}
|
||||
|
||||
export const CountdownBar = ({ timeSeconds, onCancel, onExpiry }: ICountdownBarProps) => {
|
||||
export const CountdownBar = ({ timeSeconds, onCancel, onExpiry, label, description, additionalInfo }: ICountdownBarProps) => {
|
||||
const [ countdown, setCountdown ] = useState<number>(timeSeconds);
|
||||
|
||||
useEffect(() => {
|
||||
@ -26,16 +29,13 @@ export const CountdownBar = ({ timeSeconds, onCancel, onExpiry }: ICountdownBarP
|
||||
return () => clearTimeout(timer);
|
||||
}, [countdown]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
label={"Authentication Timeout"}
|
||||
description={"Dial the buzzer before the time expires"}
|
||||
additionalInfo={`Authentication expires after ${timeSeconds}s to keep the door secure. Please dial the buzzer within the time remaining or it won't unlock`}
|
||||
label={label || "Authentication Timeout"}
|
||||
description={description || "Dial the buzzer before the time expires"}
|
||||
additionalInfo={additionalInfo || `Authentication expires after ${timeSeconds}s to keep the door secure. Please dial the buzzer within the time remaining or it won't unlock`}
|
||||
value={(countdown / timeSeconds) * 100}
|
||||
variant="flash"
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { ErrorPage } from './pages/ErrorPage';
|
||||
import { QueryRouter } from './routers/QueryRouter';
|
||||
import { EditPage } from './pages/EditPage';
|
||||
import { RedirectPage } from './pages/RedirectPage';
|
||||
import { OnboardingPage } from './pages/OnboardingPage';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
@ -26,7 +27,8 @@ const router = createBrowserRouter([
|
||||
mapping={{
|
||||
edit: <EditPage />,
|
||||
door: <DoorPage />,
|
||||
onboard: <EditPage isOnboarding={true} />,
|
||||
onboard: <OnboardingPage />,
|
||||
onboardForm: <EditPage isOnboarding={true} />,
|
||||
state: <RedirectPage />
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -20,9 +20,14 @@ export interface EditPageProps {
|
||||
export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
const doorResponse = useLoaderData() as DoorResponse;
|
||||
const door = doorResponse.id;
|
||||
const [params, setParams] = useSearchParams();
|
||||
|
||||
const buzzer = params.get("buzzer") || undefined;
|
||||
|
||||
const { setValue, setError, getValues, watch, formState, clearErrors, control, register } = useForm<DoorEditForm>({
|
||||
defaultValues: {
|
||||
...doorResponse,
|
||||
buzzer,
|
||||
isConfirmed: !isOnboarding,
|
||||
},
|
||||
mode: "all",
|
||||
@ -62,7 +67,7 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
|
||||
const discordUsersError = formState.errors.discordUsers?.message;
|
||||
|
||||
const backUrl = isOnboarding? '/': `?door=${door}`;
|
||||
const backUrl = isOnboarding? '/?onboard': `?door=${door}`;
|
||||
const apiRoute = isOnboarding ? ONBOARD_ROUTE: EDIT_ROUTE;
|
||||
|
||||
return (
|
||||
@ -74,7 +79,7 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
<BreadcrumbGroup
|
||||
items={[
|
||||
{ text: 'Door', href: backUrl },
|
||||
isOnboarding ? { text: "Onboard", href: "?onboard" }: { text: door, href: `?edit&door=${door}` },
|
||||
isOnboarding ? { text: "Onboard Form", href: `?onboardForm&buzzer=${buzzer}` }: { text: door, href: `?edit&door=${door}` },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
@ -146,11 +151,19 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
>
|
||||
<Container>
|
||||
<SpaceBetween direction="vertical" size="l">
|
||||
{isOnboarding &&
|
||||
<FormField label="Register Doorman in Building" constraintText="Confirm that you added our number to your buzzer system: 604-757-1824" errorText={formState.errors.isConfirmed?.message}>
|
||||
<label>
|
||||
I have added Doorman's number to my buzzer
|
||||
<input type="checkbox" {...register("isConfirmed")} />
|
||||
</label>
|
||||
</FormField>
|
||||
}
|
||||
<FormField label="Door Name" constraintText="Unique name for this Door, will be used in your Doorman URL">
|
||||
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="id" control={control} />
|
||||
</FormField>
|
||||
<FormField label="Buzzer Number" constraintText="Phone number that calls you when your buzzer is called">
|
||||
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="buzzer" control={control} />
|
||||
<CInput readOnly={!!buzzer} disabled={!!buzzer} name="buzzer" control={control} />
|
||||
</FormField>
|
||||
<FormField label="PIN" constraintText={"Code to unlock this Door in Doorman"}>
|
||||
<CInput type="password" name="pin" control={control} />
|
||||
@ -233,14 +246,6 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||
<FormField label="Welcome Message" constraintText="Message to display after a successful unlock">
|
||||
<CTextArea name="greeting" control={control} />
|
||||
</FormField>
|
||||
{isOnboarding &&
|
||||
<FormField label="Register Doorman in Building" constraintText="Confirm that you added our number to your buzzer system: 604-757-1824" errorText={formState.errors.isConfirmed?.message}>
|
||||
<label>
|
||||
I have added Doorman's number to my buzzer
|
||||
<input type="checkbox" {...register("isConfirmed")} />
|
||||
</label>
|
||||
</FormField>
|
||||
}
|
||||
</SpaceBetween>
|
||||
</Container>
|
||||
</Form>
|
||||
|
||||
239
packages/doorman-ui/src/pages/OnboardingPage.tsx
Normal file
239
packages/doorman-ui/src/pages/OnboardingPage.tsx
Normal file
@ -0,0 +1,239 @@
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useLoaderData, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { AppLayout, BreadcrumbGroup, Container, Flashbar, FlashbarProps, Header, SpaceBetween, TextContent, Wizard } from "@cloudscape-design/components";
|
||||
import OtpInput from 'react-otp-input';
|
||||
import { CountdownBar } from "../components/CountdownBar";
|
||||
import { fetchUrlEncoded } from "../helpers/FetchHelper";
|
||||
|
||||
export function OnboardingPage() {
|
||||
const navigate = useNavigate();
|
||||
const [ step, setStep ] = useState(0);
|
||||
const [ alerts, setAlerts ] = useState<FlashbarProps.MessageDefinition[]>([]);
|
||||
|
||||
const [ otp, setOtp ] = useState("");
|
||||
const [ buzzerNumber, setBuzzerNumber ] = useState("");
|
||||
|
||||
const dismissAlert = (id: string) => {
|
||||
setAlerts(alerts => alerts.filter(alert => alert.id !== id));
|
||||
}
|
||||
|
||||
const dismissInProgressAlerts = () => {
|
||||
setAlerts(alerts => alerts.filter(alert => alert.type !== 'in-progress'));
|
||||
}
|
||||
|
||||
const createAlert = (type: FlashbarProps.Type, content: ReactNode): FlashbarProps.MessageDefinition => {
|
||||
const id = `${Math.random()}`;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
content,
|
||||
dismissible: type !== 'in-progress',
|
||||
onDismiss: () => dismissAlert(id),
|
||||
}
|
||||
}
|
||||
|
||||
const addAlert = (type: FlashbarProps.Type, content: ReactNode): string => {
|
||||
const newAlert = createAlert(type, content);
|
||||
setAlerts(alerts => [
|
||||
newAlert,
|
||||
...alerts,
|
||||
]);
|
||||
|
||||
return newAlert.id as string;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (otp.length === 4) {
|
||||
// see if this is valid otp
|
||||
fetchUrlEncoded('/api/door/checkOtp', {
|
||||
otp,
|
||||
}).then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.msg) {
|
||||
addAlert("error", res.msg);
|
||||
return;
|
||||
}
|
||||
const apiNumber = res.buzzer;
|
||||
|
||||
setBuzzerNumber(apiNumber);
|
||||
addAlert("success", `Successfully verified Buzzer as ${apiNumber}`);
|
||||
setTimeout(() => {
|
||||
setStep(3);
|
||||
}, 1_000);
|
||||
});
|
||||
}
|
||||
}, [otp]);
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
contentType="wizard"
|
||||
navigationHide
|
||||
toolsHide
|
||||
breadcrumbs={
|
||||
<BreadcrumbGroup
|
||||
items={[
|
||||
{ text: 'Door', href: '#' },
|
||||
{ text: 'Onboarding', href: '#' },
|
||||
]}
|
||||
/>
|
||||
}
|
||||
notifications={
|
||||
<Flashbar
|
||||
stackItems={true}
|
||||
items={alerts}
|
||||
/>
|
||||
}
|
||||
content={
|
||||
<Wizard
|
||||
i18nStrings={{
|
||||
stepNumberLabel: stepNumber =>
|
||||
`Step ${stepNumber}`,
|
||||
collapsedStepsLabel: (stepNumber, stepsCount) =>
|
||||
`Step ${stepNumber} of ${stepsCount}`,
|
||||
skipToButtonLabel: (step, stepNumber) =>
|
||||
`Skip to ${step.title}`,
|
||||
navigationAriaLabel: "Steps",
|
||||
cancelButton: "Cancel",
|
||||
previousButton: "Previous",
|
||||
nextButton: "Next",
|
||||
submitButton: "Finish",
|
||||
optional: "optional"
|
||||
}}
|
||||
activeStepIndex={step}
|
||||
onSubmit={() => {
|
||||
navigate(`/?onboardForm&buzzer=${buzzerNumber}`)
|
||||
}}
|
||||
onNavigate={({ detail }) => {
|
||||
if (detail.requestedStepIndex < step) {
|
||||
dismissInProgressAlerts();
|
||||
setStep(detail.requestedStepIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.requestedStepIndex === 2) {
|
||||
fetch('/api/door/auth?door=onboardingflag&key=1234')
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
if (res.status === "CLOSED") {
|
||||
addAlert('error', 'Something went wrong, please try again');
|
||||
} else {
|
||||
addAlert("in-progress", (
|
||||
<CountdownBar
|
||||
label="Onboarding Timeout"
|
||||
description="Complete Onboarding before timeout"
|
||||
additionalInfo="Onboarding times out after 15 minutes, please complete the onboarding before then"
|
||||
timeSeconds={15 * 60}
|
||||
onExpiry={() => {
|
||||
addAlert("error", "Onboarding timed out: please try again");
|
||||
setStep(1);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
setStep(detail.requestedStepIndex);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail.requestedStepIndex === 3) {
|
||||
if (buzzerNumber === "") {
|
||||
addAlert('error', "Please enter 4 digit verification code to proceed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setStep(detail.requestedStepIndex);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setStep(0);
|
||||
dismissInProgressAlerts();
|
||||
}}
|
||||
steps={[
|
||||
{
|
||||
title: "Set your Buzzer Number",
|
||||
description:"Before onboarding Doorman, you need to configure your Buzzer number",
|
||||
content: (
|
||||
<Container
|
||||
header={
|
||||
<Header variant="h2">
|
||||
Set your Buzzer Number
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
This would usually involve contacting your building to change your Buzzer to call Doorman's phone number:
|
||||
<Header variant="h3">604-757-1824</Header>
|
||||
Once you have done this, continue along the onboarding process!
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Get to the Buzzer",
|
||||
description:"To continue onboarding this Door, you should go physically to the Buzzer",
|
||||
content: (
|
||||
<Container
|
||||
header={
|
||||
<Header variant="h2">
|
||||
Get to the buzzer
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
The next step will require you to dial your Buzzer so Doorman can identify who is calling. Once you arrive at your Buzzer, advance to the next step.
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Identify",
|
||||
description: "Enter the 4 digit code that you hear back from Doorman",
|
||||
content: (
|
||||
<Container
|
||||
header={
|
||||
<Header variant="h2">
|
||||
Identify
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
For this step, you need to dial your Buzzer. It should call Doorman, and you will hear a message with a generated 4 digit code.
|
||||
<br/>
|
||||
Enter the 4 digit code below to proceed
|
||||
<OtpInput
|
||||
containerStyle={{
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
inputStyle={{
|
||||
margin: '0.5rem',
|
||||
height: '3rem',
|
||||
width: '2rem',
|
||||
fontSize: '2rem',
|
||||
}}
|
||||
value={otp}
|
||||
numInputs={4}
|
||||
onChange={(e) => setOtp(e)}
|
||||
renderInput={(props) => <input {...props} />}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "Complete Onboarding Form",
|
||||
description: "Redirect to complete the rest of onboarding form",
|
||||
content: (
|
||||
<Container
|
||||
header={
|
||||
<Header variant="h2">
|
||||
Complete Onboarding Form
|
||||
</Header>
|
||||
}
|
||||
>
|
||||
Your buzzer number was identified as:
|
||||
<Header>{buzzerNumber}</Header>
|
||||
Click Finish below to be redirected to complete the rest of the onboarding for Doorman
|
||||
</Container>
|
||||
)
|
||||
},
|
||||
]}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user