import * as React from 'react'; import {useContext, useEffect, useState} from 'react'; import { Alert, CardActions, CardContent, FormControl, LinearProgress, Link, Portal, Select, Snackbar, Stack, Table, TableBody, TableCell, TableHead, TableRow, useMediaQuery } from "@mui/material"; import Tooltip from '@mui/material/Tooltip'; import Typography from "@mui/material/Typography"; import EditIcon from '@mui/icons-material/Edit'; import Container from "@mui/material/Container"; import Card from "@mui/material/Card"; import Button from "@mui/material/Button"; import {Trans, useTranslation} from "react-i18next"; import session from "../app/Session"; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import theme from "./theme"; import Dialog from "@mui/material/Dialog"; import DialogTitle from "@mui/material/DialogTitle"; import DialogContent from "@mui/material/DialogContent"; import TextField from "@mui/material/TextField"; import routes from "./routes"; import IconButton from "@mui/material/IconButton"; import {formatBytes, formatShortDate, formatShortDateTime, truncateString, validUrl} from "../app/utils"; import accountApi, {IncorrectPasswordError, UnauthorizedError} from "../app/AccountApi"; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import {Pref, PrefGroup} from "./Pref"; import db from "../app/db"; import i18n from "i18next"; import humanizeDuration from "humanize-duration"; import UpgradeDialog from "./UpgradeDialog"; import CelebrationIcon from "@mui/icons-material/Celebration"; import {AccountContext} from "./App"; import DialogFooter from "./DialogFooter"; import {useLiveQuery} from "dexie-react-hooks"; import userManager from "../app/UserManager"; import {Paragraph} from "./styles"; import CloseIcon from "@mui/icons-material/Close"; import DialogActions from "@mui/material/DialogActions"; import {ContentCopy} from "@mui/icons-material"; import MenuItem from "@mui/material/MenuItem"; import ListItemIcon from "@mui/material/ListItemIcon"; import {PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite} from "./ReserveIcons"; import ListItemText from "@mui/material/ListItemText"; import DialogContentText from "@mui/material/DialogContentText"; const Account = () => { if (!session.exists()) { window.location.href = routes.app; return <>; } return ( ); }; const Basics = () => { const { t } = useTranslation(); return ( {t("account_basics_title")} ); }; const Username = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); const labelId = "prefUsername"; return (
{session.username()} {account?.role === "admin" ? <>{" "}👑 : ""}
) }; const ChangePassword = () => { const { t } = useTranslation(); const [dialogKey, setDialogKey] = useState(0); const [dialogOpen, setDialogOpen] = useState(false); const labelId = "prefChangePassword"; const handleDialogOpen = () => { setDialogKey(prev => prev+1); setDialogOpen(true); }; const handleDialogClose = () => { setDialogOpen(false); }; return (
⬤⬤⬤⬤⬤⬤⬤⬤⬤⬤
) }; const ChangePasswordDialog = (props) => { const { t } = useTranslation(); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [errorText, setErrorText] = useState(""); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const handleDialogSubmit = async () => { try { console.debug(`[Account] Changing password`); await accountApi.changePassword(currentPassword, newPassword); props.onClose(); } catch (e) { console.log(`[Account] Error changing password`, e); if ((e instanceof IncorrectPasswordError)) { setErrorText(t("account_basics_password_dialog_current_password_incorrect")); } else if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } // TODO show error } }; return ( {t("account_basics_password_dialog_title")} setCurrentPassword(ev.target.value)} fullWidth variant="standard" /> setNewPassword(ev.target.value)} fullWidth variant="standard" /> setConfirmPassword(ev.target.value)} fullWidth variant="standard" /> ); }; const AccountType = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); const [upgradeDialogKey, setUpgradeDialogKey] = useState(0); const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false); if (!account) { return <>; } const handleUpgradeClick = () => { setUpgradeDialogKey(k => k + 1); setUpgradeDialogOpen(true); } const handleManageBilling = async () => { try { const response = await accountApi.createBillingPortalSession(); window.open(response.redirect_url, "billing_portal"); } catch (e) { console.log(`[Account] Error changing password`, e); if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } // TODO show error } }; let accountType; if (account.role === "admin") { const tierSuffix = (account.tier) ? `(with ${account.tier.name} tier)` : `(no tier)`; accountType = `${t("account_usage_tier_admin")} ${tierSuffix}`; } else if (!account.tier) { accountType = (config.enable_payments) ? t("account_usage_tier_free") : t("account_usage_tier_basic"); } else { accountType = account.tier.name; } return ( 0} title={t("account_usage_tier_title")} description={t("account_usage_tier_description")} >
{accountType} {account.billing?.paid_until && !account.billing?.cancel_at && } {config.enable_payments && account.role === "user" && !account.billing?.subscription && } {config.enable_payments && account.role === "user" && account.billing?.subscription && } {config.enable_payments && account.role === "user" && account.billing?.customer && } setUpgradeDialogOpen(false)} />
{account.billing?.status === "past_due" && {t("account_usage_tier_payment_overdue")} } {account.billing?.cancel_at > 0 && {t("account_usage_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })} }
) }; const Stats = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); if (!account) { return <>; } const normalize = (value, max) => { return Math.min(value / max * 100, 100); }; return ( {t("account_usage_title")} {account.role !== "admin" && {account.limits.reservations > 0 && <>
{account.stats.reservations} {account.role === "user" ? t("account_usage_of_limit", {limit: account.limits.reservations}) : t("account_usage_unlimited")}
0 ? normalize(account.stats.reservations, account.limits.reservations) : 100} /> } {account.limits.reservations === 0 && No reserved topics for this account }
} {t("account_usage_messages_title")} }>
{account.stats.messages} {account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.messages }) : t("account_usage_unlimited")}
{t("account_usage_emails_title")} }>
{account.stats.emails} {account.role === "user" ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}
{formatBytes(account.stats.attachment_total_size)} {account.role === "user" ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}
{account.role === "user" && account.limits.basis === "ip" && {t("account_usage_basis_ip_description")} }
); }; const InfoIcon = () => { return ( ); } const Tokens = () => { const { t } = useTranslation(); const { account } = useContext(AccountContext); const [dialogKey, setDialogKey] = useState(0); const [dialogOpen, setDialogOpen] = useState(false); const tokens = account?.tokens || []; const handleCreateClick = () => { setDialogKey(prev => prev+1); setDialogOpen(true); }; const handleDialogClose = () => { setDialogOpen(false); }; const handleDialogSubmit = async (user) => { setDialogOpen(false); // }; return ( {t("account_tokens_title")} }} /> {tokens?.length > 0 && } ); }; const TokensTable = (props) => { const { t } = useTranslation(); const [snackOpen, setSnackOpen] = useState(false); const [upsertDialogKey, setUpsertDialogKey] = useState(0); const [upsertDialogOpen, setUpsertDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [selectedToken, setSelectedToken] = useState(null); const tokens = (props.tokens || []) .sort( (a, b) => { if (a.token === session.token()) { return -1; } else if (b.token === session.token()) { return 1; } return a.token.localeCompare(b.token); }); const handleEditClick = (token) => { setUpsertDialogKey(prev => prev+1); setSelectedToken(token); setUpsertDialogOpen(true); }; const handleDialogClose = () => { setUpsertDialogOpen(false); setDeleteDialogOpen(false); setSelectedToken(null); }; const handleDeleteClick = async (token) => { setSelectedToken(token); setDeleteDialogOpen(true); }; const handleCopy = async (token) => { await navigator.clipboard.writeText(token); setSnackOpen(true); }; return ( {t("account_tokens_table_token_header")} {t("account_tokens_table_label_header")} {t("account_tokens_table_expires_header")} {tokens.map(token => ( {token.token.slice(0, 20)} ... handleCopy(token.token)}> {token.token === session.token() && {t("account_tokens_table_current_session")}} {token.token !== session.token() && (token.label || "-")} {token.expires ? formatShortDateTime(token.expires) : {t("account_tokens_table_never_expires")}} {token.token !== session.token() && <> handleEditClick(token)} aria-label={t("account_tokens_dialog_title_edit")}> handleDeleteClick(token)} aria-label={t("account_tokens_dialog_title_delete")}> } {token.token === session.token() && } ))} setSnackOpen(false)} message={t("account_tokens_table_copied_to_clipboard")} />
); }; const TokenDialog = (props) => { const { t } = useTranslation(); const [label, setLabel] = useState(props.token?.label || ""); const [expires, setExpires] = useState(props.token ? -1 : 0); const [errorText, setErrorText] = useState(""); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const editMode = !!props.token; const handleSubmit = async () => { try { if (editMode) { await accountApi.updateToken(props.token.token, label, expires); } else { await accountApi.createToken(label, expires); } props.onClose(); } catch (e) { console.log(`[Account] Error creating token`, e); if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } // TODO show error } }; return ( {editMode ? t("account_tokens_dialog_title_edit") : t("account_tokens_dialog_title_create")} setLabel(ev.target.value)} fullWidth variant="standard" /> ); }; const TokenDeleteDialog = (props) => { const { t } = useTranslation(); const handleSubmit = async () => { try { await accountApi.deleteToken(props.token.token); props.onClose(); } catch (e) { console.log(`[Account] Error deleting token`, e); if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } // TODO show error } }; return ( {t("account_tokens_delete_dialog_title")} ); } const Delete = () => { const { t } = useTranslation(); return ( {t("account_delete_title")} ); }; const DeleteAccount = () => { const { t } = useTranslation(); const [dialogKey, setDialogKey] = useState(0); const [dialogOpen, setDialogOpen] = useState(false); const handleDialogOpen = () => { setDialogKey(prev => prev+1); setDialogOpen(true); }; const handleDialogClose = () => { setDialogOpen(false); }; return (
) }; const DeleteAccountDialog = (props) => { const { t } = useTranslation(); const { account } = useContext(AccountContext); const [password, setPassword] = useState(""); const [errorText, setErrorText] = useState(""); const fullScreen = useMediaQuery(theme.breakpoints.down('sm')); const handleSubmit = async () => { try { await accountApi.delete(password); await db.delete(); console.debug(`[Account] Account deleted`); session.resetAndRedirect(routes.app); } catch (e) { console.log(`[Account] Error deleting account`, e); if ((e instanceof IncorrectPasswordError)) { setErrorText(t("account_basics_password_dialog_current_password_incorrect")); } else if ((e instanceof UnauthorizedError)) { session.resetAndRedirect(routes.login); } // TODO show error } }; return ( {t("account_delete_title")} {t("account_delete_dialog_description")} setPassword(ev.target.value)} fullWidth variant="standard" /> {account?.billing?.subscription && {t("account_delete_dialog_billing_warning")} } ); }; export default Account;