Add android autofill instructions page (#846)

This commit is contained in:
Leendert de Borst
2025-05-26 09:49:40 +02:00
parent b736edbb68
commit 8ba8eb684e
4 changed files with 219 additions and 1 deletions

View File

@@ -32,6 +32,14 @@ export default function SettingsLayout(): React.ReactNode {
...defaultHeaderOptions,
}}
/>
<Stack.Screen
name="android-autofill"
options={{
title: 'Android Autofill',
headerBackTitle: 'Settings',
...defaultHeaderOptions,
}}
/>
<Stack.Screen
name="vault-unlock"
options={{

View File

@@ -0,0 +1,145 @@
import { StyleSheet, View, TouchableOpacity, Linking } from 'react-native';
import { router } from 'expo-router';
import { ThemedText } from '@/components/themed/ThemedText';
import { useColors } from '@/hooks/useColorScheme';
import { useAuth } from '@/context/AuthContext';
import { ThemedScrollView } from '@/components/themed/ThemedScrollView';
import { ThemedContainer } from '@/components/themed/ThemedContainer';
/**
* Android autofill screen.
*/
export default function AndroidAutofillScreen() : React.ReactNode {
const colors = useColors();
const { markAndroidAutofillConfigured, shouldShowAndroidAutofillReminder } = useAuth();
/**
* Handle the configure press.
*/
const handleConfigurePress = async () : Promise<void> => {
await markAndroidAutofillConfigured();
try {
await Linking.sendIntent('android.settings.SETTINGS');
router.back();
} catch (err) {
console.warn('Failed to open settings:', err);
}
};
/**
* Handle the already configured press.
*/
const handleAlreadyConfigured = async () : Promise<void> => {
await markAndroidAutofillConfigured();
router.back();
};
const styles = StyleSheet.create({
buttonContainer: {
padding: 16,
paddingBottom: 32,
},
configureButton: {
alignItems: 'center',
backgroundColor: colors.primary,
borderRadius: 10,
paddingVertical: 16,
},
configureButtonText: {
color: colors.primarySurfaceText,
fontSize: 16,
fontWeight: '600',
},
header: {
paddingTop: 12,
},
headerText: {
color: colors.textMuted,
fontSize: 13,
},
instructionContainer: {
paddingTop: 16,
},
instructionStep: {
color: colors.text,
fontSize: 15,
lineHeight: 22,
marginBottom: 8,
},
instructionTitle: {
color: colors.text,
fontSize: 17,
fontWeight: '600',
marginBottom: 8,
},
secondaryButton: {
alignItems: 'center',
backgroundColor: colors.accentBackground,
borderRadius: 10,
marginTop: 12,
paddingVertical: 16,
},
secondaryButtonText: {
color: colors.text,
fontSize: 16,
fontWeight: '600',
},
warningText: {
color: colors.textMuted,
fontSize: 15,
fontStyle: 'italic',
marginTop: 8,
},
});
return (
<ThemedContainer>
<ThemedScrollView>
<View style={styles.header}>
<ThemedText style={styles.headerText}>
You can configure AliasVault to provide native password autofill functionality in Android. Follow the instructions below to enable it.
</ThemedText>
</View>
<View style={styles.instructionContainer}>
<ThemedText style={styles.instructionTitle}>How to enable:</ThemedText>
<ThemedText style={styles.instructionStep}>
1. Open Android Settings via the button below
</ThemedText>
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.configureButton}
onPress={handleConfigurePress}
>
<ThemedText style={styles.configureButtonText}>
Open Android Settings
</ThemedText>
</TouchableOpacity>
</View>
<ThemedText style={styles.instructionStep}>
2. Navigate to &quot;Passwords, passkeys & accounts&quot; in the settings menu
</ThemedText>
<ThemedText style={styles.instructionStep}>
3. Change the &quot;prefered service&quot; to &quot;AliasVault&quot;
</ThemedText>
<ThemedText style={styles.warningText}>
Note: You&apos;ll need to authenticate with your device security method when using autofill.
</ThemedText>
<View style={styles.buttonContainer}>
{shouldShowAndroidAutofillReminder && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleAlreadyConfigured}
>
<ThemedText style={styles.secondaryButtonText}>
I already configured it
</ThemedText>
</TouchableOpacity>
)}
</View>
</View>
</ThemedScrollView>
</ThemedContainer>
);
}

View File

