link-stack/apps/metamigo-frontend/app/_components/signal/bots/SignalBotShow.tsx
2023-07-05 09:39:24 +00:00

477 lines
11 KiB
TypeScript

"use client";
import { FC, useEffect, useState } from "react";
import {
Show,
SimpleShowLayout,
BooleanField,
TextField,
ShowProps,
EditButton,
TopToolbar,
useTranslate,
useRefresh,
} from "react-admin";
import {
TextField as MuiTextField,
Button,
Card,
Grid,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
Typography,
Box,
CircularProgress,
} from "@mui/material";
import { SixDigitInput } from "../../DigitInput";
import {
sanitizeE164Number,
isValidE164Number,
} from "../../../_lib/phone-numbers";
const Sidebar = ({ record }: any) => {
const [phoneNumber, setPhoneNumber] = useState("");
const [errorNumber, setErrorNumber] = useState(false);
const handlePhoneNumberChange = (event: any) => {
setPhoneNumber(event.target.value);
};
const [message, setMessage] = useState("");
const handleMessageChange = (event: any) => {
setMessage(event.target.value);
};
const sendMessage = async (phoneNumber: string, message: string) => {
await fetch(`/api/v1/signal/bots/${record.token}/send`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber, message }),
});
};
const resetSession = async (phoneNumber: string) => {
await fetch(`/api/v1/signal/bots/${record.token}/resetSession`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ phoneNumber }),
});
};
const handleBlurNumber = () => {
setErrorNumber(!isValidE164Number(sanitizeE164Number(phoneNumber)));
};
const handleSend = () => {
const sanitized = sanitizeE164Number(phoneNumber);
if (isValidE164Number(sanitized)) {
setErrorNumber(false);
sendMessage(sanitized, message);
} else setErrorNumber(false);
};
const handleResetSession = () => {
const sanitized = sanitizeE164Number(phoneNumber);
if (isValidE164Number(sanitized)) {
setErrorNumber(false);
resetSession(sanitized);
} else setErrorNumber(false);
};
return (
<Card style={{ width: "33%", marginLeft: 20, padding: 14 }}>
<Grid container direction="column" spacing={2}>
<Grid item>
<Typography variant="h6">Send message</Typography>
</Grid>
<Grid item>
<MuiTextField
variant="outlined"
label="Phone number"
fullWidth
size="small"
error={errorNumber}
onBlur={handleBlurNumber}
value={phoneNumber}
onChange={handlePhoneNumberChange}
/>
</Grid>
<Grid item>
<MuiTextField
variant="outlined"
label="Message"
multiline
minRows={3}
fullWidth
size="small"
value={message}
onChange={handleMessageChange}
/>
</Grid>
<Grid item container direction="row-reverse">
<Button
variant="contained"
color="primary"
onClick={() => handleSend()}
>
Send
</Button>
<Button variant="contained" onClick={() => handleResetSession()}>
Reset Session
</Button>
</Grid>
</Grid>
</Card>
);
};
const MODE = {
SMS: "SMS",
VOICE: "VOICE",
};
const handleRequestCode = async ({
verifyMode,
id,
onSuccess,
onError,
captchaCode = undefined,
}: any) => {
if (verifyMode === MODE.SMS) console.log("REQUESTING sms");
else if (verifyMode === MODE.VOICE) console.log("REQUESTING voice");
let response: Response;
let url = `/api/v1/signal/bots/${id}/requestCode?mode=${verifyMode.toLowerCase()}`;
if (captchaCode) {
url += `&captcha=${captchaCode}`;
}
try {
response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response && response.ok) {
onSuccess();
} else {
onError(response.status || 400);
}
} catch (error: any) {
console.error("Failed to request verification code:", error);
}
};
const VerificationCodeRequest = ({
verifyMode,
data,
onSuccess,
onError,
}: any) => {
useEffect(() => {
(async () => {
await handleRequestCode({
verifyMode,
id: data.id,
onSuccess,
onError,
});
})();
}, [data.id, onError, onSuccess, verifyMode]);
return (
<>
<DialogTitle id="form-dialog-title">
Requesting code for {data.phoneNumber}
</DialogTitle>
<DialogContent>
<Box display="flex">
<Box m="auto">
<CircularProgress />
</Box>
</Box>
</DialogContent>
</>
);
};
const VerificationCaptcha = ({
verifyMode,
data,
onSuccess,
onError,
handleClose,
}: any) => {
const [code, setCode] = useState(undefined);
const [isSubmitting, setSubmitting] = useState(false);
const handleSubmitVerification = async () => {
setSubmitting(true);
await handleRequestCode({
verifyMode,
id: data.id,
onSuccess,
onError,
captchaCode: code,
});
setSubmitting(false);
};
const handleCaptchaChange = (value: any) => {
if (value)
setCode(
value
.replace(/signalcaptcha:\/\//, "")
.replace("“", "")
.replace("”", "")
.trim()
);
else setCode(value);
};
return (
<>
<DialogTitle id="form-dialog-title">
Captcha for {data.phoneNumber}
</DialogTitle>
<DialogContent>
<MuiTextField
value={code}
onChange={(ev) => handleCaptchaChange(ev.target.value)}
/>
</DialogContent>
<DialogActions>
{isSubmitting && <CircularProgress />}
{!isSubmitting && (
<Button onClick={handleClose} color="primary">
Cancel
</Button>
)}
{!isSubmitting && (
<Button onClick={handleSubmitVerification} color="primary">
Request
</Button>
)}
</DialogActions>
</>
);
};
const VerificationCodeInput = ({
data,
verifyMode,
handleClose,
handleRestartVerification,
confirmVerification,
}: any) => {
const [code, setValue] = useState("");
const [isSubmitting, setSubmitting] = useState(false);
const [isValid, setValid] = useState(false);
const [submissionError, setSubmissionError] = useState(undefined);
const translate = useTranslate();
const validator = (v: any) => v.trim().length === 6;
const handleValueChange = (newValue: any) => {
setValue(newValue);
setValid(validator(newValue));
};
const handleSubmitVerification = async () => {
setSubmitting(true);
// await sleep(2000)
const response = await fetch(
`/api/v1/signal/bots/${data.id}/register?code=${code}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
}
);
setSubmitting(false);
const responseBody = await response.json();
console.log(responseBody);
if (response.status === 200) {
confirmVerification();
} else if (responseBody.message)
// @ts-expect-error
setSubmissionError(`Error: ${responseBody.message}`);
else {
setSubmissionError(
// @ts-expect-error
"There was an error, sorry about that. Please try again later or contact support."
);
}
};
const title =
verifyMode === MODE.SMS
? translate("resources.signalBots.verifyDialog.sms", {
phoneNumber: data.phoneNumber,
})
: translate("resources.signalBots.verifyDialog.voice", {
phoneNumber: data.phoneNumber,
});
return (
<>
<DialogTitle id="form-dialog-title">
Verify {data.phoneNumber}
</DialogTitle>
<DialogContent>
<DialogContentText>{title}</DialogContentText>
<SixDigitInput value={code} onChange={handleValueChange} />
{submissionError && (
<Typography variant="body1" gutterBottom color="error">
{submissionError}
</Typography>
)}
</DialogContent>
<DialogActions>
{isSubmitting && <CircularProgress />}
{!isSubmitting && (
<Button onClick={handleClose} color="primary">
Cancel
</Button>
)}
{!isSubmitting && (
<Button onClick={handleRestartVerification} color="primary">
Restart
</Button>
)}
{!isSubmitting && (
<Button
onClick={handleSubmitVerification}
color="primary"
disabled={!isValid}
>
Verify
</Button>
)}
</DialogActions>
</>
);
};
const VerificationCodeDialog = (props: any) => {
const [stage, setStage] = useState("request");
const onRequestSuccess = () => setStage("verify");
const onRestartVerification = () => setStage("request");
const handleClose = () => {
setStage("request");
props.handleClose();
};
const onError = (code: number) => {
if (code === 402 || code === 500) {
setStage("captcha");
} else {
setStage("request");
}
};
return (
<Dialog
open={props.open}
onClose={props.handleClose}
aria-labelledby="form-dialog-title"
>
{props.open && stage === "request" && (
<VerificationCodeRequest
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onError={onError}
{...props}
/>
)}
{props.open && stage === "verify" && (
<VerificationCodeInput
{...props}
handleRestartVerification={onRestartVerification}
handleClose={handleClose}
/>
)}
{props.open && stage === "captcha" && (
<VerificationCaptcha
mode={props.verifyMode}
onSuccess={onRequestSuccess}
onError={onRestartVerification}
handleClose={handleClose}
{...props}
/>
)}
</Dialog>
);
};
const SignalBotShowActions = ({ data }: any) => {
const [open, setOpen] = useState(false);
const [verifyMode, setVerifyMode] = useState("");
const refresh = useRefresh();
const handleOpenSMS = () => {
setVerifyMode(MODE.SMS);
setOpen(true);
};
const handleOpenVoice = () => {
setVerifyMode(MODE.VOICE);
setOpen(true);
};
const handleClose = () => setOpen(false);
const confirmVerification = () => {
setOpen(false);
refresh();
};
return (
<TopToolbar>
<EditButton record={data} />
{data && !data.isVerified && (
<Button onClick={handleOpenSMS} color="primary">
Verify with SMS
</Button>
)}
{data && !data.isVerified && (
<Button onClick={handleOpenVoice} color="primary">
Verify with Voice
</Button>
)}
{data && !data.isVerified && (
<VerificationCodeDialog
data={data}
verifyMode={verifyMode}
handleClose={handleClose}
open={open}
confirmVerification={confirmVerification}
/>
)}
</TopToolbar>
);
};
const SignalBotShow: FC<ShowProps> = (props) => (
<Show
actions={<SignalBotShowActions />}
{...props}
title="Signal Bot"
aside={<Sidebar />}
>
<SimpleShowLayout>
<TextField source="phoneNumber" />
<BooleanField source="isVerified" />
<TextField source="description" />
<TextField source="token" />
</SimpleShowLayout>
</Show>
);
export default SignalBotShow;