add apply promocode button

This commit is contained in:
nflnkr 2024-03-21 19:34:44 +03:00
parent 3ffa3418c0
commit 82e2f550c7
4 changed files with 470 additions and 272 deletions

32
src/api/promocode.ts Normal file

@ -0,0 +1,32 @@
import { makeRequest } from "@frontend/kitui";
import { isAxiosError } from "axios";
const apiUrl = process.env.REACT_APP_DOMAIN + "/codeword/promocode";
export async function activatePromocode(promocode: string) {
try {
const response = await makeRequest<{
codeword: string;
} | {
fastLink: string;
}, {
greetings: string;
}>({
url: apiUrl + "/activate",
method: "POST",
body: {
codeword: promocode,
}
});
return response.greetings;
} catch (error) {
let message = "Неизвестная ошибка";
if (isAxiosError(error) && error.status === 404) {
message = "Промокод не найден";
}
throw new Error(message);
}
}

@ -0,0 +1,26 @@
import { Box, SxProps, Theme } from "@mui/material";
type Props = {
sx?: SxProps<Theme>;
};
export default function SimpleArrowDown({ sx }: Props) {
return (
<Box sx={{
width: "14px",
height: "9px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
...sx,
}}>
<svg width="14" height="9" viewBox="0 0 14 9" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 1.48535L7 7.48535" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d="M1 1.48535L7 7.48535" stroke="#7E2AEA" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</Box>
);
}

