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