feat(driver-pwa): payment screen + auto status polling
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getPaymentState } from "@/api/driver";
|
||||
|
||||
export function usePaymentStatus(orderId: string) {
|
||||
return useQuery({
|
||||
queryKey: ["payment", orderId],
|
||||
queryFn: () => getPaymentState(orderId),
|
||||
refetchInterval: (q) => (q.state.data?.status === "PAID" || q.state.data?.status === "REJECTED" ? false : 1500),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MemoryRouter, Routes, Route } from "react-router-dom";
|
||||
|
||||
const states = ["FORM", "PAID"];
|
||||
vi.mock("@/api/driver", () => ({
|
||||
getPaymentState: vi.fn(async () => ({ order_id: "o9", status: states.shift() ?? "PAID" })),
|
||||
}));
|
||||
import { PaymentPage } from "./PaymentPage";
|
||||
|
||||
function wrap() {
|
||||
const qc = new QueryClient();
|
||||
return render(
|
||||
<QueryClientProvider client={qc}>
|
||||
<MemoryRouter initialEntries={[{ pathname: "/pay/o9", state: { payUrl: "https://qr.nspk.ru/x", total: 1052, bucket: "Долг аренда", base: 1000 } }]}>
|
||||
<Routes><Route path="/pay/:orderId" element={<PaymentPage />} /></Routes>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe("PaymentPage", () => {
|
||||
it("shows pay button and flips to success on PAID", async () => {
|
||||
wrap();
|
||||
expect(screen.getByRole("link", { name: /Оплатить/i })).toHaveAttribute("href", "https://qr.nspk.ru/x");
|
||||
await waitFor(() => expect(screen.getByText(/Оплачено/i)).toBeInTheDocument(), { timeout: 3000 });
|
||||
});
|
||||
});
|
||||
@@ -1 +1,42 @@
|
||||
export function PaymentPage() { return null; }
|
||||
import { useEffect } from "react";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Check } from "lucide-react";
|
||||
import { formatMoney } from "@/lib/format";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { usePaymentStatus } from "@/hooks/usePaymentStatus";
|
||||
|
||||
export function PaymentPage() {
|
||||
const { orderId = "" } = useParams();
|
||||
const nav = useNavigate();
|
||||
const qc = useQueryClient();
|
||||
const st = (useLocation().state as { payUrl?: string; total?: number; bucket?: string; base?: number } | null) ?? {};
|
||||
const { data } = usePaymentStatus(orderId);
|
||||
const paid = data?.status === "PAID";
|
||||
|
||||
useEffect(() => {
|
||||
if (paid) qc.invalidateQueries({ queryKey: ["balance"] });
|
||||
}, [paid, qc]);
|
||||
|
||||
if (paid) {
|
||||
return (
|
||||
<div className="pt-24 flex flex-col items-center text-center">
|
||||
<span className="w-14 h-14 rounded-full bg-pos text-white flex items-center justify-center mb-3"><Check size={28} /></span>
|
||||
<p className="text-lg font-extrabold">Оплачено</p>
|
||||
<p className="text-muted text-xs mt-1">{st.bucket} пополнен на {formatMoney(st.base ?? 0)}</p>
|
||||
<button className="btn-primary mt-8" onClick={() => nav("/")}>На главную</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-10 flex flex-col items-center text-center">
|
||||
<span className="w-12 h-12 rounded-xl2 bg-ink text-cream flex items-center justify-center font-extrabold mb-4">СБП</span>
|
||||
<p className="text-lg font-extrabold">{formatMoney(st.total ?? 0)}</p>
|
||||
<p className="text-muted text-xs mb-6">{st.bucket}</p>
|
||||
<a className="btn-primary no-underline" href={st.payUrl} target="_blank" rel="noopener">Оплатить</a>
|
||||
<p className="text-muted text-xs mt-2">Откроется ваше приложение банка</p>
|
||||
<div className="flex items-center gap-2 mt-6 text-muted text-xs"><Spinner /> Ожидаем оплату…</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user