2023-02-13 12:41:30 +00:00
|
|
|
import React, { 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,
|
2023-05-25 07:03:57 +00:00
|
|
|
} from "@mui/material";
|
2023-02-13 12:41:30 +00:00
|
|
|
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
|
2023-03-14 17:40:24 +00:00
|
|
|
minRows={3}
|
2023-02-13 12:41:30 +00:00
|
|
|
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,
|
|
|
|
|
onFailure,
|
|
|
|
|
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",
|
|
|
|
|
},
|
|
|
|
|
});
|
2023-03-14 17:40:24 +00:00
|
|
|
|
|
|
|
|
if (response && response.ok) {
|
|
|
|
|
onSuccess();
|
|
|
|
|
} else {
|
|
|
|
|
onFailure(response.status || 400);
|
|
|
|
|
}
|
2023-02-13 12:41:30 +00:00
|
|
|
} catch (error: any) {
|
|
|
|
|
console.error("Failed to request verification code:", error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const VerificationCodeRequest = ({
|
|
|
|
|
verifyMode,
|
|
|
|
|
data,
|
|
|
|
|
onSuccess,
|
|
|
|
|
onFailure,
|
|
|
|
|
}: any) => {
|
|
|
|
|
React.useEffect(() => {
|
|
|
|
|
(async () => {
|
|
|
|
|
await handleRequestCode({
|
|
|
|
|
verifyMode,
|
|
|
|
|
id: data.id,
|
|
|
|
|
onSuccess,
|
|
|
|
|
onFailure,
|
|
|
|
|
});
|
|
|
|
|
})();
|
2023-03-15 12:17:43 +00:00
|
|
|
}, [data.id, onFailure, onSuccess, verifyMode]);
|
2023-02-13 12:41:30 +00:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
onFailure,
|
|
|
|
|
handleClose,
|
|
|
|
|
}: any) => {
|
|
|
|
|
const [code, setCode] = React.useState(undefined);
|
|
|
|
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
|
|
|
|
|
|
|
|
|
const handleSubmitVerification = async () => {
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
await handleRequestCode({
|
|
|
|
|
verifyMode,
|
|
|
|
|
id: data.id,
|
|
|
|
|
onSuccess,
|
|
|
|
|
onFailure,
|
|
|
|
|
captchaCode: code,
|
|
|
|
|
});
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-14 17:40:24 +00:00
|
|
|
const handleCaptchaChange = (value: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
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,
|
2023-03-14 17:40:24 +00:00
|
|
|
}: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
const [code, setValue] = React.useState("");
|
|
|
|
|
const [isSubmitting, setSubmitting] = React.useState(false);
|
|
|
|
|
const [isValid, setValid] = React.useState(false);
|
|
|
|
|
const [submissionError, setSubmissionError] = React.useState(undefined);
|
|
|
|
|
const translate = useTranslate();
|
|
|
|
|
|
2023-03-14 17:40:24 +00:00
|
|
|
const validator = (v: any) => v.trim().length === 6;
|
2023-02-13 12:41:30 +00:00
|
|
|
|
2023-03-14 17:40:24 +00:00
|
|
|
const handleValueChange = (newValue: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
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)
|
2023-03-14 17:40:24 +00:00
|
|
|
// @ts-expect-error
|
2023-02-13 12:41:30 +00:00
|
|
|
setSubmissionError(`Error: ${responseBody.message}`);
|
2023-03-14 17:40:24 +00:00
|
|
|
else {
|
2023-02-13 12:41:30 +00:00
|
|
|
setSubmissionError(
|
2023-03-14 17:40:24 +00:00
|
|
|
// @ts-expect-error
|
2023-02-13 12:41:30 +00:00
|
|
|
"There was an error, sorry about that. Please try again later or contact support."
|
|
|
|
|
);
|
2023-03-14 17:40:24 +00:00
|
|
|
}
|
2023-02-13 12:41:30 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-14 17:40:24 +00:00
|
|
|
const VerificationCodeDialog = (props: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
const [stage, setStage] = React.useState("request");
|
|
|
|
|
const onRequestSuccess = () => setStage("verify");
|
|
|
|
|
const onRestartVerification = () => setStage("request");
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
setStage("request");
|
|
|
|
|
props.handleClose();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onFailure = (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}
|
|
|
|
|
onFailure={onFailure}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{props.open && stage === "verify" && (
|
|
|
|
|
<VerificationCodeInput
|
|
|
|
|
{...props}
|
|
|
|
|
handleRestartVerification={onRestartVerification}
|
|
|
|
|
handleClose={handleClose}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
{props.open && stage === "captcha" && (
|
|
|
|
|
<VerificationCaptcha
|
|
|
|
|
mode={props.verifyMode}
|
|
|
|
|
onSuccess={onRequestSuccess}
|
|
|
|
|
onFailure={onRestartVerification}
|
|
|
|
|
handleClose={handleClose}
|
|
|
|
|
{...props}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2023-03-15 12:17:43 +00:00
|
|
|
const SignalBotShowActions = ({ data }: any) => {
|
2023-02-13 12:41:30 +00:00
|
|
|
const [open, setOpen] = React.useState(false);
|
|
|
|
|
const [verifyMode, setVerifyMode] = React.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>
|
2023-03-14 17:40:24 +00:00
|
|
|
<EditButton record={data} />
|
2023-02-13 12:41:30 +00:00
|
|
|
{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 = (props: ShowProps) => (
|
|
|
|
|
<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;
|