Fix back button navigating to Stripe checkout after payment completion. Use when: (1) User completes Stripe payment and clicks back button, but returns to Stripe instead of the app, (2) document.referrer check for external sites doesn't work with Stripe, (3) router.back() goes to payment provider instead of previous app page. Covers Next.js, React Router, and any SPA with external payment redirects. Solution uses sessionStorage to track external returns reliably.
After returning from Stripe checkout (or similar external payment provider), clicking
a "back" button that uses browser history (router.back(), history.back()) navigates
back to Stripe instead of the previous app page. The browser history includes:
/settings → Stripe checkout → /settings?payment=success
So router.back() goes to Stripe.
router.back() or browser history navigationdocument.referrer check for external domains returns empty or doesn't work<Link> instead of history-based backWhy document.referrer doesn't work:
Stripe and many payment providers set strict Referrer-Policy headers (e.g.,
or ) that prevent the referrer from
being passed back to your app. The referrer may be empty or just the origin without path.
strict-origin-when-cross-originno-referrerUse sessionStorage to explicitly track when returning from an external redirect:
// Session storage key for tracking external payment returns
const EXTERNAL_RETURN_KEY = 'external_payment_return';
/**
* Mark that the user just returned from an external site (e.g., Stripe).
* Call this when handling payment returns or other external redirects.
*/
export function markExternalReturn() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(EXTERNAL_RETURN_KEY, 'true');
}
}
/**
* Clear the external return flag. Call after handling.
*/
export function clearExternalReturn() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem(EXTERNAL_RETURN_KEY);
}
}
/**
* Check if the user just returned from an external site.
*/
export function hasExternalReturn(): boolean {
if (typeof sessionStorage === 'undefined') return false;
return sessionStorage.getItem(EXTERNAL_RETURN_KEY) === 'true';
}
// In your payment return handler (e.g., /settings?payment=success)
function PaymentReturnHandler() {
const searchParams = useSearchParams();
useEffect(() => {
const payment = searchParams.get("payment");
if (payment === "success" || payment === "cancelled") {
// Mark that we returned from external payment
markExternalReturn();
// ... handle payment verification
}
}, [searchParams]);
}
function BackButton({ fallback = '/app' }) {
const router = useRouter();
const handleBack = () => {
// Check if we explicitly marked an external return (most reliable)
const markedExternal = hasExternalReturn();
// Fallback checks (less reliable but catch edge cases)
const referrer = typeof document !== 'undefined' ? document.referrer : '';
const host = typeof window !== 'undefined' ? window.location.host : '';
const isExternalReferrer = referrer && !referrer.includes(host);
const historyTooShort = typeof window !== 'undefined' && window.history.length <= 2;
if (markedExternal || isExternalReferrer || historyTooShort) {
clearExternalReturn(); // Clear flag after use
router.push(fallback);
} else {
router.back();
}
};
return <button onClick={handleBack}>Back</button>;
}
/settings)/app)Full implementation in a Next.js settings page with Stripe:
// components/ui/BackButton.tsx
'use client';
import { useRouter } from 'next/navigation';
const EXTERNAL_RETURN_KEY = 'external_payment_return';
// Static method pattern allows: BackButton.markExternalReturn()
BackButton.markExternalReturn = function() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(EXTERNAL_RETURN_KEY, 'true');
}
};
export function BackButton({ fallback = '/app' }) {
const router = useRouter();
const handleBack = () => {
const hasExternal = sessionStorage?.getItem(EXTERNAL_RETURN_KEY) === 'true';
if (hasExternal) {
sessionStorage.removeItem(EXTERNAL_RETURN_KEY);
router.push(fallback);
} else {
router.back();
}
};
return (
<button onClick={handleBack} aria-label="Go back">
← Back
</button>
);
}
// app/settings/page.tsx
function PaymentReturnHandler() {
const searchParams = useSearchParams();
useEffect(() => {
if (searchParams.get("payment") === "success") {
BackButton.markExternalReturn(); // Mark before clearing URL
// Handle payment verification...
router.replace("/settings"); // Clean URL
}
}, [searchParams]);
}
export default function SettingsPage() {
return (
<>
<PaymentReturnHandler />
<BackButton fallback="/app" />
{/* ... */}
</>
);
}
sessionStorage vs localStorage: Use sessionStorage because it's tab-specific. If user opens multiple tabs, each has its own payment flow state.
Security: This flag only affects UX navigation, not payment security. Payment verification must still happen server-side with Stripe session validation.
Flag cleanup: Always clear the flag after use to prevent it from affecting future navigation in the same session.
Multiple fallback checks: Keep the document.referrer and history.length
checks as fallbacks for edge cases, but don't rely on them for Stripe.
Why not just use Link?: Using a simple <Link href="/"> works but loses the
"back" behavior for internal navigation. The sessionStorage approach gives you
the best of both: proper back navigation internally, fallback for external returns.