mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix: support method query param fallback for external browser redirects
When payment providers redirect to external browser, sessionStorage is unavailable. TopUpResult now reads method from URL query params and polls via /latest endpoint as fallback.
This commit is contained in:
@@ -114,6 +114,14 @@ export const balanceApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get latest pending payment by method (fallback when sessionStorage unavailable)
|
||||||
|
getLatestPayment: async (method: string): Promise<PendingPayment> => {
|
||||||
|
const response = await apiClient.get<PendingPayment>(
|
||||||
|
`/cabinet/balance/pending-payments/${encodeURIComponent(method)}/latest`,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
// Manually check payment status
|
// Manually check payment status
|
||||||
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
|
checkPaymentStatus: async (method: string, paymentId: number): Promise<ManualCheckResponse> => {
|
||||||
const response = await apiClient.post<ManualCheckResponse>(
|
const response = await apiClient.post<ManualCheckResponse>(
|
||||||
|
|||||||
@@ -226,6 +226,9 @@ export default function TopUpResult() {
|
|||||||
// Load saved payment info from sessionStorage (once on mount)
|
// Load saved payment info from sessionStorage (once on mount)
|
||||||
const [pendingInfo] = useState(() => loadTopUpPendingInfo());
|
const [pendingInfo] = useState(() => loadTopUpPendingInfo());
|
||||||
|
|
||||||
|
// Fallback: read method from query params (for external browser redirects where sessionStorage is unavailable)
|
||||||
|
const methodFromUrl = searchParams.get('method');
|
||||||
|
|
||||||
// Detect if user arrived via redirect with success param (no polling needed)
|
// Detect if user arrived via redirect with success param (no polling needed)
|
||||||
const redirectStatus = searchParams.get('status') || searchParams.get('payment');
|
const redirectStatus = searchParams.get('status') || searchParams.get('payment');
|
||||||
const isRedirectSuccess = redirectStatus
|
const isRedirectSuccess = redirectStatus
|
||||||
@@ -233,28 +236,30 @@ export default function TopUpResult() {
|
|||||||
: searchParams.get('success') === 'true';
|
: searchParams.get('success') === 'true';
|
||||||
const isRedirectFailed = redirectStatus ? isFailedStatus(redirectStatus) : false;
|
const isRedirectFailed = redirectStatus ? isFailedStatus(redirectStatus) : false;
|
||||||
|
|
||||||
// Determine if we can poll (need method + numeric payment_id)
|
// Determine if we can poll by specific payment_id (need method + numeric payment_id)
|
||||||
const parsedPaymentId = pendingInfo?.payment_id ? parseInt(pendingInfo.payment_id, 10) : NaN;
|
const parsedPaymentId = pendingInfo?.payment_id ? parseInt(pendingInfo.payment_id, 10) : NaN;
|
||||||
const canPoll =
|
const canPollById =
|
||||||
!!(pendingInfo?.method_id && !isNaN(parsedPaymentId)) &&
|
!!(pendingInfo?.method_id && !isNaN(parsedPaymentId)) &&
|
||||||
!isRedirectSuccess &&
|
!isRedirectSuccess &&
|
||||||
!isRedirectFailed;
|
!isRedirectFailed;
|
||||||
|
|
||||||
// Poll payment status
|
// Fallback: poll by method via /latest endpoint when no sessionStorage data
|
||||||
|
const canPollByMethod =
|
||||||
|
!canPollById && !!methodFromUrl && !isRedirectSuccess && !isRedirectFailed;
|
||||||
|
|
||||||
|
// Poll payment status by specific ID (primary path — sessionStorage available)
|
||||||
const { data: paymentStatus, refetch } = useQuery({
|
const { data: paymentStatus, refetch } = useQuery({
|
||||||
queryKey: ['topup-status', pendingInfo?.method_id, parsedPaymentId],
|
queryKey: ['topup-status', pendingInfo?.method_id, parsedPaymentId],
|
||||||
queryFn: () => balanceApi.getPendingPayment(pendingInfo!.method_id, parsedPaymentId),
|
queryFn: () => balanceApi.getPendingPayment(pendingInfo!.method_id, parsedPaymentId),
|
||||||
enabled: canPoll && !pollTimedOut,
|
enabled: canPollById && !pollTimedOut,
|
||||||
refetchInterval: (query) => {
|
refetchInterval: (query) => {
|
||||||
const payment = query.state.data;
|
const payment = query.state.data;
|
||||||
if (!payment) return POLL_INTERVAL_MS;
|
if (!payment) return POLL_INTERVAL_MS;
|
||||||
|
|
||||||
// Stop polling if paid or failed
|
|
||||||
if (payment.is_paid || isPaidStatus(payment.status) || isFailedStatus(payment.status)) {
|
if (payment.is_paid || isPaidStatus(payment.status) || isFailedStatus(payment.status)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check timeout
|
|
||||||
if (Date.now() - pollStart.current > MAX_POLL_MS) {
|
if (Date.now() - pollStart.current > MAX_POLL_MS) {
|
||||||
setPollTimedOut(true);
|
setPollTimedOut(true);
|
||||||
return false;
|
return false;
|
||||||
@@ -265,34 +270,64 @@ export default function TopUpResult() {
|
|||||||
retry: 2,
|
retry: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Poll payment status by method latest (fallback — external browser, no sessionStorage)
|
||||||
|
const { data: latestPayment, refetch: refetchLatest } = useQuery({
|
||||||
|
queryKey: ['topup-status-latest', methodFromUrl],
|
||||||
|
queryFn: () => balanceApi.getLatestPayment(methodFromUrl!),
|
||||||
|
enabled: canPollByMethod && !pollTimedOut,
|
||||||
|
refetchInterval: (query) => {
|
||||||
|
const payment = query.state.data;
|
||||||
|
if (!payment) return POLL_INTERVAL_MS;
|
||||||
|
|
||||||
|
if (payment.is_paid || isPaidStatus(payment.status) || isFailedStatus(payment.status)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() - pollStart.current > MAX_POLL_MS) {
|
||||||
|
setPollTimedOut(true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return POLL_INTERVAL_MS;
|
||||||
|
},
|
||||||
|
retry: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merge both polling sources
|
||||||
|
const effectivePayment = paymentStatus ?? latestPayment;
|
||||||
|
|
||||||
const handleRetryPoll = useCallback(() => {
|
const handleRetryPoll = useCallback(() => {
|
||||||
pollStart.current = Date.now();
|
pollStart.current = Date.now();
|
||||||
setPollTimedOut(false);
|
setPollTimedOut(false);
|
||||||
|
if (canPollById) {
|
||||||
refetch();
|
refetch();
|
||||||
}, [setPollTimedOut, refetch]);
|
} else {
|
||||||
|
refetchLatest();
|
||||||
|
}
|
||||||
|
}, [canPollById, setPollTimedOut, refetch, refetchLatest]);
|
||||||
|
|
||||||
const handleGoBack = useCallback(() => {
|
const handleGoBack = useCallback(() => {
|
||||||
clearTopUpPendingInfo();
|
clearTopUpPendingInfo();
|
||||||
navigate('/balance', { replace: true });
|
navigate('/balance', { replace: true });
|
||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
// Redirect to balance if no data at all
|
// Redirect to balance if absolutely no data source available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pendingInfo && !redirectStatus) {
|
if (!pendingInfo && !redirectStatus && !methodFromUrl) {
|
||||||
navigate('/balance', { replace: true });
|
navigate('/balance', { replace: true });
|
||||||
}
|
}
|
||||||
}, [pendingInfo, redirectStatus, navigate]);
|
}, [pendingInfo, redirectStatus, methodFromUrl, navigate]);
|
||||||
|
|
||||||
// Determine current visual state
|
// Determine current visual state
|
||||||
const amountKopeks = paymentStatus?.amount_kopeks ?? pendingInfo?.amount_kopeks ?? null;
|
const amountKopeks = effectivePayment?.amount_kopeks ?? pendingInfo?.amount_kopeks ?? null;
|
||||||
|
|
||||||
const resolvedPaid =
|
const resolvedPaid =
|
||||||
isRedirectSuccess ||
|
isRedirectSuccess ||
|
||||||
paymentStatus?.is_paid ||
|
effectivePayment?.is_paid ||
|
||||||
(paymentStatus && isPaidStatus(paymentStatus.status));
|
(effectivePayment && isPaidStatus(effectivePayment.status));
|
||||||
|
|
||||||
const resolvedFailed =
|
const resolvedFailed =
|
||||||
isRedirectFailed || (paymentStatus && isFailedStatus(paymentStatus.status));
|
isRedirectFailed || (effectivePayment && isFailedStatus(effectivePayment.status));
|
||||||
|
|
||||||
// Clean up sessionStorage and invalidate queries when payment resolves
|
// Clean up sessionStorage and invalidate queries when payment resolves
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user