@@ -21,7 +21,7 @@ import { ThemedContainer } from '@/components/themed/ThemedContainer';
export default function SettingsScreen() : React.ReactNode {
const webApi = useWebApi();
const colors = useColors();
const { getAuthMethodDisplay, shouldShowIosAutofillReminder } = useAuth();
const { getAuthMethodDisplay, shouldShowIosAutofillReminder, shouldShowAndroidAutofillReminder } = useAuth();
const { getAutoLockTimeout } = useAuth();
const scrollY = useRef(new Animated.Value(0)).current;
const scrollViewRef = useRef<ScrollView>(null);
@@ -123,6 +123,13 @@ export default function SettingsScreen() : React.ReactNode {
router.push('/(tabs)/settings/ios-autofill');
};
/**
* Handle the Android autofill press.
*/
const handleAndroidAutofillPress = () : void => {
router.push('/(tabs)/settings/android-autofill');
};
const styles = StyleSheet.create({
scrollContent: {
paddingBottom: 40,
@@ -246,6 +253,28 @@ export default function SettingsScreen() : React.ReactNode {
<View style={styles.separator} />
</>
)}
{Platform.OS === 'android' && (
<>
<TouchableOpacity
style={styles.settingItem}
onPress={handleAndroidAutofillPress}
>
<View style={styles.settingItemIcon}>
<Ionicons name="key-outline" size={20} color={colors.text} />
</View>
<View style={styles.settingItemContent}>
<ThemedText style={styles.settingItemText}>Android Autofill</ThemedText>
{shouldShowAndroidAutofillReminder && (
<View style={styles.settingItemBadge}>
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
</View>
)}
<Ionicons name="chevron-forward" size={20} color={colors.textMuted} />
</View>
</TouchableOpacity>
<View style={styles.separator} />
</>
)}
<TouchableOpacity
style={styles.settingItem}
onPress={handleVaultUnlockPress}

View File

@@ -37,12 +37,16 @@ type AuthContextType = {
// iOS Autofill methods
shouldShowIosAutofillReminder: boolean;
markIosAutofillConfigured: () => Promise<void>;
// Android Autofill methods
shouldShowAndroidAutofillReminder: boolean;
markAndroidAutofillConfigured: () => Promise<void>;
// Return URL methods
returnUrl: { path: string; params?: object } | null;
setReturnUrl: (url: { path: string; params?: object } | null) => void;
}
const IOS_AUTOFILL_CONFIGURED_KEY = 'ios_autofill_configured';
const ANDROID_AUTOFILL_CONFIGURED_KEY = 'android_autofill_configured';
/**
* Auth context.
@@ -58,6 +62,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
const [username, setUsername] = useState<string | null>(null);
const [globalMessage, setGlobalMessage] = useState<string | null>(null);
const [shouldShowIosAutofillReminder, setShouldShowIosAutofillReminder] = useState(false);
const [shouldShowAndroidAutofillReminder, setShouldShowAndroidAutofillReminder] = useState(false);
const [returnUrl, setReturnUrl] = useState<{ path: string; params?: object } | null>(null);
const [isOffline, setIsOffline] = useState(false);
const appState = useRef(AppState.currentState);
@@ -357,17 +362,45 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setShouldShowIosAutofillReminder(false);
}, []);
/**
* Mark Android autofill as configured.
*/
const markAndroidAutofillConfigured = useCallback(async (): Promise<void> => {
await AsyncStorage.setItem(ANDROID_AUTOFILL_CONFIGURED_KEY, 'true');
setShouldShowAndroidAutofillReminder(false);
}, []);
// Load iOS Autofill state on mount
useEffect(() => {
loadIosAutofillState();
}, [loadIosAutofillState]);
// Check if autofill is configured on mount
useEffect(() => {
/**
* Check if autofill is configured for the current platform.
* @returns {Promise<void>} A promise that resolves when the check is complete
*/
const checkAutofillConfiguration = async (): Promise<void> => {
if (Platform.OS === 'ios') {
const isConfigured = await AsyncStorage.getItem(IOS_AUTOFILL_CONFIGURED_KEY);
setShouldShowIosAutofillReminder(!isConfigured);
} else if (Platform.OS === 'android') {
const isConfigured = await AsyncStorage.getItem(ANDROID_AUTOFILL_CONFIGURED_KEY);
setShouldShowAndroidAutofillReminder(!isConfigured);
}
};
checkAutofillConfiguration();
}, []);
const contextValue = useMemo(() => ({
isLoggedIn,
isInitialized,
username,
globalMessage,
shouldShowIosAutofillReminder,
shouldShowAndroidAutofillReminder,
returnUrl,
isOffline,
getEnabledAuthMethods,
@@ -383,6 +416,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setAutoLockTimeout,
getBiometricDisplayName,
markIosAutofillConfigured,
markAndroidAutofillConfigured,
setReturnUrl,
verifyPassword,
setOfflineMode: setIsOffline,
@@ -392,6 +426,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
username,
globalMessage,
shouldShowIosAutofillReminder,
shouldShowAndroidAutofillReminder,
returnUrl,
isOffline,
getEnabledAuthMethods,
@@ -407,6 +442,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children
setAutoLockTimeout,
getBiometricDisplayName,
markIosAutofillConfigured,
markAndroidAutofillConfigured,
setReturnUrl,
verifyPassword,
]);