WIP Twilio

This commit is contained in:
binwiederhier 2023-05-12 21:47:41 -04:00
parent 214efbde36
commit cea434a57c
34 changed files with 311 additions and 143 deletions

View file

@ -1,7 +1,7 @@
import {
accountBillingPortalUrl,
accountBillingSubscriptionUrl,
accountPasswordUrl,
accountPasswordUrl, accountPhoneUrl,
accountReservationSingleUrl,
accountReservationUrl,
accountSettingsUrl,
@ -299,6 +299,43 @@ class AccountApi {
return await response.json(); // May throw SyntaxError
}
async verifyPhone(phoneNumber) {
const url = accountPhoneUrl(config.base_url);
console.log(`[AccountApi] Sending phone verification ${url}`);
await fetchOrThrow(url, {
method: "PUT",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber
})
});
}
async checkVerifyPhone(phoneNumber, code) {
const url = accountPhoneUrl(config.base_url);
console.log(`[AccountApi] Checking phone verification code ${url}`);
await fetchOrThrow(url, {
method: "POST",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber,
code: code
})
});
}
async deletePhoneNumber(phoneNumber, code) {
const url = accountPhoneUrl(config.base_url);
console.log(`[AccountApi] Deleting phone number ${url}`);
await fetchOrThrow(url, {
method: "DELETE",
headers: withBearerAuth({}, session.token()),
body: JSON.stringify({
number: phoneNumber
})
});
}
async sync() {
try {
if (!session.token()) {

View file

@ -27,6 +27,7 @@ export const accountReservationUrl = (baseUrl) => `${baseUrl}/v1/account/reserva
export const accountReservationSingleUrl = (baseUrl, topic) => `${baseUrl}/v1/account/reservation/${topic}`;
export const accountBillingSubscriptionUrl = (baseUrl) => `${baseUrl}/v1/account/billing/subscription`;
export const accountBillingPortalUrl = (baseUrl) => `${baseUrl}/v1/account/billing/portal`;
export const accountPhoneUrl = (baseUrl) => `${baseUrl}/v1/account/phone`;
export const tiersUrl = (baseUrl) => `${baseUrl}/v1/tiers`;
export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
export const expandUrl = (url) => [`https://${url}`, `http://${url}`];

View file

@ -325,37 +325,183 @@ const AccountType = () => {
const PhoneNumbers = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [snackOpen, setSnackOpen] = useState(false);
const labelId = "prefPhoneNumbers";
const handleAdd = () => {
const handleDialogOpen = () => {
setDialogKey(prev => prev+1);
setDialogOpen(true);
};
const handleClick = () => {
const handleDialogClose = () => {
setDialogOpen(false);
};
const handleDelete = () => {
const handleCopy = (phoneNumber) => {
navigator.clipboard.writeText(phoneNumber);
setSnackOpen(true);
};
const handleDelete = async (phoneNumber) => {
try {
await accountApi.deletePhoneNumber(phoneNumber);
} catch (e) {
console.log(`[Account] Error deleting phone number`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
}
}
};
if (!config.enable_calls) {
return null;
}
return (
<Pref labelId={labelId} title={t("account_basics_phone_numbers_title")} description={t("account_basics_phone_numbers_description")}>
<div aria-labelledby={labelId}>
{account?.phone_numbers.map(p =>
<Chip
label={p.number}
variant="outlined"
onClick={() => navigator.clipboard.writeText(p.number)}
onDelete={() => handleDelete(p.number)}
/>
{account?.phone_numbers?.map(phoneNumber =>
<Chip
label={
<Tooltip title={t("common_copy_to_clipboard")}>
<span>{phoneNumber}</span>
</Tooltip>
}
variant="outlined"
onClick={() => handleCopy(phoneNumber)}
onDelete={() => handleDelete(phoneNumber)}
/>
)}
<IconButton onClick={() => handleAdd()}><AddIcon/></IconButton>
{!account?.phone_numbers &&
<em>{t("account_basics_phone_numbers_no_phone_numbers_yet")}</em>
}
<IconButton onClick={handleDialogOpen}><AddIcon/></IconButton>
</div>
<AddPhoneNumberDialog
key={`addPhoneNumberDialog${dialogKey}`}
open={dialogOpen}
onClose={handleDialogClose}
/>
<Portal>
<Snackbar
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message={t("account_basics_phone_numbers_copied_to_clipboard")}
/>
</Portal>
</Pref>
)
};
const AddPhoneNumberDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [code, setCode] = useState("");
const [sending, setSending] = useState(false);
const [verificationCodeSent, setVerificationCodeSent] = useState(false);
const fullScreen = useMediaQuery(theme.breakpoints.down('sm'));
const handleDialogSubmit = async () => {
if (!verificationCodeSent) {
await verifyPhone();
} else {
await checkVerifyPhone();
}
};
const handleCancel = () => {
if (verificationCodeSent) {
setVerificationCodeSent(false);
} else {
props.onClose();
}
};
const verifyPhone = async () => {
try {
setSending(true);
await accountApi.verifyPhone(phoneNumber);
setVerificationCodeSent(true);
} catch (e) {
console.log(`[Account] Error sending verification`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
} finally {
setSending(false);
}
};
const checkVerifyPhone = async () => {
try {
setSending(true);
await accountApi.checkVerifyPhone(phoneNumber, code);
props.onClose();
} catch (e) {
console.log(`[Account] Error confirming verification`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
} finally {
setSending(false);
}
};
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{t("account_basics_phone_numbers_dialog_title")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("account_basics_phone_numbers_dialog_description")}
</DialogContentText>
{!verificationCodeSent &&
<TextField
margin="dense"
label={t("account_basics_phone_numbers_dialog_number_label")}
aria-label={t("account_basics_phone_numbers_dialog_number_label")}
placeholder={t("account_basics_phone_numbers_dialog_number_placeholder")}
type="tel"
value={phoneNumber}
onChange={ev => setPhoneNumber(ev.target.value)}
fullWidth
inputProps={{ inputMode: 'tel', pattern: '\+[0-9]*' }}
variant="standard"
/>
}
{verificationCodeSent &&
<TextField
margin="dense"
label={t("account_basics_phone_numbers_dialog_code_label")}
aria-label={t("account_basics_phone_numbers_dialog_code_label")}
placeholder={t("account_basics_phone_numbers_dialog_code_placeholder")}
type="text"
value={code}
onChange={ev => setCode(ev.target.value)}
fullWidth
inputProps={{ inputMode: 'numeric', pattern: '[0-9]*' }}
variant="standard"
/>
}
</DialogContent>
<DialogFooter status={error}>
<Button onClick={handleCancel}>{verificationCodeSent ? t("common_back") : t("common_cancel")}</Button>
<Button onClick={handleDialogSubmit} disabled={sending || !/^\+\d+$/.test(phoneNumber)}>
{verificationCodeSent ?t("account_basics_phone_numbers_dialog_check_verification_button") : t("account_basics_phone_numbers_dialog_send_verification_button")}
</Button>
</DialogFooter>
</Dialog>
);
};
const Stats = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
@ -594,7 +740,7 @@ const TokensTable = (props) => {
<span>
<span style={{fontFamily: "Monospace", fontSize: "0.9rem"}}>{token.token.slice(0, 12)}</span>
...
<Tooltip title={t("account_tokens_table_copy_to_clipboard")} placement="right">
<Tooltip title={t("common_copy_to_clipboard")} placement="right">
<IconButton onClick={() => handleCopy(token.token)}><ContentCopy/></IconButton>
</Tooltip>
</span>

View file

@ -288,7 +288,7 @@ const LoginPage = (props) => {
/>
</DialogContent>
<DialogFooter status={error}>
<Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
<Button onClick={props.onBack}>{t("common_back")}</Button>
<Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
</DialogFooter>
</>

View file

@ -300,11 +300,9 @@ const TierCard = (props) => {
{tier.limits.reservations > 0 && <Feature>{t("account_upgrade_dialog_tier_features_reservations", { reservations: tier.limits.reservations, count: tier.limits.reservations })}</Feature>}
<Feature>{t("account_upgrade_dialog_tier_features_messages", { messages: formatNumber(tier.limits.messages), count: tier.limits.messages })}</Feature>
<Feature>{t("account_upgrade_dialog_tier_features_emails", { emails: formatNumber(tier.limits.emails), count: tier.limits.emails })}</Feature>
{tier.limits.sms > 0 && <Feature>{t("account_upgrade_dialog_tier_features_sms", { sms: formatNumber(tier.limits.sms), count: tier.limits.sms })}</Feature>}
{tier.limits.calls > 0 && <Feature>{t("account_upgrade_dialog_tier_features_calls", { calls: formatNumber(tier.limits.calls), count: tier.limits.calls })}</Feature>}
<Feature>{t("account_upgrade_dialog_tier_features_attachment_file_size", { filesize: formatBytes(tier.limits.attachment_file_size, 0) })}</Feature>
{tier.limits.reservations === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_reservations")}</NoFeature>}
{tier.limits.sms === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_sms")}</NoFeature>}
{tier.limits.calls === 0 && <NoFeature>{t("account_upgrade_dialog_tier_features_no_calls")}</NoFeature>}
</List>
{tier.prices && props.interval === SubscriptionInterval.MONTH &&