feat: implement yookassa redirect payment flow on client

This commit is contained in:
Kirill
2026-05-20 19:28:46 +05:00
parent 698293e2f1
commit faac332138
3 changed files with 64 additions and 31 deletions
@@ -69,6 +69,18 @@ export async function fetchMyOrder(id: string): Promise<OrderDetailResponse> {
return data return data
} }
/** Создать платёж в ЮKassa и получить URL для редиректа на форму оплаты. */
export async function createOrderPayment(orderId: string): Promise<{ confirmationUrl: string }> {
const { data } = await apiClient.post<{ confirmationUrl: string }>(`me/orders/${orderId}/pay`)
return data
}
/** Получить статус платежа для заказа. */
export async function getOrderPaymentStatus(orderId: string): Promise<{ status: string | null; paid: boolean }> {
const { data } = await apiClient.get<{ status: string | null; paid: boolean }>(`me/orders/${orderId}/payment`)
return data
}
export async function postOrderMessage(id: string, text: string): Promise<void> { export async function postOrderMessage(id: string, text: string): Promise<void> {
await apiClient.post(`me/orders/${id}/messages`, { text }) await apiClient.post(`me/orders/${id}/messages`, { text })
} }
@@ -1,9 +1,7 @@
import { useState } from 'react'
import Box from '@mui/material/Box' import Box from '@mui/material/Box'
import Button from '@mui/material/Button' import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels' import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
import { PaymentDialog } from './PaymentDialog'
type Props = { type Props = {
status: string status: string
@@ -12,7 +10,7 @@ type Props = {
totalCents: number totalCents: number
isPayPending: boolean isPayPending: boolean
payError: unknown payError: unknown
onPay: (params: { detail: string; receiptFile: File | null }) => void onPay: () => void
} }
export function OrderPaymentSection({ export function OrderPaymentSection({
@@ -24,7 +22,6 @@ export function OrderPaymentSection({
onPay, onPay,
}: Props) { }: Props) {
const payOnPickup = (paymentMethod ?? 'online') === 'on_pickup' const payOnPickup = (paymentMethod ?? 'online') === 'on_pickup'
const [payModalOpen, setPayModalOpen] = useState(false)
if (payOnPickup) { if (payOnPickup) {
return ( return (
@@ -52,30 +49,24 @@ export function OrderPaymentSection({
{status === 'PENDING_PAYMENT' && deliveryFeeLocked === true && ( {status === 'PENDING_PAYMENT' && deliveryFeeLocked === true && (
<> <>
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}> <Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
После перевода подтвердите оплату откроется форма для комментария и фото чека. Заказ получит статус « Вы будете перенаправлены на защищённую платёжную страницу ЮKassa. После оплаты заказ получит статус «
{orderStatusLabelRu('PAID')}». {orderStatusLabelRu('PAID')}».
</Typography> </Typography>
<Button variant="contained" onClick={() => setPayModalOpen(true)}> <Button variant="contained" onClick={onPay} disabled={isPayPending}>
Оплатить {isPayPending ? 'Создание платежа…' : 'Оплатить'}
</Button> </Button>
</> </>
)} )}
{status !== 'PENDING_PAYMENT' && ( {status === 'PAID' && (
<Typography color="text.secondary" variant="body2"> <Typography color="success.main" variant="body1">
На этом этапе действий по оплате в этом блоке не требуется. Оплачено. Спасибо!
</Typography>
)}
{status !== 'PENDING_PAYMENT' && status !== 'PAID' && (
<Typography color="text.secondary" variant="body2">
На этом этапе действий по оплате не требуется.
</Typography> </Typography>
)} )}
<PaymentDialog
open={payModalOpen}
isPending={isPayPending}
error={payError}
onClose={() => setPayModalOpen(false)}
onSubmit={(params) => {
onPay(params)
setPayModalOpen(false)
}}
/>
</Box> </Box>
) )
} }
@@ -7,12 +7,13 @@ import Link from '@mui/material/Link'
import Stack from '@mui/material/Stack' import Stack from '@mui/material/Stack'
import Typography from '@mui/material/Typography' import Typography from '@mui/material/Typography'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Link as RouterLink, useParams } from 'react-router-dom' import { Link as RouterLink, useParams, useSearchParams } from 'react-router-dom'
import { import {
confirmOrderReceived, confirmOrderReceived,
createOrderPayment,
fetchMyOrder, fetchMyOrder,
getOrderPaymentStatus,
postOrderMessage, postOrderMessage,
submitOrderPayment,
fetchOrderReviewEligibility, fetchOrderReviewEligibility,
} from '@/entities/order/api/order-api' } from '@/entities/order/api/order-api'
import { postProductReview, uploadReviewImage } from '@/entities/review/api/reviews-api' import { postProductReview, uploadReviewImage } from '@/entities/review/api/reviews-api'
@@ -30,6 +31,9 @@ export function OrderDetailPage() {
const { id } = useParams() const { id } = useParams()
const qc = useQueryClient() const qc = useQueryClient()
const [searchParams] = useSearchParams()
const paidParam = searchParams.get('paid')
const orderQuery = useQuery({ const orderQuery = useQuery({
queryKey: ['me', 'orders', id], queryKey: ['me', 'orders', id],
queryFn: () => fetchMyOrder(id!), queryFn: () => fetchMyOrder(id!),
@@ -37,13 +41,20 @@ export function OrderDetailPage() {
}) })
const payMut = useMutation({ const payMut = useMutation({
mutationFn: (params: { detail: string; receiptFile: File | null }) => submitOrderPayment(id!, params), mutationFn: () => createOrderPayment(id!),
onSuccess: async () => { onSuccess: async (data) => {
await Promise.all([ window.location.href = data.confirmationUrl
qc.invalidateQueries({ queryKey: ['me', 'orders', id] }), },
qc.invalidateQueries({ queryKey: ['me', 'orders'] }), })
qc.invalidateQueries({ queryKey: ['me', 'conversations'] }),
]) const paymentStatusQuery = useQuery({
queryKey: ['me', 'orders', id, 'payment-status'],
queryFn: () => getOrderPaymentStatus(id!),
enabled: Boolean(id && paidParam === '1'),
refetchInterval: (query) => {
const data = query.state.data
if (data && (data.paid || data.status === 'canceled')) return false
return 3000
}, },
}) })
@@ -117,6 +128,25 @@ export function OrderDetailPage() {
</Button> </Button>
</Stack> </Stack>
{paidParam === '1' && paymentStatusQuery.data && (
<Alert
severity={
paymentStatusQuery.data.paid
? 'success'
: paymentStatusQuery.data.status === 'canceled'
? 'warning'
: 'info'
}
sx={{ mb: 2 }}
>
{paymentStatusQuery.data.paid
? 'Оплата прошла успешно!'
: paymentStatusQuery.data.status === 'canceled'
? 'Оплата отмена. Вы можете попробовать снова.'
: 'Ожидаем подтверждения оплаты…'}
</Alert>
)}
<Stack spacing={2} sx={{ maxWidth: 900 }}> <Stack spacing={2} sx={{ maxWidth: 900 }}>
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}> <Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
@@ -212,7 +242,7 @@ export function OrderDetailPage() {
totalCents={order.totalCents} totalCents={order.totalCents}
isPayPending={payMut.isPending} isPayPending={payMut.isPending}
payError={payMut.error} payError={payMut.error}
onPay={(params) => payMut.mutate(params)} onPay={() => payMut.mutate()}
/> />
{(order.deliveryType === 'delivery' && order.status === 'SHIPPED') || {(order.deliveryType === 'delivery' && order.status === 'SHIPPED') ||