@ -1,10 +1,12 @@
import { import {
Box, Box,
Button, Button,
IconButton, Collapse,
Typography, Grow,
useMediaQuery, IconButton,
useTheme, Typography,
useMediaQuery,
useTheme,
} from "@mui/material"; } from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import SectionWrapper from "@components/SectionWrapper"; import SectionWrapper from "@components/SectionWrapper";
@ -28,314 +30,374 @@ import { useUserStore } from "@root/stores/user";
import { VerificationStatus } from "@root/model/account"; import { VerificationStatus } from "@root/model/account";
import { WarnModal } from "./WarnModal"; import { WarnModal } from "./WarnModal";
import { SorryModal } from "./SorryModal"; import { SorryModal } from "./SorryModal";
import SimpleArrowDown from "@root/components/icons/SimpleArrowDown";
import PromocodeTextField from "./PromocodeTextField";
import { activatePromocode } from "@root/api/promocode";
type PaymentMethod = { type PaymentMethod = {
label: string; label: string;
name: string; name: string;
image: string; image: string;
unpopular?: boolean; unpopular?: boolean;
}; };
const paymentMethods: PaymentMethod[] = [ const paymentMethods: PaymentMethod[] = [
{ label: "Тинькофф", name: "tinkoffBank", image: tinkoffLogo }, { label: "Тинькофф", name: "tinkoffBank", image: tinkoffLogo },
{ label: "СБП", name: "sbp", image: spbLogo }, { label: "СБП", name: "sbp", image: spbLogo },
{ label: "SberPay", name: "sberbank", image: sberpayLogo }, { label: "SberPay", name: "sberbank", image: sberpayLogo },
{ label: "B2B Сбербанк", name: "b2bSberbank", image: b2bLogo }, { label: "B2B Сбербанк", name: "b2bSberbank", image: b2bLogo },
{ label: "ЮMoney", name: "yoomoney", image: umoneyLogo }, { label: "ЮMoney", name: "yoomoney", image: umoneyLogo },
]; ];
type PaymentMethodType = (typeof paymentMethods)[number]["name"]; type PaymentMethodType = (typeof paymentMethods)[number]["name"];
export default function Payment() { export default function Payment() {
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const upSm = useMediaQuery(theme.breakpoints.up("sm")); const upSm = useMediaQuery(theme.breakpoints.up("sm"));
const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const [promocodeField, setPromocodeField] = useState<string>("");
const [isPromocodeFieldOpen, setIsPromocodeFieldOpen] = useState<boolean>(false);
const [selectedPaymentMethod, setSelectedPaymentMethod] =
useState<PaymentMethodType | null>("");
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
const [sorryModalOpen, setSorryModalOpen] = useState<boolean>(false);
const [paymentValueField, setPaymentValueField] = useState<string>("0");
const [paymentLink, setPaymentLink] = useState<string>("");
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false);
const location = useLocation();
const verificationStatus = useUserStore((state) => state.verificationStatus);
const navigate = useNavigate();
const handleCustomBackNavigation = useHistoryTracker();
const [selectedPaymentMethod, setSelectedPaymentMethod] = const notEnoughMoneyAmount =
useState<PaymentMethodType | null>(""); (location.state?.notEnoughMoneyAmount as number) ?? 0;
const [warnModalOpen, setWarnModalOpen] = useState<boolean>(false);
const [sorryModalOpen, setSorryModalOpen] = useState<boolean>(false);
const [paymentValueField, setPaymentValueField] = useState<string>("0");
const [paymentLink, setPaymentLink] = useState<string>("");
const [fromSquiz, setIsFromSquiz] = useState<boolean>(false);
const location = useLocation();
const verificationStatus = useUserStore((state) => state.verificationStatus);
const navigate = useNavigate();
const handleCustomBackNavigation = useHistoryTracker();
const notEnoughMoneyAmount = const paymentValue = parseFloat(paymentValueField) * 100;
(location.state?.notEnoughMoneyAmount as number) ?? 0;
const paymentValue = parseFloat(paymentValueField) * 100; useLayoutEffect(() => {
setPaymentValueField((notEnoughMoneyAmount / 100).toString());
const params = new URLSearchParams(window.location.search);
const fromSquiz = params.get("action");
if (fromSquiz === "squizpay") {
setIsFromSquiz(true);
setPaymentValueField((Number(params.get("dif") || "0") / 100).toString());
}
history.pushState(null, document.title, "/payment");
console.log(fromSquiz);
}, []);
useLayoutEffect(() => { async function handleChoosePaymentClick() {
setPaymentValueField((notEnoughMoneyAmount / 100).toString()); if (!selectedPaymentMethod) {
const params = new URLSearchParams(window.location.search); enqueueSnackbar("Введите метод оплаты");
const fromSquiz = params.get("action"); return;
if (fromSquiz === "squizpay") { }
setIsFromSquiz(true);
setPaymentValueField((Number(params.get("dif") || "0") / 100).toString());
}
history.pushState(null, document.title, "/payment");
console.log(fromSquiz);
}, []);
useEffect(() => { if (Number(paymentValueField) === 0) {
setPaymentLink(""); enqueueSnackbar("Введите сумму");
}, [selectedPaymentMethod]); return;
}
async function handleChoosePaymentClick() { if (selectedPaymentMethod !== "rspay") {
if (!selectedPaymentMethod) { const [sendPaymentResponse, sendPaymentError] = await sendPayment({
enqueueSnackbar("Введите метод оплаты") fromSquiz,
return; body: {
} type: selectedPaymentMethod,
amount: Number(paymentValueField) * 100,
},
});
if (Number(paymentValueField) === 0) { if (selectedPaymentMethod === "rspay") {
enqueueSnackbar("Введите сумму") if (verificationStatus !== VerificationStatus.VERIFICATED) {
return; setWarnModalOpen(true);
}
if (selectedPaymentMethod !== "rspay") { return;
const [sendPaymentResponse, sendPaymentError] = await sendPayment({ }
fromSquiz, console.log(paymentValueField);
body: { if (Number(paymentValueField) < 900) {
type: selectedPaymentMethod, enqueueSnackbar("Минимальная сумма 900р");
amount: Number(paymentValueField) * 100,
},
});
if (selectedPaymentMethod === "rspay") { return;
if (verificationStatus !== VerificationStatus.VERIFICATED) { }
setWarnModalOpen(true);
return; const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
}
console.log(paymentValueField)
if (Number(paymentValueField) < 900){
enqueueSnackbar("Минимальная сумма 900р")
return; if (sendRSPaymentError) {
} return enqueueSnackbar(sendRSPaymentError);
}
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField)); enqueueSnackbar(
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
);
if (sendRSPaymentError) { navigate("/settings");
return enqueueSnackbar(sendRSPaymentError); }
}
enqueueSnackbar( if (sendPaymentError) {
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." return enqueueSnackbar(sendPaymentError);
); }
navigate("/settings"); if (sendPaymentResponse) {
} setPaymentLink(sendPaymentResponse.link);
}
if (sendPaymentError) { return;
return enqueueSnackbar(sendPaymentError); } else {
} if (verificationStatus !== VerificationStatus.VERIFICATED) {
setWarnModalOpen(true);
if (sendPaymentResponse) { return;
setPaymentLink(sendPaymentResponse.link); }
} console.log(paymentValueField);
if (Number(paymentValueField) < 900) {
enqueueSnackbar("Минимальная сумма 900р");
return; return;
} else { }
if (verificationStatus !== VerificationStatus.VERIFICATED) {
setWarnModalOpen(true);
return; const sendRSPaymentError = await sendRSPayment(Number(paymentValueField));
}
console.log(paymentValueField)
if (Number(paymentValueField) < 900){
enqueueSnackbar("Минимальная сумма 900р")
return; if (sendRSPaymentError) {
} return enqueueSnackbar(sendRSPaymentError);
}
const sendRSPaymentError = await sendRSPayment(Number(paymentValueField)); enqueueSnackbar(
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
);
if (sendRSPaymentError) { navigate("/settings");
return enqueueSnackbar(sendRSPaymentError);
}
enqueueSnackbar( }
"Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг."
);
navigate("/settings");
} }
} function handleApplyPromocode() {
if (!promocodeField) return;
activatePromocode(promocodeField).then(response => {
enqueueSnackbar(response);
}).catch(error => {
enqueueSnackbar(error.message);
});
}
return ( return (
<SectionWrapper <SectionWrapper
maxWidth="lg" maxWidth="lg"
sx={{
mt: "25px",
mb: "70px",
px: isTablet ? (upMd ? "40px" : "18px") : "20px",
}}
>
<Box
sx={{
mt: "20px",
mb: "40px",
display: "flex",
gap: "10px",
}}
>
{!upMd && (
<IconButton
onClick={handleCustomBackNavigation}
sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
>
<ArrowBackIcon />
</IconButton>
)}
<Typography variant="h4">Способ оплаты</Typography>
</Box>
{!upMd && (
<Typography variant="body2" mb="30px">
Выберите способ оплаты
</Typography>
)}
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexDirection: upMd ? "row" : "column",
borderRadius: "12px",
boxShadow: upMd ? cardShadow : undefined,
}}
>
<Box
sx={{
width: upMd ? "68.5%" : undefined,
p: upMd ? "20px" : undefined,
display: "flex",
flexDirection: upSm ? "row" : "column",
flexWrap: "wrap",
gap: upMd ? "14px" : "20px",
alignContent: "start",
}}
>
{paymentMethods.map(({ name, label, image, unpopular = false }) => (
<PaymentMethodCard
isSelected={selectedPaymentMethod === name}
key={name}
label={label}
image={image}
onClick={() => {
setSelectedPaymentMethod(name)
}}
unpopular={false}
/>
))}
<PaymentMethodCard
isSelected={selectedPaymentMethod === "rspay"}
label={"Расчётный счёт"}
image={rsPayLogo}
onClick={async() => {
setSelectedPaymentMethod("rspay")
}}
unpopular={false}
/>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{ sx={{
display: "flex", mt: "25px",
flexDirection: "column", mb: "70px",
maxWidth: "85%", px: isTablet ? (upMd ? "40px" : "18px") : "20px",
}} }}
> >
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>} <Box
<Typography mb="20px">К оплате</Typography>
{paymentLink ? (
<Typography
sx={{ sx={{
fontWeight: 500, mt: "20px",
fontSize: "20px", mb: "40px",
lineHeight: "48px", display: "flex",
mb: "28px", gap: "10px",
}} }}
> >
{currencyFormatter.format(paymentValue / 100)} {!upMd && (
</Typography> <IconButton
) : ( onClick={handleCustomBackNavigation}
<InputTextfield sx={{ p: 0, height: "28px", width: "28px", color: "black" }}
TextfieldProps={{ >
placeholder: "К оплате", <ArrowBackIcon />
value: paymentValueField, </IconButton>
type: "number", )}
}} <Typography variant="h4">Способ оплаты</Typography>
onChange={(e) => setPaymentValueField(e.target.value.replace(/^0+(?=\d\.)/, ''))} </Box>
id="payment-amount" {!upMd && (
gap={upMd ? "16px" : "10px"} <Typography variant="body2" mb="30px">
color={"#F2F3F7"} Выберите способ оплаты
FormInputSx={{ mb: "28px" }} </Typography>
/>
)} )}
</Box> <Box
{paymentLink ? ( sx={{
<Button backgroundColor: upMd ? "white" : undefined,
variant="pena-outlined-light" display: "flex",
component="a" flexDirection: upMd ? "row" : "column",
href={paymentLink} borderRadius: "12px",
sx={{ boxShadow: upMd ? cardShadow : undefined,
mt: "auto", }}
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
backgroundColor: theme.palette.purple.dark,
border: `1px solid ${theme.palette.purple.dark}`,
},
}}
> >
Оплатить <Box
</Button> sx={{
) : ( width: upMd ? "68.5%" : undefined,
<Button p: upMd ? "20px" : undefined,
variant="pena-outlined-light" display: "flex",
disabled={!isFinite(paymentValue)} flexDirection: "column",
onClick={handleChoosePaymentClick} alignItems: "start",
sx={{ gap: "40px",
mt: "auto", }}
color: "black", >
border: `1px solid ${theme.palette.purple.main}`, <Box
"&:hover": { sx={{
color: "white", width: "100%",
}, display: "flex",
"&:active": { flexDirection: upSm ? "row" : "column",
color: "white", flexWrap: "wrap",
}, gap: upMd ? "14px" : "20px",
}} alignContent: "start",
> }}
Выбрать >
</Button> {paymentMethods.map(({ name, label, image, unpopular = false }) => (
)} <PaymentMethodCard
</Box> isSelected={selectedPaymentMethod === name}
</Box> key={name}
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} /> label={label}
<SorryModal open={sorryModalOpen} setOpen={setSorryModalOpen} /> image={image}
</SectionWrapper> onClick={() => {
); setSelectedPaymentMethod(name);
setPaymentLink("");
}}
unpopular={false}
/>
))}
<PaymentMethodCard
isSelected={selectedPaymentMethod === "rspay"}
label={"Расчётный счёт"}
image={rsPayLogo}
onClick={async () => {
setSelectedPaymentMethod("rspay");
setPaymentLink("");
}}
unpopular={false}
/>
</Box>
<Box sx={{
width: "100%",
display: "flex",
flexDirection: "column",
alignItems: "start",
}}>
<Button
variant="pena-text"
onClick={() => setIsPromocodeFieldOpen(p => !p)}
sx={{
fontWeight: 400,
fontSize: "18px",
lineHeight: "21.33px",
textUnderlineOffset: "1px",
".MuiButton-endIcon": {
mr: 0,
ml: "15px",
},
}}
endIcon={
<SimpleArrowDown sx={{
transform: isPromocodeFieldOpen ? "scaleY(-1)" : undefined,
transition: "transform 0.3s ease",
}} />
}
>У меня есть промокод</Button>
<Collapse
in={isPromocodeFieldOpen}
sx={{
width: "100%",
}}
>
<PromocodeTextField
value={promocodeField}
onChange={e => setPromocodeField(e.target.value)}
onApplyClick={handleApplyPromocode}
/>
</Collapse>
</Box>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
alignItems: "start",
color: theme.palette.gray.dark,
width: upMd ? "31.5%" : undefined,
p: upMd ? "20px" : undefined,
pl: upMd ? "33px" : undefined,
mt: upMd ? undefined : "30px",
borderLeft: upMd
? `1px solid ${theme.palette.gray.main}`
: undefined,
}}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
maxWidth: "85%",
}}
>
{upMd && <Typography mb="56px">Выберите способ оплаты</Typography>}
<Typography mb="20px">К оплате</Typography>
{paymentLink ? (
<Typography
sx={{
fontWeight: 500,
fontSize: "20px",
lineHeight: "48px",
mb: "28px",
}}
>
{currencyFormatter.format(paymentValue / 100)}
</Typography>
) : (
<InputTextfield
TextfieldProps={{
placeholder: "К оплате",
value: paymentValueField,
type: "number",
}}
onChange={(e) => setPaymentValueField(e.target.value.replace(/^0+(?=\d\.)/, ""))}
id="payment-amount"
gap={upMd ? "16px" : "10px"}
color={"#F2F3F7"}
FormInputSx={{ mb: "28px" }}
/>
)}
</Box>
{paymentLink ? (
<Button
variant="pena-outlined-light"
component="a"
href={paymentLink}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
backgroundColor: theme.palette.purple.dark,
border: `1px solid ${theme.palette.purple.dark}`,
},
}}
>
Оплатить
</Button>
) : (
<Button
variant="pena-outlined-light"
disabled={!isFinite(paymentValue)}
onClick={handleChoosePaymentClick}
sx={{
mt: "auto",
color: "black",
border: `1px solid ${theme.palette.purple.main}`,
"&:hover": {
color: "white",
},
"&:active": {
color: "white",
},
}}
>
Выбрать
</Button>
)}
</Box>
</Box>
<WarnModal open={warnModalOpen} setOpen={setWarnModalOpen} />
<SorryModal open={sorryModalOpen} setOpen={setSorryModalOpen} />
</SectionWrapper>
);
} }

