diff --git a/packages/doorman-ui/src/components/CountdownBar.tsx b/packages/doorman-ui/src/components/CountdownBar.tsx index 6f641bc..6d92be7 100644 --- a/packages/doorman-ui/src/components/CountdownBar.tsx +++ b/packages/doorman-ui/src/components/CountdownBar.tsx @@ -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(timeSeconds); useEffect(() => { @@ -26,16 +29,13 @@ export const CountdownBar = ({ timeSeconds, onCancel, onExpiry }: ICountdownBarP return () => clearTimeout(timer); }, [countdown]); - - return ( ); - -} \ No newline at end of file +} diff --git a/packages/doorman-ui/src/index.tsx b/packages/doorman-ui/src/index.tsx index 32493b5..bc47d1e 100644 --- a/packages/doorman-ui/src/index.tsx +++ b/packages/doorman-ui/src/index.tsx @@ -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: , door: , - onboard: , + onboard: , + onboardForm: , state: }} /> diff --git a/packages/doorman-ui/src/pages/EditPage.tsx b/packages/doorman-ui/src/pages/EditPage.tsx index d9f734f..f2c446a 100644 --- a/packages/doorman-ui/src/pages/EditPage.tsx +++ b/packages/doorman-ui/src/pages/EditPage.tsx @@ -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({ 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) => { } @@ -146,11 +151,19 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => { > + {isOnboarding && + + + + } - + @@ -233,14 +246,6 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => { - {isOnboarding && - - - - } diff --git a/packages/doorman-ui/src/pages/OnboardingPage.tsx b/packages/doorman-ui/src/pages/OnboardingPage.tsx new file mode 100644 index 0000000..e248c88 --- /dev/null +++ b/packages/doorman-ui/src/pages/OnboardingPage.tsx @@ -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([]); + + 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 ( + + } + notifications={ + + } + content={ + + `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", ( + { + 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: ( + + Set your Buzzer Number + + } + > + This would usually involve contacting your building to change your Buzzer to call Doorman's phone number: +
604-757-1824
+ Once you have done this, continue along the onboarding process! +
+ ) + }, + { + title: "Get to the Buzzer", + description:"To continue onboarding this Door, you should go physically to the Buzzer", + content: ( + + Get to the buzzer + + } + > + 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. + + ) + }, + { + title: "Identify", + description: "Enter the 4 digit code that you hear back from Doorman", + content: ( + + Identify + + } + > + 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. +
+ Enter the 4 digit code below to proceed + setOtp(e)} + renderInput={(props) => } + /> +
+ ) + }, + { + title: "Complete Onboarding Form", + description: "Redirect to complete the rest of onboarding form", + content: ( + + Complete Onboarding Form + + } + > + Your buzzer number was identified as: +
{buzzerNumber}
+ Click Finish below to be redirected to complete the rest of the onboarding for Doorman +
+ ) + }, + ]} + /> + } + /> + ); +};