9 min left
0% complete
Secure Account Deactivation in React Native: A Step-by-Step Guide
Audience: Developers building secure mobile apps
Audience: Developers building secure mobile apps
Difficulty: Intermediate
Time to read: 15β20 minutes
π‘ Why Secure Account Deletion Matters
Allowing users to delete their account is more than a featureβit's a privacy requirement. But poorly implemented account deletion can expose sensitive data, enable unauthorized access, or create security loopholes attackers love to exploit.
In this guide, youβll learn how to build a secure, user-friendly account deactivation flow in a React Native app. The pattern ensures that critical authentication tokens stay protected while still enabling seamless interaction between the mobile app and a web-based confirmation screen.
Youβll follow the full implementation: from UI design and API integration to deep linking and security hardeningβso you can implement it step by step in your own app.
π How It Works: The Two-Phase Deletion Flow
We use a two-phase authentication flow to securely bridge the mobile app and web:
- Phase 1 (Mobile App):
Request a one-time deletion code from your backend using the userβs JWT. The token is sent securely in HTTP headers, never in the URL.
- Phase 2 (Web Browser):
Open an in-app browser to a web confirmation page, passing only the short-lived, single-use deletion code in the URL.
βββββββββββββββ βββββββββββββββ βββββββββββββββ
β Mobile β ββJWTβββΆβ REST API β β Web Page β
β App βββcodeββ β Backend β β (Browser) β
β β βββββββββββββββ β β
β β ββββββββββββcodeββββββββββββββββΆβ β
β βββββββββdeep link redirectββββββ β β
βββββββββββββββ βββββββββββββββThis keeps your JWT secure, while allowing the web backend to verify the request and perform the deletion.
π Step-by-Step User Flow
Hereβs how a real user experiences the process:
- User navigates: Profile β Edit Profile β Deactivate Account
- App sends a secure
POSTto/api/v1/account/request-deletionwith JWT inAuthorizationheader - Backend generates and returns a one-time code:
{ "deletionCode": "abc123xyz", "expiresAt": "2025-04-05T10:00:00Z" }
- App builds and opens a URL:
https://your-domain.com/en/account/delete?code=abc123xyz&returnTo=app://logout?reason=account_deleted
- User confirms deletion on the web page
- Backend deletes account and redirects to the deep link:
app://logout?reason=account_deleted - Mobile app handles the deep link: clears session data and navigates to sign-in screen
Letβs walk through how each part is implemented.
π Why This Design? Security Over Convenience
β Never Pass JWTs in URLs
You might be tempted to do this:
// NEVER DO THIS
const url = `https://your-domain.com/delete?token=${jwt}`;But itβs a serious security risk. URLs with tokens can be:
- Logged in browser history
- Stored in server access logs
- Leaked via
Refererheaders - Visible on-screen (shoulder surfing)
Even transient exposure can lead to account takeover.
β One-Time Deletion Codes: The Safer Alternative
Instead, we generate short-lived, single-use codes that have no value after deletion.
| Factor | JWT in URL | One-Time Code |
|---|---|---|
| Logging exposure | Full user takeover | Harmless |
| Reusability | Valid until expiry | Invalid after one use |
| Attack window | Hours or days | 5β15 minutes |
This slight complexity (extra backend endpoint + code storage) is a small price for strong security.
π» Code Implementation
Letβs go hands-on. Hereβs the real code youβll write, structured in a typical React Native app.
ποΈ File Structure Overview
βββ app/profile/update.tsx # Profile screen with deactivation button
βββ shared/utils/constants.ts # URL builder for deletion flow
βββ shared/hooks/useDeepLinkHandler.ts # Handles deep link after deletion
βββ services/i18n/ # Language support
βββ (backend) account-deletion.md # Backend implementation guideThis structure separates concerns: UI, routing, security, and deep linking.
1. Secure URL Builder (constants.ts)
Generates the confirmation URL with the one-time code and deep link callback.
export function getDeleteAccountUrl(isArabic: boolean, deletionCode: string): string {
const lang = isArabic ? 'ar' : 'en';
const returnTo = `${APP_SCHEME}logout?reason=account_deleted`;
return `${BACKEND_WEB_URL}/${lang}/account/delete?code=${encodeURIComponent(deletionCode)}&returnTo=${encodeURIComponent(returnTo)}`;
}πΉ Key details:
deletionCodeis from the API, not the JWTreturnTouses your appβs deep link scheme (app://,myapp://) to return control- Always
encodeURIComponent()query parameters APP_SCHEMEvaries by environment:app://,app-dev://, etc.
Define these constants in your config:
// Example config
const BACKEND_WEB_URL = 'https://your-domain.com';
const APP_SCHEME = 'app'; // matches your app.json deep link scheme2. Request Deletion Code (update.tsx)
This is where the secure flow beginsβright from the UI.
const handleDeactivateAccount = async () => {
if (deactivateInFlightRef.current) return;
if (Date.now() < deactivateCooldownUntilRef.current) {
showToast({ type: 'info', title: 'Please wait before trying again.' });
return;
}
deactivateInFlightRef.current = true;
try {
const response = await api.post('/account/request-deletion');
const deletionCode = response.data?.deletionCode;
if (!deletionCode) {
throw new Error('No deletion code received');
}
const deleteUrl = getDeleteAccountUrl(isArabic, deletionCode);
await WebBrowser.openBrowserAsync(deleteUrl, {
presentationStyle: WebBrowser.WebBrowserPresentationStyle.PAGE_SHEET,
});
} catch (error) {
if (error.response?.status === 429) {
showToast({ type: 'error', title: 'Too many attempts. Try again later.' });
} else {
showToast({ type: 'error', title: 'Unable to deactivate account' });
}
} finally {
deactivateInFlightRef.current = false;
}
};πΉ Security & UX best practices:
useRefprevents multiple concurrent requestsdeactivateCooldownUntilRefenforces rate limiting client-side (e.g., 60 seconds between attempts)- JWT is sent via
Authorizationheaderβhandled automatically by Axios or similar WebBrowser.openBrowserAsync()opens an in-app browser, not the system browser, to avoid leaking the URL
β
Tip: Use Expoβs expo-web-browser to open pages in a secure, sandboxed in-app browser.Install it:
npx expo install expo-web-browserImport it:
import * as WebBrowser from 'expo-web-browser';3. Handle the Deep Link (useDeepLinkHandler.ts)
After deletion, the backend redirects to your app using a deep link:
app://logout?reason=account_deleted
This hook catches that and logs the user out.
function extractLogoutReason(url: string): string | null {
if (!url.includes('/logout') && !url.includes('://logout')) return null;
const reasonMatch = url.match(/[?&]reason=([^&]+)/);
return reasonMatch ? decodeURIComponent(reasonMatch[1]) : 'unknown';
}
const handleLogoutDeepLink = async (url: string): Promise<boolean> => {
const reason = extractLogoutReason(url);
if (!reason) return false;
console.log('[DeepLink] Logout deep link received, reason:', reason);
await signOut(); // Clears tokens from secure storage, resets auth context
router.replace('/(auth)/signIn');
return true;
};πΉ Why this matters:
- Prevents the user from staying "logged in" locally after deletion
signOut()should remove all tokens and user data from memory and storage- Deep links should work whether the app is open or starting fresh
β Pro tip: Register this handler in your app root or layout component.
useEffect(() => {
const handleDeepLink = (event) => {
const url = event.url;
handleLogoutDeepLink(url);
};
Linking.addEventListener('url', handleDeepLink);
return () => Linking.removeEventListener('url', handleDeepLink);
}, []);π¨ UX: Designing for Safety and Clarity
Account deactivation is irreversible. The UI should guideβnot trickβusers into this action.
πΊ Why the Button Is Hard to Reach
Location: At the very bottom of βEdit Profileβ
β Why it works:
- Prevents accidental taps
- Introduces intentional friction
- Matches iOS/Android patterns (e.g., βDelete Accountβ buried in settings)
π‘ Destructive (Red) Styling
<View className="border border-destructive/20 rounded-3xl p-4">
<View className="bg-destructive/10 p-3 rounded-full w-12 h-12 items-center justify-center">
<UserXIcon color="#ef4444" />
</View>
<Text className="text-destructive mt-2 text-center">Deactivate Account</Text>
</View>β Red signals danger across cultures. Users pause and read before proceeding.
π€ Conditional Access: Hiding for Special Roles
Some accounts (e.g., admin, influencer) shouldnβt be deletable via self-serve.
{!isSpecialRole && (
<TouchableOpacity onPress={handleDeactivateAccount}>
<Text className="text-destructive">Deactivate Account</Text>
</TouchableOpacity>
)}Handle those cases with manual or reviewed workflows.
π Security Checklist
| β | Measure | Why |
|---|---|---|
| β | JWT sent in Authorization header | Prevents logging/interception |
| β | One-time deletion code | Safe to pass in URL |
| β | 5β15 minute expiry | Limits attack window |
| β | Rate limiting on API | Blocks brute force |
| β | In-app browser | No system history leak |
| β | Backend validates code | Prevents forged requests |
| β | Deep link clears tokens | Ensures full logout |
π« Common Implementation Mistakes (And How to Avoid Them)
β Mistake 1: Passing JWT in Query Parameters
// BAD
const url = `https://your-domain.com/delete?token=${jwt}`;π Fix: Use one-time codes instead.
β Mistake 2: Not Clearing Local Tokens
// BAD - User still "logged in" after deletion
await WebBrowser.openBrowserAsync(deleteUrl);
// App state unchanged!π Fix: Ensure the deep link handler calls signOut().
β Mistake 3: Using System Browser
// Risky - URL appears in browser history
Linking.openURL(deleteUrl);π Fix: Use WebBrowser.openBrowserAsync() (Expo) or InAppBrowser (React Native).
βοΈ Testing the Flow
Validate your implementation with these test cases:
- β Happy path: User deactivates β web confirmation β app logs out
- β Rate limiting: Second tap within cooldown shows a message
- β Network error: Toast shown, no crash
- β 404/500 handling: Graceful fallback UI
- β Role-based UI: Deactivate option hidden for special roles
- β RTL support: Layout flips correctly in Arabic
- β
Cold start deep link:
app://logout?reason=account_deletedworks on app launch - β Warm start handling: Deep link caught while app is open
π§ Key Takeaways
- JWTs belong in headers, never URLs. Even short exposure can lead to breaches.
- Use one-time codes to securely pass authentication across domains.
- Always pair deletion with a deep link callback to ensure local logout.
- Design destructive actions with frictionβit prevents regrets.
- Test edge cases thoroughly, especially deep link handling and rate limits.
βSecure account deletion isnβt just about removing data. Itβs about ensuring no trace of access remainsβanywhere.β