@ -0,0 +1,78 @@
import { Box, Button, TextField, useMediaQuery, useTheme } from "@mui/material";
interface Props {
value?: string;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
onApplyClick?: () => void;
}
export default function PromocodeTextField({ value, onChange, onApplyClick }: Props) {
const theme = useTheme();
const upSm = useMediaQuery(theme.breakpoints.up("sm"));
return (
<Box
sx={{
width: upSm ? "407px" : "100%",
mt: "20px",
display: "flex",
flexDirection: upSm ? "row" : "column",
}}
>
<TextField
fullWidth
placeholder="Введите промокод"
value={value}
onChange={onChange}
sx={{
height: "48px",
"& .MuiInputBase-root": {
height: "100%",
borderRadius: "8px",
border: "1px solid #9A9AAF",
...(upSm ? {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
} : {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}),
},
}}
inputProps={{
sx: {
color: "#7E2AEA",
backgroundColor: "#7E2AEA1A",
height: "100%",
boxSizing: "border-box",
borderRadius: "8px",
py: 0,
...(upSm ? {
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
} : {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}),
},
}}
/>
<Button
variant="pena-contained-dark"
onClick={onApplyClick}
sx={{
px: "25px",
minWidth: 0,
...(upSm ? {
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
} : {
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
}),
}}
>Применить</Button>
</Box>
);
}