| name | stripe-external-redirect-navigation |
| description | 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.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-01-27T00:00:00.000Z" |
Stripe External Redirect Navigation Fix
Problem
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.
Context / Trigger Conditions
- User completes Stripe checkout and returns to your app
- Back button uses
router.back() or browser history navigation
document.referrer check for external domains returns empty or doesn't work
- Works on staging/production but staging uses a simple
<Link> instead of history-based back
Why document.referrer doesn't work:
Stripe and many payment providers set strict Referrer-Policy headers (e.g.,
strict-origin-when-cross-origin or no-referrer) that prevent the referrer from
being passed back to your app. The referrer may be empty or just the origin without path.
Solution
Use sessionStorage to explicitly track when returning from an external redirect:
1. Create a utility for tracking external returns
const EXTERNAL_RETURN_KEY = 'external_payment_return';
export function markExternalReturn() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem(EXTERNAL_RETURN_KEY, 'true');
}
}
export function clearExternalReturn() {
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem(EXTERNAL_RETURN_KEY);
}
}
export function hasExternalReturn(): boolean {
if (typeof sessionStorage === 'undefined') return false;
return sessionStorage.getItem(EXTERNAL_RETURN_KEY) === 'true';
}
2. Mark external return when handling payment callback
function PaymentReturnHandler() {
const searchParams = useSearchParams();
useEffect(() => {
const payment = searchParams.get("payment");
if (payment === "success" || payment === "cancelled") {
markExternalReturn();
}
}, [searchParams]);
}
3. Check flag in BackButton before using history
function BackButton({ fallback = '/app' }) {
const router = useRouter();
const handleBack = () => {
const markedExternal = hasExternalReturn();
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();
router.push(fallback);
} else {
router.back();
}
};
return <button onClick={handleBack}>Back</button>;
}
Verification
- Go to your payment page (e.g.,
/settings)
- Click "Purchase" to go to Stripe checkout
- Complete payment (use test card 4242 4242 4242 4242)
- After redirect back to your app, click the back button
- Expected: Navigate to your app's home/fallback page (e.g.,
/app)
- Before fix: Would navigate back to Stripe checkout
Example
Full implementation in a Next.js settings page with Stripe:
'use client';
import { useRouter } from 'next/navigation';
const EXTERNAL_RETURN_KEY = 'external_payment_return';
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>
);
}
function PaymentReturnHandler() {
const searchParams = useSearchParams();
useEffect(() => {
if (searchParams.get("payment") === "success") {
BackButton.markExternalReturn();
router.replace("/settings");
}
}, [searchParams]);
}
export default function SettingsPage() {
return (
<>
<PaymentReturnHandler />
<BackButton fallback="/app" />
{/* ... */}
</>
);
}
Notes
-
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.
References