eToro Plus (React Native) โ 2FA feature specification for the mobile app
src/features/2fa/
โโโ screens/
โ โโโ TwoFASettingsScreen.tsx # Main 2FA settings hub
โ โโโ TOTPSetupScreen.tsx # TOTP setup wizard (3 steps)
โ โโโ TOTPCodeInputScreen.tsx # 6-digit code entry (login)
โ โโโ WebAuthnSetupScreen.tsx # Passkey registration
โ โโโ WebAuthnPromptScreen.tsx # Passkey auth during login
โ โโโ BackupCodesScreen.tsx # Display & copy backup codes
โ โโโ BackupCodeInputScreen.tsx # Enter backup code (login)
โ โโโ RecoveryScreen.tsx # Account recovery form
โโโ components/
โ โโโ QRCodeDisplay.tsx # Show QR code for authenticator
โ โโโ QRScanner.tsx # Camera-based QR scanner
โ โโโ CodeInput.tsx # 6-digit OTP input with auto-focus
โ โโโ BackupCodeCard.tsx # Single backup code display
โ โโโ PasskeyCard.tsx # Registered passkey item
โ โโโ TwoFAMethodPicker.tsx # Choose TOTP vs Passkey
โ โโโ SecurityBanner.tsx # "2FA not enabled" warning
โ โโโ CountdownTimer.tsx # Rate limit countdown
โโโ hooks/
โ โโโ useTwoFA.ts # Main 2FA state hook
โ โโโ useTOTPSetup.ts # TOTP setup flow state
โ โโโ useWebAuthn.ts # WebAuthn registration/auth
โ โโโ useBackupCodes.ts # Backup code management
โโโ services/
โ โโโ twoFAApi.ts # API client for all 2FA endpoints
โ โโโ webauthnBridge.ts # @simplewebauthn/browser wrapper
โโโ context/
โ โโโ TwoFAContext.tsx # Global 2FA status provider
โโโ types/
โ โโโ twoFA.types.ts # TypeScript interfaces
โโโ utils/
โโโ otpauthUri.ts # Parse otpauth:// URIs
โโโ codeFormatter.ts # Format "123456" โ "123 456"
Main hub showing current 2FA status, enabled methods, registered passkeys, and backup code count. Entry point from Account โ Security.
// Key sections:
- 2FA Status badge (enabled/disabled)
- TOTP section (enable/disable toggle)
- Passkeys section (list + add new)
- Backup Codes (remaining count + regenerate)
- Recovery options
Reusable 6-digit OTP input with individual boxes, auto-advance, paste support, and haptic feedback on completion.
interface CodeInputProps {
length?: 6 | 8;
onComplete: (code: string) => void;
error?: string;
loading?: boolean;
autoFocus?: boolean;
}
Camera view for scanning authenticator QR codes. Uses react-native-camera. Fallback: manual secret entry.
// Parses otpauth:// URI from QR
// Auto-dismisses on successful scan
// Shows "Enter manually" link below
Shows registered passkey with device name, last used date, and delete option.
interface PasskeyCardProps {
credential: WebAuthnCredential;
onDelete: (id: string) => void;
onRename: (id: string, name: string) => void;
}
POST /2fa/totp/setupPOST /2fa/totp/verifyScan with your authenticator app
{ requires2FA: true, tempToken }POST /2fa/totp/validate with temp tokenPOST /2fa/webauthn/register/optionsstartRegistration() triggers OS biometric promptPOST /2fa/webauthn/register/verifyPOST /2fa/passkey/authenticate/optionsstartAuthentication() triggers biometric prompt (Face ID / fingerprint)POST /2fa/passkey/authenticate/verifyOn web: the login page uses mediation: "conditional" to show passkey suggestions in the browser's autofill dropdown โ no button click needed.
POST /passkey/authenticate/options with empty body.startAuthentication({mediation: "conditional"}) โ browser shows passkey in username autofill: "Sign in as yoni@etoro.com ๐"โ Browser autofill shows passkey suggestion
When the user is on a device without a registered passkey (e.g., a shared computer), they can use their phone as an authenticator via QR + Bluetooth.
Settings โ Security โ Passkeys. Shows all registered passkeys with device info, sync status, and management options.
GET /2fa/passkey/credentialsAfter successful SMS or TOTP login, show a bottom sheet prompting upgrade to Passkey.
Use Face ID or fingerprint instead of typing codes. It's also more secure โ immune to phishing attacks.
When passkey authentication fails (device issue, cancelled, etc.), graceful fallback to alternative methods.
POST /2fa/backup-codes/verifyDELETE /2fa/totp called โ 2FA disabled, secrets purgedinterface TwoFAState {
isEnabled: boolean;
primaryMethod: 'totp' | 'webauthn' | null;
totp: {
enabled: boolean;
configuredAt: string | null;
};
webauthn: {
enabled: boolean;
credentials: WebAuthnCredential[];
};
backupCodes: {
remaining: number;
generatedAt: string | null;
};
loading: boolean;
error: string | null;
}
// Actions
type TwoFAAction =
| { type: 'SET_STATUS'; payload: TwoFAStatus }
| { type: 'TOTP_ENABLED' }
| { type: 'TOTP_DISABLED' }
| { type: 'PASSKEY_ADDED'; payload: WebAuthnCredential }
| { type: 'PASSKEY_REMOVED'; payload: string }
| { type: 'BACKUP_CODES_REGENERATED'; payload: number }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null };
// Provider wraps entire app โ populated on login
// Refreshed via GET /2fa/status on each settings screen visit
interface LoginState {
step: 'credentials' | '2fa_required' | '2fa_input' | 'authenticated';
tempToken: string | null;
requires2FA: boolean;
availableMethods: ('totp' | 'webauthn' | 'backup')[];
selectedMethod: string | null;
attemptsRemaining: number;
lockoutUntil: number | null; // Unix timestamp
}
otpauth://totp/eToro:user@email.com?secret=JBSWY3DPEHPK3PXP&issuer=eToro&algorithm=SHA1&digits=6&period=30
The app registers as a handler for otpauth:// URIs. When a user taps the URI (e.g., from an email), the app opens directly to the TOTP verification step.
| URI | Screen |
|---|---|
etoroplus://settings/2fa | TwoFASettingsScreen |
etoroplus://settings/2fa/setup-totp | TOTPSetupScreen |
etoroplus://settings/2fa/add-passkey | WebAuthnSetupScreen |
etoroplus://settings/2fa/backup-codes | BackupCodesScreen |
etoroplus://2fa/recovery?token=xxx | RecoveryScreen |
ASAuthorizationPlatformPublicKeyCredentialProviderNSFaceIDUsageDescription in Info.plistandroid.permission.USE_BIOMETRICimport * as Keychain from 'react-native-keychain';
// Store 2FA preference securely
await Keychain.setGenericPassword(
'twofa_preference',
JSON.stringify({ method: 'webauthn', skipUntil: Date.now() + 30 * 86400000 }),
{ service: 'com.etoro.plus.2fa', accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY }
);
// Check if biometric is available
const biometryType = await Keychain.getSupportedBiometryType();
// Returns: 'TouchID' | 'FaceID' | 'Fingerprint' | 'Face' | null
| Package | Version | Purpose | Platform |
|---|---|---|---|
@simplewebauthn/browser | ^10.0 | WebAuthn client operations | Both |
react-native-camera | ^4.2 | QR code scanning | Both |
react-native-keychain | ^8.2 | Secure storage + biometric access | Both |
react-native-qrcode-svg | ^6.3 | Render QR code for TOTP URI | Both |
react-native-clipboard | ^1.14 | Copy secret / backup codes | Both |
react-native-haptic-feedback | ^2.2 | Haptics on code entry / success | Both |
| Error | UI Response |
|---|---|
| Invalid code | Shake animation + red border + haptic |
| Rate limited | Countdown timer, inputs disabled |
| Account locked | Full-screen lockout message + support link |
| Network error | Retry button + offline indicator |
| Biometric failed | "Try again" + fallback to TOTP |
| No backup codes left | Red warning + "Contact Support" |