Compare commits
No commits in common. "838ca2cb2304b99f1feb56c19581f09e12f63662" and "a9924a7f7679e1659dda8071c3d9759748df46ea" have entirely different histories.
838ca2cb23
...
a9924a7f76
@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* Try to get buzzer number for a given OTP
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
|
|
||||||
import { TwilioContext } from "../../../types/TwilioContext";
|
|
||||||
import { createDynaBridgeClient } from "../../../utils/ddb";
|
|
||||||
import { withMetrics } from "../../../common/DoormanHandler";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { UserAgentHeader } from "../../../utils/blockUserAgent";
|
|
||||||
import { setResponseJson } from "../../../utils/responseUtils";
|
|
||||||
import { getLogCallID } from "../../../schema/LogCall";
|
|
||||||
import { isTTLInFuture } from "../../../common/TTLHelper";
|
|
||||||
|
|
||||||
export const CheckOtpRequestSchema = z.object({
|
|
||||||
otp: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CheckOtpRequest = z.infer<typeof CheckOtpRequestSchema>;
|
|
||||||
export interface CheckOtpRequestTwilio extends ServerlessEventObject<CheckOtpRequest, UserAgentHeader> { };
|
|
||||||
|
|
||||||
export const CheckOtpResponseSchema = z.object({
|
|
||||||
buzzer: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CheckOtpResponse = z.infer<typeof CheckOtpResponseSchema>;
|
|
||||||
|
|
||||||
export const handler: ServerlessFunctionSignature<TwilioContext, CheckOtpRequestTwilio> = withMetrics("checkotp", async (context, event, callback, metricsRegistry) => {
|
|
||||||
const response = new Twilio.Response();
|
|
||||||
const req = CheckOtpRequestSchema.parse(event);
|
|
||||||
|
|
||||||
let otp = req.otp;
|
|
||||||
|
|
||||||
const db = createDynaBridgeClient(context);
|
|
||||||
|
|
||||||
const log = await db.entities.logCall.findById(getLogCallID(otp));
|
|
||||||
|
|
||||||
if (!log || !isTTLInFuture(log)) {
|
|
||||||
setResponseJson(response, 404, {
|
|
||||||
msg: "OTP expired or not found",
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
setResponseJson(response, 200, {
|
|
||||||
buzzer: log.caller,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// destroy the internal client after
|
|
||||||
// @ts-ignore
|
|
||||||
db.ddbClient.destroy();
|
|
||||||
|
|
||||||
return callback(null, response);
|
|
||||||
});
|
|
||||||
@ -13,8 +13,6 @@ import { UserAgentHeader } from "../../../utils/blockUserAgent";
|
|||||||
import { setResponseJson } from "../../../utils/responseUtils";
|
import { setResponseJson } from "../../../utils/responseUtils";
|
||||||
import { LOG_CALL_SK, LogCallSchema } from "../../../schema/LogCall";
|
import { LOG_CALL_SK, LogCallSchema } from "../../../schema/LogCall";
|
||||||
|
|
||||||
import crypto from "crypto";
|
|
||||||
|
|
||||||
export const LogCallRequestSchema = z.object({
|
export const LogCallRequestSchema = z.object({
|
||||||
caller: z.string(),
|
caller: z.string(),
|
||||||
});
|
});
|
||||||
@ -28,24 +26,8 @@ export const LogCallResponseSchema = z.object({
|
|||||||
|
|
||||||
export type LogCallResponse = z.infer<typeof LogCallResponseSchema>;
|
export type LogCallResponse = z.infer<typeof LogCallResponseSchema>;
|
||||||
|
|
||||||
// hash is 4 digit number based on todays date + phone number caller
|
function getCode() {
|
||||||
// cost saving so we don't generate a new OTP for every caller even if its the same caller
|
return `${Math.floor(Math.random() * 10000)}`.padStart(4, '0');
|
||||||
function getCode(caller: string) {
|
|
||||||
const hash = crypto.createHash("sha256");
|
|
||||||
const today = new Date();
|
|
||||||
|
|
||||||
hash.update(today.toLocaleDateString('en-US'));
|
|
||||||
hash.update(caller);
|
|
||||||
|
|
||||||
const hashHex = hash.digest('hex');
|
|
||||||
|
|
||||||
// 2. Convert the hexadecimal string to a BigInt
|
|
||||||
// This is necessary for large hash values that exceed JavaScript's Number limit.
|
|
||||||
const hashBigInt = BigInt(`0x${hashHex}`);
|
|
||||||
|
|
||||||
// 3. Convert the BigInt to a decimal string
|
|
||||||
const hashDecimal = hashBigInt.toString();
|
|
||||||
return hashDecimal.substring(hashDecimal.length - 4,);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handler: ServerlessFunctionSignature<TwilioContext, LogCallRequestTwilio> = withMetrics("logCall", async (context, event, callback, metricsRegistry) => {
|
export const handler: ServerlessFunctionSignature<TwilioContext, LogCallRequestTwilio> = withMetrics("logCall", async (context, event, callback, metricsRegistry) => {
|
||||||
@ -64,16 +46,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, LogCallRequestT
|
|||||||
msg: "Onboarding is not open",
|
msg: "Onboarding is not open",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// TODO: best efforts cleanup
|
|
||||||
// console.log("Attempting best efforts cleanup of logged calls")
|
|
||||||
// const items = await db.entities.logCall.findAll();
|
|
||||||
// const toRemove = items.filter(item => item.SK === LOG_CALL_SK && !isTTLInFuture(item));
|
|
||||||
// console.log(`There are ${toRemove.length} old call logs to remove`);
|
|
||||||
// await db.entities.logCall.deleteBatch(toRemove);
|
|
||||||
// console.log("done cleaning up logged calls");
|
|
||||||
|
|
||||||
// log this caller
|
// log this caller
|
||||||
const otp = getCode(caller);
|
const otp = getCode();
|
||||||
const logCall = LogCallSchema.parse({
|
const logCall = LogCallSchema.parse({
|
||||||
PK: otp,
|
PK: otp,
|
||||||
SK: LOG_CALL_SK,
|
SK: LOG_CALL_SK,
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { StatusResponse } from "../src/functions/api/door/status";
|
|||||||
import { sleep } from "bun";
|
import { sleep } from "bun";
|
||||||
import { ONBOARDING_DOOR_NAME, ONBOARDING_DOOR_PIN } from "../src/schema/DoorConfig";
|
import { ONBOARDING_DOOR_NAME, ONBOARDING_DOOR_PIN } from "../src/schema/DoorConfig";
|
||||||
import { LogCallResponse } from "../src/functions/api/door/logCall";
|
import { LogCallResponse } from "../src/functions/api/door/logCall";
|
||||||
import { CheckOtpResponse } from "../src/functions/api/door/checkOtp";
|
|
||||||
|
|
||||||
// these tests should only run locally
|
// these tests should only run locally
|
||||||
if (process.env.STAGE === 'staging') {
|
if (process.env.STAGE === 'staging') {
|
||||||
@ -110,15 +109,7 @@ describe("call log path works", () => {
|
|||||||
expect(logCallRes.status).toBe(200);
|
expect(logCallRes.status).toBe(200);
|
||||||
|
|
||||||
const otp = (await logCallRes.json() as LogCallResponse).otp
|
const otp = (await logCallRes.json() as LogCallResponse).otp
|
||||||
expect(otp.length).toBe(4);
|
expect(otp.length).toBe(4)
|
||||||
|
|
||||||
const checkOtpRes = await fetch(baseUrl + `/api/door/checkOtp?otp=${otp}`);
|
|
||||||
|
|
||||||
expect(checkOtpRes.status).toBe(200);
|
|
||||||
|
|
||||||
// check OTP
|
|
||||||
const caller = (await checkOtpRes.json() as CheckOtpResponse).buzzer;
|
|
||||||
expect(caller).toBe(buzzerNumber);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("call log after door closed, should not return OTP", async () => {
|
test("call log after door closed, should not return OTP", async () => {
|
||||||
|
|||||||
@ -27,16 +27,4 @@ This will simulate a buzzer calling from 6133163433 (the test buzzer)
|
|||||||
|
|
||||||
If you let it poll for 10s then it should respond with Twilio xml saying to dial fallback numbers.
|
If you let it poll for 10s then it should respond with Twilio xml saying to dial fallback numbers.
|
||||||
|
|
||||||
If doorman-api is not running, it would always return with a `<Reject/>`
|
If doorman-api is not running, it would always return with a `<Reject/>`
|
||||||
|
|
||||||
## adding new audio assets
|
|
||||||
|
|
||||||
Generate using this website: https://ttsmp3.com/
|
|
||||||
|
|
||||||
select US English / Salli for consistency with the voice.
|
|
||||||
|
|
||||||
After generated, go to this website: https://www.mp3louder.com/
|
|
||||||
|
|
||||||
boost the audio 3db and save it
|
|
||||||
|
|
||||||
put it in the doorman-client/assets folder with suffix `.protected.mp3`
|
|
||||||
@ -5,12 +5,9 @@ export interface ICountdownBarProps {
|
|||||||
timeSeconds: number;
|
timeSeconds: number;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
onExpiry?: () => void;
|
onExpiry?: () => void;
|
||||||
label?: string;
|
|
||||||
description?: string;
|
|
||||||
additionalInfo?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CountdownBar = ({ timeSeconds, onCancel, onExpiry, label, description, additionalInfo }: ICountdownBarProps) => {
|
export const CountdownBar = ({ timeSeconds, onCancel, onExpiry }: ICountdownBarProps) => {
|
||||||
const [ countdown, setCountdown ] = useState<number>(timeSeconds);
|
const [ countdown, setCountdown ] = useState<number>(timeSeconds);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -29,13 +26,16 @@ export const CountdownBar = ({ timeSeconds, onCancel, onExpiry, label, descripti
|
|||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [countdown]);
|
}, [countdown]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
label={label || "Authentication Timeout"}
|
label={"Authentication Timeout"}
|
||||||
description={description || "Dial the buzzer before the time expires"}
|
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`}
|
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}
|
value={(countdown / timeSeconds) * 100}
|
||||||
variant="flash"
|
variant="flash"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
}
|
||||||
@ -7,7 +7,6 @@ import { ErrorPage } from './pages/ErrorPage';
|
|||||||
import { QueryRouter } from './routers/QueryRouter';
|
import { QueryRouter } from './routers/QueryRouter';
|
||||||
import { EditPage } from './pages/EditPage';
|
import { EditPage } from './pages/EditPage';
|
||||||
import { RedirectPage } from './pages/RedirectPage';
|
import { RedirectPage } from './pages/RedirectPage';
|
||||||
import { OnboardingPage } from './pages/OnboardingPage';
|
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(
|
const root = ReactDOM.createRoot(
|
||||||
document.getElementById('root') as HTMLElement
|
document.getElementById('root') as HTMLElement
|
||||||
@ -27,8 +26,7 @@ const router = createBrowserRouter([
|
|||||||
mapping={{
|
mapping={{
|
||||||
edit: <EditPage />,
|
edit: <EditPage />,
|
||||||
door: <DoorPage />,
|
door: <DoorPage />,
|
||||||
onboard: <OnboardingPage />,
|
onboard: <EditPage isOnboarding={true} />,
|
||||||
onboardForm: <EditPage isOnboarding={true} />,
|
|
||||||
state: <RedirectPage />
|
state: <RedirectPage />
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,14 +20,9 @@ export interface EditPageProps {
|
|||||||
export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
||||||
const doorResponse = useLoaderData() as DoorResponse;
|
const doorResponse = useLoaderData() as DoorResponse;
|
||||||
const door = doorResponse.id;
|
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>({
|
const { setValue, setError, getValues, watch, formState, clearErrors, control, register } = useForm<DoorEditForm>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...doorResponse,
|
...doorResponse,
|
||||||
buzzer,
|
|
||||||
isConfirmed: !isOnboarding,
|
isConfirmed: !isOnboarding,
|
||||||
},
|
},
|
||||||
mode: "all",
|
mode: "all",
|
||||||
@ -67,7 +62,7 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
|||||||
const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
|
const fallbackNumbersError = formState.errors.fallbackNumbers?.message;
|
||||||
const discordUsersError = formState.errors.discordUsers?.message;
|
const discordUsersError = formState.errors.discordUsers?.message;
|
||||||
|
|
||||||
const backUrl = isOnboarding? '/?onboard': `?door=${door}`;
|
const backUrl = isOnboarding? '/': `?door=${door}`;
|
||||||
const apiRoute = isOnboarding ? ONBOARD_ROUTE: EDIT_ROUTE;
|
const apiRoute = isOnboarding ? ONBOARD_ROUTE: EDIT_ROUTE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -79,7 +74,7 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
|||||||
<BreadcrumbGroup
|
<BreadcrumbGroup
|
||||||
items={[
|
items={[
|
||||||
{ text: 'Door', href: backUrl },
|
{ text: 'Door', href: backUrl },
|
||||||
isOnboarding ? { text: "Onboard Form", href: `?onboardForm&buzzer=${buzzer}` }: { text: door, href: `?edit&door=${door}` },
|
isOnboarding ? { text: "Onboard", href: "?onboard" }: { text: door, href: `?edit&door=${door}` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@ -151,19 +146,11 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
|||||||
>
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<SpaceBetween direction="vertical" size="l">
|
<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">
|
<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} />
|
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="id" control={control} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Buzzer Number" constraintText="Phone number that calls you when your buzzer is called">
|
<FormField label="Buzzer Number" constraintText="Phone number that calls you when your buzzer is called">
|
||||||
<CInput readOnly={!!buzzer} disabled={!!buzzer} name="buzzer" control={control} />
|
<CInput readOnly={!isOnboarding} disabled={!isOnboarding} name="buzzer" control={control} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="PIN" constraintText={"Code to unlock this Door in Doorman"}>
|
<FormField label="PIN" constraintText={"Code to unlock this Door in Doorman"}>
|
||||||
<CInput type="password" name="pin" control={control} />
|
<CInput type="password" name="pin" control={control} />
|
||||||
@ -246,6 +233,14 @@ export const EditPage = ({ isOnboarding }: EditPageProps) => {
|
|||||||
<FormField label="Welcome Message" constraintText="Message to display after a successful unlock">
|
<FormField label="Welcome Message" constraintText="Message to display after a successful unlock">
|
||||||
<CTextArea name="greeting" control={control} />
|
<CTextArea name="greeting" control={control} />
|
||||||
</FormField>
|
</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>
|
</SpaceBetween>
|
||||||
</Container>
|
</Container>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@ -1,243 +0,0 @@
|
|||||||
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";
|
|
||||||
import { readSync } from "fs";
|
|
||||||
|
|
||||||
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) {
|
|
||||||
fetchUrlEncoded('/api/door/auth', {
|
|
||||||
door: "onboardingflag",
|
|
||||||
key: 1234
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(res => {
|
|
||||||
if (res.status === "CLOSED" || !res.msg) {
|
|
||||||
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