Sovran supports optional passcode protection using a 4-digit PIN code. When enabled, the passcode must be entered each time you open the app.
Overview
The passcode is never persisted to disk . It only exists in memory during your session and protects against unauthorized physical access to your unlocked device.
How It Works
PasscodeGate component wraps the app
On launch, checks if passcode is set in settings store (memory-only)
If set, displays PasscodeScreen until correct PIN is entered
Once unlocked, app content is rendered
PasscodeGate Component (components/blocks/passcode/PasscodeGate.tsx:5-18)
Settings Store (stores/settingsStore.ts:164-167)
const PasscodeGate : React . FC <{ children : React . ReactNode }> = ({ children }) => {
const passcode = useSettingsStore (( state ) => state . passcode );
const [ unlocked , setUnlocked ] = useState ( ! passcode );
useEffect (() => {
if ( ! passcode ) setUnlocked ( true );
}, [ passcode ]);
if ( passcode && ! unlocked ) {
return < PasscodeScreen passcode = { passcode } onSuccess = { () => setUnlocked ( true ) } /> ;
}
return <> { children } </> ;
};
Setting Up a Passcode
Navigate to Settings
Open the app and tap Settings → Security → Set Passcode .
Enter Your PIN
Enter a 4-digit PIN code using the numeric keyboard.
Confirm Your PIN
Re-enter the same 4-digit code to confirm.
Passcode Active
Your passcode is now active and will be required on next app launch.
Forgetting your passcode will prevent you from accessing your wallet. The only recovery method is to uninstall and reinstall the app, then restore from your seed phrase.
Passcode Settings Screen
The passcode configuration flow has two steps:
Enter new passcode
Numeric keyboard with digits 0-9
4 dots showing entry progress
Automatically advances to confirmation when 4 digits entered
Create Step (app/settings-pages/passcode.tsx:24-38)
const handlePress = ( val : string ) => {
if ( step === 'create' ) {
if ( val . length <= PASSCODE_LENGTH ) {
setCode ( val );
if ( val . length === PASSCODE_LENGTH ) {
setStep ( 'confirm' );
setKeyIdx (( k ) => k + 1 );
}
}
} else {
if ( val . length <= PASSCODE_LENGTH ) {
setConfirm ( val );
}
}
};
Confirm passcode
Re-enter the same 4 digits
Must match the code from step 1
Shows error popup if codes don’t match
Confirm Logic (app/settings-pages/passcode.tsx:86-93)
onPress : async () => {
if ( code === confirm && code . length === PASSCODE_LENGTH ) {
setPasscode ( code );
router . back ();
} else {
passcodeNotMatchPopup ();
}
}
Passcode Entry Screen
When the app is locked, users see:
Features:
User avatar and welcome message
4-dot progress indicator
Numeric keyboard (0-9 + backspace)
Shake animation on incorrect entry
Fade-out animation on success
PasscodeScreen Component (components/blocks/passcode/PasscodeScreen.tsx:43-96)
const PasscodeScreen : React . FC < Props > = ({ passcode , onSuccess }) => {
const { keys : nostrKeys } = useNostrKeysContext ();
const [ value , setValue ] = useState ( '' );
const [ keyIdx , setKeyIdx ] = useState ( 0 );
const opacity = useRef ( new Animated . Value ( 1 )). current ;
const shake = useRef ( new Animated . Value ( 0 )). current ;
const handlePress = ( val : string ) => {
if ( val . length > passcode . length ) return ;
setValue ( val );
if ( val . length === passcode . length ) {
if ( val === passcode ) {
// Success: fade out and unlock
Animated . timing ( opacity , {
toValue: 0 ,
duration: 300 ,
useNativeDriver: true ,
}). start (() => onSuccess ());
} else {
// Failure: shake animation and reset
Animated . sequence ([
Animated . timing ( shake , { toValue: - 10 , duration: 50 , useNativeDriver: true }),
Animated . timing ( shake , { toValue: 10 , duration: 50 , useNativeDriver: true }),
Animated . timing ( shake , { toValue: - 10 , duration: 50 , useNativeDriver: true }),
Animated . timing ( shake , { toValue: 0 , duration: 50 , useNativeDriver: true }),
]). start ();
setTimeout (() => {
setValue ( '' );
setKeyIdx (( k ) => k + 1 );
}, 200 );
}
}
};
return (
< BlurView className = "bg-background flex-1" >
< Animated.View style = { { opacity , transform: [{ translateX: shake }] } } >
< VStack align = "center" justify = "center" flex = { 1 } >
< Avatar seed = { nostrKeys ?. pubkey } size = { 80 } />
< Text > Welcome back, { getUsername ( nostrKeys ?. pubkey || '' ) } </ Text >
{ /* Dot indicators */ }
< HStack >
{ Array . from ({ length: passcode . length }). map (( _ , i ) => (
< View
key = { i }
className = { `mx-1.5 h-3 w-3 rounded-full ${
value . length > i ? 'bg-foreground' : 'border-foreground border'
} ` }
/>
)) }
</ HStack >
< NumericKeyboard onKeyPress = { handlePress } />
</ VStack >
</ Animated.View >
</ BlurView >
);
};
Numeric Keyboard
The custom numeric keyboard provides:
Digits 0-9 : Standard numeric input
Backspace (⌫) : Delete last digit
Haptic feedback : Button press and action haptics
Ripple animations : Visual feedback on tap
NumericKeyboard Component (components/blocks/passcode/NumericKeyboard.tsx:64-105)
const NumericKeyboard : React . FC < Props > = ({ onKeyPress }) => {
const inputRef = useRef ( '' );
const handlePress = useCallback (
( value : KeyVal ) => {
let newValue : string ;
if ( String ( value ) === '<' ) {
EnhancedHaptics . actionHaptic ();
newValue = inputRef . current . slice ( 0 , - 1 );
} else {
EnhancedHaptics . buttonHaptic ();
newValue = inputRef . current + String ( value );
}
inputRef . current = newValue ;
onKeyPress ( newValue );
},
[ onKeyPress ]
);
const buttons : KeyVal [][] = [
[ '1' , '2' , '3' ],
[ '4' , '5' , '6' ],
[ '7' , '8' , '9' ],
[ '' , '0' , '<' ],
];
return (
< VStack align = "center" >
{ buttons . map (( row , rowIndex ) => (
< HStack key = { rowIndex } justify = "space-between" >
{ row . map ( renderButton ) }
</ HStack >
)) }
</ VStack >
);
};
Security Characteristics
Memory-Only Storage
The passcode is never written to disk . It only exists in the Zustand store’s memory state.
From stores/settingsStore.ts:235-252:
Persistence Configuration
persist (
( set , get ) => ({ /* store definition */ }),
{
name: 'settings-store' ,
storage: createJSONStorage (() => AsyncStorage ),
partialize : ( state ) => ({
theme: state . theme ,
language: state . language ,
displayBtc: state . displayBtc ,
displayCurrency: state . displayCurrency ,
// ... other settings
// NOTE: passcode is NOT included in persisted state
}),
}
)
Threat Model
Protects against unauthorized access to unlocked device
Prevents casual snooping by others nearby
Adds friction for physical theft scenarios
Does NOT protect against:
- Sophisticated attackers with device access
- Forced biometric unlock
- Memory inspection attacks
Limitations
Session-based : Passcode is lost when app is fully terminated
4-digit only : Limited entropy (10,000 possible combinations)
No biometric option : PIN-only (biometrics planned for future)
No rate limiting : Can attempt unlimited passcodes (mitigated by manual entry slowness)
For maximum security, combine passcode lock with full-disk encryption and device lock screen protection.
Resetting Your Passcode
If You Remember Current Passcode
Enter App
Unlock with your current passcode.
Clear Passcode
Go to Settings → Security → Clear Passcode .
Set New Passcode
Optionally set a new passcode immediately.
If You Forgot Your Passcode
There is no passcode recovery mechanism . You must reinstall the app and restore from seed phrase.
Ensure You Have Seed Phrase
Locate your 12-word recovery phrase backup. Without it, you will lose access to your funds.
Uninstall Sovran
Remove the app from your device. This clears all local data including the passcode.
Reinstall and Restore
Install Sovran fresh and restore from your seed phrase.
Set New Passcode
Configure a new passcode (optional).
Best Practices
Choose a Strong PIN Avoid obvious codes like 1234, 0000, or your birth year.
Remember Your PIN Use a memorable but non-obvious number. Write it down separately from your seed phrase.
Device Lock First Enable device-level lock screen protection as primary security.
Seed Phrase Backup Always maintain a secure backup of your seed phrase in case you need to reinstall.
Wallet Recovery Restore from seed phrase if passcode is lost
Privacy Features Other privacy protections in Sovran