import * as React from 'react';
import {useContext, 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, openUrl} from "../app/utils";
import accountApi, {LimitBasis, Role, SubscriptionStatus} 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 {Paragraph} from "./styles";
import CloseIcon from "@mui/icons-material/Close";
import {ContentCopy, Public} from "@mui/icons-material";
import MenuItem from "@mui/material/MenuItem";
import DialogContentText from "@mui/material/DialogContentText";
import {IncorrectPasswordError, UnauthorizedError} from "../app/errors";
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 === 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 [error, setError] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = 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) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
}
};
return (
);
};
const AccountType = () => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [upgradeDialogKey, setUpgradeDialogKey] = useState(0);
const [upgradeDialogOpen, setUpgradeDialogOpen] = useState(false);
const [showPortalError, setShowPortalError] = 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 opening billing portal`, e);
if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setShowPortalError(true);
}
}
};
let accountType;
if (account.role === Role.ADMIN) {
const tierSuffix = (account.tier) ? t("account_basics_tier_admin_suffix_with_tier", { tier: account.tier.name }) : t("account_basics_tier_admin_suffix_no_tier");
accountType = `${t("account_basics_tier_admin")} ${tierSuffix}`;
} else if (!account.tier) {
accountType = (config.enable_payments) ? t("account_basics_tier_free") : t("account_basics_tier_basic");
} else {
accountType = account.tier.name;
}
return (
0}
title={t("account_basics_tier_title")}
description={t("account_basics_tier_description")}
>
{accountType}
{account.billing?.paid_until && !account.billing?.cancel_at &&
}
{config.enable_payments && account.role === Role.USER && !account.billing?.subscription &&
}
onClick={handleUpgradeClick}
sx={{ml: 1}}
>{t("account_basics_tier_upgrade_button")}
}
{config.enable_payments && account.role === Role.USER && account.billing?.subscription &&
}
{config.enable_payments && account.role === Role.USER && account.billing?.customer &&
}
{config.enable_payments &&
setUpgradeDialogOpen(false)}
/>
}
{account.billing?.status === SubscriptionStatus.PAST_DUE &&
{t("account_basics_tier_payment_overdue")}
}
{account.billing?.cancel_at > 0 &&
{t("account_basics_tier_canceled_subscription", { date: formatShortDate(account.billing.cancel_at) })}
}
setShowPortalError(false)}
message={t("account_usage_cannot_create_portal_session")}
/>
)
};
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 === Role.ADMIN || account.limits.reservations > 0) &&
<>
{account.stats.reservations}
{account.role === 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.role === Role.USER && account.limits.reservations === 0 &&
{t("account_usage_reservations_none")}
}
{t("account_usage_messages_title")}
>
}>
{account.stats.messages}
{account.role === 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 === Role.USER ? t("account_usage_of_limit", { limit: account.limits.emails }) : t("account_usage_unlimited")}
{formatBytes(account.stats.attachment_total_size)}
{account.role === Role.USER ? t("account_usage_of_limit", { limit: formatBytes(account.limits.attachment_total_size) }) : t("account_usage_unlimited")}
{account.role === Role.USER && account.limits.basis === LimitBasis.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")}
{t("account_tokens_table_last_access_header")}
{tokens.map(token => (
{token.token.slice(0, 12)}
...
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")}}
{formatShortDateTime(token.last_access)}
openUrl(`https://whatismyipaddress.com/ip/${token.last_origin}`)}>
{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 [error, setError] = useState("");
const [label, setLabel] = useState(props.token?.label || "");
const [expires, setExpires] = useState(props.token ? -1 : 0);
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);
} else {
setError(e.message);
}
}
};
return (
);
};
const TokenDeleteDialog = (props) => {
const { t } = useTranslation();
const [error, setError] = useState("");
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);
} else {
setError(e.message);
}
}
};
return (
);
}
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 (
} onClick={handleDialogOpen}>
{t("account_delete_title")}
)
};
const DeleteAccountDialog = (props) => {
const { t } = useTranslation();
const { account } = useContext(AccountContext);
const [error, setError] = useState("");
const [password, setPassword] = 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) {
setError(t("account_basics_password_dialog_current_password_incorrect"));
} else if (e instanceof UnauthorizedError) {
session.resetAndRedirect(routes.login);
} else {
setError(e.message);
}
}
};
return (
);
};
export default Account;