feat: implement yookassa redirect payment flow on client
This commit is contained in:
@@ -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') ||
|
||||||
|
|||||||
Reference in New Issue
Block a user