WIP Twilio
This commit is contained in:
parent
214efbde36
commit
cea434a57c
34 changed files with 311 additions and 143 deletions
|
@ -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()) {
|
||||
|
|
|
@ -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}`];
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
@ -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 &&
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue