From 8ba8eb684efff463e9d42bd24d0c25c288ff7266 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Mon, 26 May 2025 09:49:40 +0200 Subject: [PATCH] Add android autofill instructions page (#846) --- .../app/(tabs)/settings/_layout.tsx | 8 + .../app/(tabs)/settings/android-autofill.tsx | 145 ++++++++++++++++++ apps/mobile-app/app/(tabs)/settings/index.tsx | 31 +++- apps/mobile-app/context/AuthContext.tsx | 36 +++++ 4 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 apps/mobile-app/app/(tabs)/settings/android-autofill.tsx diff --git a/apps/mobile-app/app/(tabs)/settings/_layout.tsx b/apps/mobile-app/app/(tabs)/settings/_layout.tsx index 648ae4c4e..6da4a1c57 100644 --- a/apps/mobile-app/app/(tabs)/settings/_layout.tsx +++ b/apps/mobile-app/app/(tabs)/settings/_layout.tsx @@ -32,6 +32,14 @@ export default function SettingsLayout(): React.ReactNode { ...defaultHeaderOptions, }} /> + => { + 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 => { + 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 ( + + + + + You can configure AliasVault to provide native password autofill functionality in Android. Follow the instructions below to enable it. + + + + + How to enable: + + 1. Open Android Settings via the button below + + + + + Open Android Settings + + + + + 2. Navigate to "Passwords, passkeys & accounts" in the settings menu + + + 3. Change the "prefered service" to "AliasVault" + + + Note: You'll need to authenticate with your device security method when using autofill. + + + {shouldShowAndroidAutofillReminder && ( + + + I already configured it + + + )} + + + + + ); +} \ No newline at end of file diff --git a/apps/mobile-app/app/(tabs)/settings/index.tsx b/apps/mobile-app/app/(tabs)/settings/index.tsx index f0741ec92..8af2eeb4b 100644 --- a/apps/mobile-app/app/(tabs)/settings/index.tsx +++ b/apps/mobile-app/app/(tabs)/settings/index.tsx @@ -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(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 { )} + {Platform.OS === 'android' && ( + <> + + + + + + Android Autofill + {shouldShowAndroidAutofillReminder && ( + + 1 + + )} + + + + + + )} Promise; + // Android Autofill methods + shouldShowAndroidAutofillReminder: boolean; + markAndroidAutofillConfigured: () => Promise; // 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(null); const [globalMessage, setGlobalMessage] = useState(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 => { + 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} A promise that resolves when the check is complete + */ + const checkAutofillConfiguration = async (): Promise => { + 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, ]);