0

9 min left

0% complete

Blog

Secure Account Deactivation in React Native: A Step-by-Step Guide

Audience: Developers building secure mobile apps

Β·9 min readΒ·
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:

  1. 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.

  1. 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:

  1. User navigates: Profile β†’ Edit Profile β†’ Deactivate Account
  2. App sends a secure POST to /api/v1/account/request-deletion with JWT in Authorization header
  3. Backend generates and returns a one-time code:

{ "deletionCode": "abc123xyz", "expiresAt": "2025-04-05T10:00:00Z" }

  1. App builds and opens a URL:

https://your-domain.com/en/account/delete?code=abc123xyz&returnTo=app://logout?reason=account_deleted

  1. User confirms deletion on the web page
  2. Backend deletes account and redirects to the deep link: app://logout?reason=account_deleted
  3. 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 Referer headers
  • 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.

FactorJWT in URLOne-Time Code
Logging exposureFull user takeoverHarmless
ReusabilityValid until expiryInvalid after one use
Attack windowHours or days5–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 guide

This 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:

  • deletionCode is from the API, not the JWT
  • returnTo uses your app’s deep link scheme (app://, myapp://) to return control
  • Always encodeURIComponent() query parameters
  • APP_SCHEME varies 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 scheme

2. 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:

  • useRef prevents multiple concurrent requests
  • deactivateCooldownUntilRef enforces rate limiting client-side (e.g., 60 seconds between attempts)
  • JWT is sent via Authorization headerβ€”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-browser

Import 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

βœ…MeasureWhy
βœ…JWT sent in Authorization headerPrevents logging/interception
βœ…One-time deletion codeSafe to pass in URL
βœ…5–15 minute expiryLimits attack window
βœ…Rate limiting on APIBlocks brute force
βœ…In-app browserNo system history leak
βœ…Backend validates codePrevents forged requests
βœ…Deep link clears tokensEnsures 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:

  1. βœ… Happy path: User deactivates β†’ web confirmation β†’ app logs out
  2. βœ… Rate limiting: Second tap within cooldown shows a message
  3. βœ… Network error: Toast shown, no crash
  4. βœ… 404/500 handling: Graceful fallback UI
  5. βœ… Role-based UI: Deactivate option hidden for special roles
  6. βœ… RTL support: Layout flips correctly in Arabic
  7. βœ… Cold start deep link: app://logout?reason=account_deleted works on app launch
  8. βœ… 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.”

πŸ“š Further Reading

Contents