fix: auth and cart API

This commit is contained in:
IlyaDoronin 2023-08-30 12:56:14 +03:00
parent 731157322a
commit 5ef7adc198
9 changed files with 598 additions and 484 deletions

@ -1,13 +1,23 @@
import { makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@root/utils/parse-error";
const apiUrl = process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
const apiUrl =
process.env.NODE_ENV === "production" ? "" : "https://hub.pena.digital";
export function logout() {
return makeRequest<never, void>({
export async function logout(): Promise<[unknown, string?]> {
try {
const logoutResponse = await makeRequest<never, void>({
url: apiUrl + "/auth/logout",
method: "POST",
useToken: true,
withCredentials: true,
});
return [logoutResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [null, `Не удалось выйти. ${error}`];
}
}

@ -1,39 +1,84 @@
import { UserAccount, makeRequest } from "@frontend/kitui";
import { parseAxiosError } from "@root/utils/parse-error";
const apiUrl = process.env.NODE_ENV === "production" ? "/customer" : "https://hub.pena.digital/customer";
const apiUrl =
process.env.NODE_ENV === "production"
? "/customer"
: "https://hub.pena.digital/customer";
export function patchCart(tariffId: string) {
return makeRequest<never, string[]>({
export async function patchCart(
tariffId: string
): Promise<[string[], string?]> {
try {
const patchCartResponse = await makeRequest<never, string[]>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "PATCH",
useToken: true,
});
return [patchCartResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [[], `Не удалось добавить товар в корзину. ${error}`];
}
}
export function deleteCart(tariffId: string) {
return makeRequest<never, string[]>({
export async function deleteCart(
tariffId: string
): Promise<[string[], string?]> {
try {
const deleteCartResponse = await makeRequest<never, string[]>({
url: apiUrl + `/cart?id=${tariffId}`,
method: "DELETE",
useToken: true,
});
return [deleteCartResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [[], `Не удалось удалить товар из корзины. ${error}`];
}
}
export function payCart() {
return makeRequest<never, UserAccount>({
export async function payCart(): Promise<[UserAccount | null, string?]> {
try {
const payCartResponse = await makeRequest<never, UserAccount>({
url: apiUrl + "/cart/pay",
method: "POST",
useToken: true,
});
return [payCartResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [null, `Не удалось оплатить товар из корзины. ${error}`];
}
}
export function patchCurrency(currency: string) {
return makeRequest<{ currency: string; }, UserAccount>({
export async function patchCurrency(
currency: string
): Promise<[UserAccount | null, string?]> {
try {
const patchCurrencyResponse = await makeRequest<
{ currency: string },
UserAccount
>({
url: apiUrl + "/wallet",
method: "PATCH",
useToken: true,
body: {
currency
currency,
},
});
return [patchCurrencyResponse];
} catch (nativeError) {
const error = parseAxiosError(nativeError);
return [null, `Не удалось изменить валюту. ${error}`];
}
}

@ -1,12 +1,25 @@
import { useState, useRef } from "react";
import { Typography, Drawer, useMediaQuery, useTheme, Box, IconButton, Badge, Button } from "@mui/material";
import {
Typography,
Drawer,
useMediaQuery,
useTheme,
Box,
IconButton,
Badge,
Button,
} from "@mui/material";
import { getMessageFromFetchError } from "@frontend/kitui";
import SectionWrapper from "./SectionWrapper";
import CustomWrapperDrawer from "./CustomWrapperDrawer";
import { NotificationsModal } from "./NotificationsModal";
import { useCart } from "@root/utils/hooks/useCart";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { closeCartDrawer, openCartDrawer, useCartStore } from "@root/stores/cart";
import {
closeCartDrawer,
openCartDrawer,
useCartStore,
} from "@root/stores/cart";
import { setUserAccount, useUserStore } from "@root/stores/user";
import { useTicketStore } from "@root/stores/tickets";
@ -20,7 +33,8 @@ import { enqueueSnackbar } from "notistack";
import { Link, useNavigate } from "react-router-dom";
export default function Drawers() {
const [openNotificationsModal, setOpenNotificationsModal] = useState<boolean>(false);
const [openNotificationsModal, setOpenNotificationsModal] =
useState<boolean>(false);
const bellRef = useRef<HTMLButtonElement | null>(null);
const navigate = useNavigate();
const theme = useTheme();
@ -33,25 +47,26 @@ export default function Drawers() {
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0);
const notificationsCount = tickets.filter(
({ user, top_message }) => user !== top_message.user_id && top_message.shown.me !== 1
({ user, top_message }) =>
user !== top_message.user_id && top_message.shown.me !== 1
).length;
function handlePayClick() {
payCart()
.then((result) => {
setUserAccount(result);
})
.catch((error) => {
if (isAxiosError(error) && error.response?.status === 402) {
async function handlePayClick() {
const [payCartResponse, payCartError] = await payCart();
if (payCartError) {
const notEnoughMoneyAmount = parseInt(
(error.response.data.message as string).replace("insufficient funds: ", "")
payCartError.replace("insufficient funds: ", "")
);
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
} else {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
return enqueueSnackbar(payCartError);
}
if (payCartResponse) {
setUserAccount(payCartResponse);
}
});
}
function handleReplenishWallet() {
@ -169,7 +184,12 @@ export default function Drawers() {
<CartIcon />
</Badge>
</IconButton>
<Drawer anchor={"right"} open={isDrawerOpen} onClose={closeCartDrawer} sx={{ background: "rgba(0, 0, 0, 0.55)" }}>
<Drawer
anchor={"right"}
open={isDrawerOpen}
onClose={closeCartDrawer}
sx={{ background: "rgba(0, 0, 0, 0.55)" }}
>
<SectionWrapper
maxWidth="lg"
sx={{
@ -280,7 +300,11 @@ export default function Drawers() {
variant="pena-contained-dark"
component={Link}
to="/cart"
onClick={notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet}
onClick={
notEnoughMoneyAmount === 0
? handlePayClick
: handleReplenishWallet
}
sx={{ mt: "25px" }}
>
Оплатить

@ -1,20 +1,22 @@
import { Link, useNavigate } from "react-router-dom";
import {
Box, Container,
Typography,
useTheme
} from "@mui/material";
import { Box, Container, Typography, useTheme } from "@mui/material";
import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo";
import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
import { AvatarButton, LogoutButton, WalletButton, clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import {
AvatarButton,
LogoutButton,
WalletButton,
clearAuthToken,
} from "@frontend/kitui";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import type { ReactNode } from "react";
import { clearTickets } from "@root/stores/tickets";
import type { ReactNode } from "react";
interface Props {
children: ReactNode;
@ -24,19 +26,20 @@ export default function NavbarFull({ children }: Props) {
const theme = useTheme();
const navigate = useNavigate();
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const initials = useUserStore(state => state.initials);
const initials = useUserStore((state) => state.initials);
async function handleLogoutClick() {
try {
await logout();
const [_, logoutError] = await logout();
if (logoutError) {
return enqueueSnackbar(logoutError);
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
} catch (error: any) {
const message = getMessageFromFetchError(error, "Не удалось выйти");
if (message) enqueueSnackbar(message);
}
}
return (
@ -64,11 +67,7 @@ export default function NavbarFull({ children }: Props) {
<Menu />
<Box sx={{ display: "flex", ml: "auto" }}>
<Drawers />
<WalletButton
component={Link}
to="/wallet"
sx={{ ml: "20px" }}
/>
<WalletButton component={Link} to="/wallet" sx={{ ml: "20px" }} />
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
<Typography
sx={{
@ -79,22 +78,14 @@ export default function NavbarFull({ children }: Props) {
>
Мой баланс
</Typography>
<Typography
variant="body2"
color={theme.palette.purple.main}
>
<Typography variant="body2" color={theme.palette.purple.main}>
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton
component={Link}
to="/settings"
sx={{ ml: "27px" }}
>{initials}</AvatarButton>
<LogoutButton
onClick={handleLogoutClick}
sx={{ ml: "20px" }}
/>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<LogoutButton onClick={handleLogoutClick} sx={{ ml: "20px" }} />
</Box>
</Container>
<Box>{children}</Box>

@ -11,7 +11,7 @@ import Drawers from "../Drawers";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
import { AvatarButton, clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import { AvatarButton, clearAuthToken } from "@frontend/kitui";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
@ -24,20 +24,20 @@ export const NavbarPanel = () => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const cash = useUserStore((state) => state.userAccount?.wallet.cash) ?? 0;
const initials = useUserStore(state => state.initials);
const initials = useUserStore((state) => state.initials);
async function handleLogoutClick() {
try {
await logout();
const [_, logoutError] = await logout();
if (logoutError) {
return enqueueSnackbar(logoutError);
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
} catch (error: any) {
const message = getMessageFromFetchError(error, "Не удалось выйти");
if (message) enqueueSnackbar(message);
}
}
return (
@ -71,11 +71,9 @@ export const NavbarPanel = () => {
{currencyFormatter.format(cash / 100)}
</Typography>
</Box>
<AvatarButton
component={Link}
to="/settings"
sx={{ ml: "27px" }}
>{initials}</AvatarButton>
<AvatarButton component={Link} to="/settings" sx={{ ml: "27px" }}>
{initials}
</AvatarButton>
<IconButton
onClick={handleLogoutClick}
sx={{

@ -1,6 +1,14 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Box, Button, Container, IconButton, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
Box,
Button,
Container,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import SectionWrapper from "../SectionWrapper";
import LogoutIcon from "../icons/LogoutIcon";
import WalletIcon from "../icons/WalletIcon";
@ -11,7 +19,11 @@ import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUserData, useUserStore } from "@root/stores/user";
import { BurgerButton, clearAuthToken, getMessageFromFetchError } from "@frontend/kitui";
import {
BurgerButton,
clearAuthToken,
getMessageFromFetchError,
} from "@frontend/kitui";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { clearCustomTariffs } from "@root/stores/customTariffs";
import { clearTickets } from "@root/stores/tickets";
@ -30,17 +42,17 @@ export default function NavbarFull({ isLoggedIn }: Props) {
const [open, setOpen] = useState(false);
async function handleLogoutClick() {
try {
await logout();
const [_, logoutError] = await logout();
if (logoutError) {
return enqueueSnackbar(logoutError);
}
clearAuthToken();
clearUserData();
clearCustomTariffs();
clearTickets();
navigate("/");
} catch (error: any) {
const message = getMessageFromFetchError(error, "Не удалось выйти");
if (message) enqueueSnackbar(message);
}
}
return isLoggedIn ? (
@ -70,7 +82,10 @@ export default function NavbarFull({ isLoggedIn }: Props) {
}}
>
<Drawers />
<IconButton sx={{ p: 0, ml: "8px" }} onClick={() => navigate("/wallet")}>
<IconButton
sx={{ p: 0, ml: "8px" }}
onClick={() => navigate("/wallet")}
>
<WalletIcon color={theme.palette.gray.main} bgcolor="#F2F3F7" />
</IconButton>
<Box sx={{ ml: "8px", whiteSpace: "nowrap" }}>
@ -134,7 +149,10 @@ export default function NavbarFull({ isLoggedIn }: Props) {
>
Личный кабинет
</Button>
<BurgerButton onClick={() => setOpen(!open)} sx={{ color: "white", display: !isTablet ? "block" : "none" }} />
<BurgerButton
onClick={() => setOpen(!open)}
sx={{ color: "white", display: !isTablet ? "block" : "none" }}
/>
</SectionWrapper>
</>
);

@ -1,4 +1,11 @@
import { Alert, Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
Alert,
Box,
Button,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { currencyFormatter } from "@root/utils/currencyFormatter";
import { payCart } from "@root/api/cart";
import { setUserAccount } from "@root/stores/user";
@ -13,29 +20,32 @@ interface Props {
priceAfterDiscounts: number;
}
export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }: Props) {
export default function TotalPrice({
priceAfterDiscounts,
priceBeforeDiscounts,
}: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.down(550));
const [notEnoughMoneyAmount, setNotEnoughMoneyAmount] = useState<number>(0);
const navigate = useNavigate();
function handlePayClick() {
payCart()
.then((result) => {
setUserAccount(result);
})
.catch((error) => {
if (isAxiosError(error) && error.response?.status === 402) {
async function handlePayClick() {
const [payCartResponse, payCartError] = await payCart();
if (payCartError) {
const notEnoughMoneyAmount = parseInt(
(error.response.data.message as string).replace("insufficient funds: ", "")
payCartError.replace("insufficient funds: ", "")
);
setNotEnoughMoneyAmount(notEnoughMoneyAmount);
} else {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
return enqueueSnackbar(payCartError);
}
if (payCartResponse) {
setUserAccount(payCartResponse);
}
});
}
function handleReplenishWallet() {
@ -65,8 +75,9 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }
Итоговая цена
</Typography>
<Typography color={theme.palette.gray.main}>
Текст-заполнитель это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
это текст, который имеет Текст-заполнитель это текст, который имеет Текст-заполнитель
Текст-заполнитель это текст, который имеет Текст-заполнитель это
текст, который имеет Текст-заполнитель это текст, который имеет
Текст-заполнитель это текст, который имеет Текст-заполнитель
</Typography>
</Box>
<Box
@ -108,9 +119,13 @@ export default function TotalPrice({ priceAfterDiscounts, priceBeforeDiscounts }
)}
<Button
variant="pena-contained-dark"
onClick={notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet}
onClick={
notEnoughMoneyAmount === 0 ? handlePayClick : handleReplenishWallet
}
sx={{ mt: "10px" }}
>{notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}</Button>
>
{notEnoughMoneyAmount === 0 ? "Оплатить" : "Пополнить"}
</Button>
</Box>
</Box>
);

@ -293,18 +293,29 @@ export const sendUserData = async () => {
};
export const addTariffToCart = async (tariffId: string) => {
const result = await patchCart(tariffId);
setCart(result);
const [patchCartResponse, patchCartError] = await patchCart(tariffId);
if (!patchCartError) {
setCart(patchCartResponse);
}
};
export const removeTariffFromCart = async (tariffId: string) => {
const result = await deleteCart(tariffId);
setCart(result);
const [deleteCartResponse, deleteCartError] = await deleteCart(tariffId);
if (!deleteCartError) {
setCart(deleteCartResponse);
}
};
export const changeUserCurrency = async (currency: string) => {
const result = await patchCurrency(currency);
setUserAccount(result);
const [patchCurrencyResponse, patchCurrencyError] = await patchCurrency(
currency
);
if (!patchCurrencyError && patchCurrencyResponse) {
setUserAccount(patchCurrencyResponse);
}
};
const validators: Record<UserSettingsField | keyof UserName, StringSchema> = {

@ -1,23 +1,25 @@
import { AxiosError } from "axios";
const parseAxiosError = (error: AxiosError): string => {
export const parseAxiosError = (nativeError: unknown): string => {
const error = nativeError as AxiosError;
switch (error.status) {
case 404:
return "Не найдено";
return "Не найдено.";
case 403:
return "Доступ ограничен";
return "Доступ ограничен.";
case 401:
return "Ошибка авторизации";
return "Ошибка авторизации.";
case 500:
return "Внутренняя ошибка сервера";
return "Внутренняя ошибка сервера.";
case 503:
return "Сервис недоступен";
return "Сервис недоступен.";
default:
return "Неизвестная ошибка сервера";
return "Неизвестная ошибка сервера.";
}
};