From 84df5b7d989f54c58d07d3e8d8a2c6bdbaa3bbf5 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 28 May 2025 14:57:55 +0200 Subject: [PATCH] Add native settings page open callback for android (#846) --- .../nativevaultmanager/NativeVaultManager.kt | 31 +++++++ apps/mobile-app/app/(tabs)/_layout.tsx | 2 +- .../app/(tabs)/settings/android-autofill.tsx | 33 ++++---- apps/mobile-app/app/(tabs)/settings/index.tsx | 6 +- .../app/(tabs)/settings/ios-autofill.tsx | 18 +++-- apps/mobile-app/context/AuthContext.tsx | 81 +++++-------------- .../RCTNativeVaultManager.mm | 4 + .../ios/NativeVaultManager/VaultManager.swift | 13 +++ apps/mobile-app/specs/NativeVaultManager.ts | 1 + 9 files changed, 102 insertions(+), 87 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt index 250832a6d..6fc4871da 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt @@ -12,6 +12,10 @@ import org.json.JSONArray import net.aliasvault.app.vaultstore.VaultStore import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.core.net.toUri @ReactModule(name = NativeVaultManager.NAME) class NativeVaultManager(reactContext: ReactApplicationContext) : @@ -349,4 +353,31 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : promise.reject("ERR_GET_AUTH_METHODS", "Failed to get auth methods: ${e.message}", e) } } + + @ReactMethod + override fun openAutofillSettingsPage(promise: Promise) { + try { + val autofillIntent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE).apply { + data = "package:${reactApplicationContext.packageName}".toUri() + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + // Try to resolve the intent first + if (autofillIntent.resolveActivity(reactApplicationContext.packageManager) != null) { + reactApplicationContext.startActivity(autofillIntent) + } else { + // Fallback to privacy settings (may contain Autofill on Samsung) + val fallbackIntent = Intent(Settings.ACTION_PRIVACY_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + reactApplicationContext.startActivity(fallbackIntent) + } + + promise.resolve(null) + + } catch (e: Exception) { + Log.e(TAG, "Error opening autofill settings", e) + promise.reject("ERR_OPEN_AUTOFILL_SETTINGS", "Failed to open autofill settings: ${e.message}", e) + } + } } diff --git a/apps/mobile-app/app/(tabs)/_layout.tsx b/apps/mobile-app/app/(tabs)/_layout.tsx index 4af3abc74..7089e9e25 100644 --- a/apps/mobile-app/app/(tabs)/_layout.tsx +++ b/apps/mobile-app/app/(tabs)/_layout.tsx @@ -118,7 +118,7 @@ export default function TabLayout() : React.ReactNode { tabBarIcon: ({ color }) => ( - {Platform.OS === 'ios' && authContext.shouldShowIosAutofillReminder && ( + {authContext.shouldShowAutofillReminder && ( 1 diff --git a/apps/mobile-app/app/(tabs)/settings/android-autofill.tsx b/apps/mobile-app/app/(tabs)/settings/android-autofill.tsx index 51b7d695f..d80d9c599 100644 --- a/apps/mobile-app/app/(tabs)/settings/android-autofill.tsx +++ b/apps/mobile-app/app/(tabs)/settings/android-autofill.tsx @@ -1,6 +1,7 @@ import { StyleSheet, View, TouchableOpacity, Linking } from 'react-native'; import { router } from 'expo-router'; +import NativeVaultManager from '@/specs/NativeVaultManager'; import { ThemedText } from '@/components/themed/ThemedText'; import { useColors } from '@/hooks/useColorScheme'; import { useAuth } from '@/context/AuthContext'; @@ -12,16 +13,15 @@ import { ThemedContainer } from '@/components/themed/ThemedContainer'; */ export default function AndroidAutofillScreen() : React.ReactNode { const colors = useColors(); - const { markAndroidAutofillConfigured, shouldShowAndroidAutofillReminder } = useAuth(); + const { markAutofillConfigured, shouldShowAutofillReminder } = useAuth(); /** * Handle the configure press. */ const handleConfigurePress = async () : Promise => { - await markAndroidAutofillConfigured(); + await markAutofillConfigured(); try { - await Linking.sendIntent('android.settings.SETTINGS'); - router.back(); + await NativeVaultManager.openAutofillSettingsPage(); } catch (err) { console.warn('Failed to open settings:', err); } @@ -31,7 +31,7 @@ export default function AndroidAutofillScreen() : React.ReactNode { * Handle the already configured press. */ const handleAlreadyConfigured = async () : Promise => { - await markAndroidAutofillConfigured(); + await markAutofillConfigured(); router.back(); }; @@ -89,6 +89,12 @@ export default function AndroidAutofillScreen() : React.ReactNode { fontSize: 16, fontWeight: '600', }, + tipStep: { + color: colors.textMuted, + fontSize: 13, + lineHeight: 20, + marginTop: 8, + }, warningContainer: { backgroundColor: colors.accentBackground, marginBottom: 16, @@ -134,7 +140,7 @@ export default function AndroidAutofillScreen() : React.ReactNode { How to enable: - 1. Open Android Settings via the button below + 1. Open Android Settings via the button below, and change the "autofill preferred service" to "AliasVault" - Open Android Settings + Open Autofill Settings + + If the button above doesn't work it might be blocked because of security settings. You can manually go to Android Settings → General Management → Passwords and autofill. + - 2. Navigate to the "Passwords and autofill" section in the settings menu. Depending on your device, this option may be under "General management" or "System Settings". - - - 3. Change the "autofill preferred service" to "AliasVault" - - - 4. Some apps, like Google Chrome, may require manual configuration in their settings to allow third-party autofill apps. However, most apps should work with autofill by default. + 2. Some apps, e.g. Google Chrome, may require manual configuration in their settings to allow third-party autofill apps. However, most apps should work with autofill by default. - {shouldShowAndroidAutofillReminder && ( + {shouldShowAutofillReminder && ( (null); @@ -242,7 +242,7 @@ export default function SettingsScreen() : React.ReactNode { iOS Autofill - {shouldShowIosAutofillReminder && ( + {shouldShowAutofillReminder && ( 1 @@ -264,7 +264,7 @@ export default function SettingsScreen() : React.ReactNode { Android Autofill - {shouldShowAndroidAutofillReminder && ( + {shouldShowAutofillReminder && ( 1 diff --git a/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx b/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx index ba4b15faa..35dd11118 100644 --- a/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx +++ b/apps/mobile-app/app/(tabs)/settings/ios-autofill.tsx @@ -1,6 +1,7 @@ -import { StyleSheet, View, TouchableOpacity, Linking } from 'react-native'; +import { StyleSheet, View, TouchableOpacity } from 'react-native'; import { router } from 'expo-router'; +import NativeVaultManager from '@/specs/NativeVaultManager'; import { ThemedText } from '@/components/themed/ThemedText'; import { useColors } from '@/hooks/useColorScheme'; import { useAuth } from '@/context/AuthContext'; @@ -12,22 +13,25 @@ import { ThemedContainer } from '@/components/themed/ThemedContainer'; */ export default function IosAutofillScreen() : React.ReactNode { const colors = useColors(); - const { markIosAutofillConfigured, shouldShowIosAutofillReminder } = useAuth(); + const { markAutofillConfigured, shouldShowAutofillReminder } = useAuth(); /** * Handle the configure press. */ const handleConfigurePress = async () : Promise => { - await markIosAutofillConfigured(); - await Linking.openURL('App-Prefs:root'); - router.back(); + await markAutofillConfigured(); + try { + await NativeVaultManager.openAutofillSettingsPage(); + } catch (err) { + console.warn('Failed to open settings:', err); + } }; /** * Handle the already configured press. */ const handleAlreadyConfigured = async () : Promise => { - await markIosAutofillConfigured(); + await markAutofillConfigured(); router.back(); }; @@ -125,7 +129,7 @@ export default function IosAutofillScreen() : React.ReactNode { Note: You'll need to authenticate with Face ID/Touch ID or your device passcode when using autofill. - {shouldShowIosAutofillReminder && ( + {shouldShowAutofillReminder && ( Promise; setOfflineMode: (isOffline: boolean) => void; verifyPassword: (password: string) => Promise; - // iOS Autofill methods - shouldShowIosAutofillReminder: boolean; - markIosAutofillConfigured: () => Promise; - // Android Autofill methods - shouldShowAndroidAutofillReminder: boolean; - markAndroidAutofillConfigured: () => Promise; + // Autofill methods + shouldShowAutofillReminder: boolean; + markAutofillConfigured: () => 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'; +const AUTOFILL_CONFIGURED_KEY = 'autofill_configured'; /** * Auth context. @@ -61,8 +57,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const [isInitialized, setIsInitialized] = useState(false); const [username, setUsername] = useState(null); const [globalMessage, setGlobalMessage] = useState(null); - const [shouldShowIosAutofillReminder, setShouldShowIosAutofillReminder] = useState(false); - const [shouldShowAndroidAutofillReminder, setShouldShowAndroidAutofillReminder] = useState(false); + const [shouldShowAutofillReminder, setShouldShowAutofillReminder] = useState(false); const [returnUrl, setReturnUrl] = useState<{ path: string; params?: object } | null>(null); const [isOffline, setIsOffline] = useState(false); const appState = useRef(AppState.currentState); @@ -342,65 +337,32 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }, [isVaultUnlocked, pathname]); /** - * Load iOS Autofill state from storage + * Load autofill state from storage */ - const loadIosAutofillState = useCallback(async () => { - if (Platform.OS !== 'ios') { - setShouldShowIosAutofillReminder(false); - return; - } - - const configured = await AsyncStorage.getItem(IOS_AUTOFILL_CONFIGURED_KEY); - setShouldShowIosAutofillReminder(configured !== 'true'); + const loadAutofillState = useCallback(async () => { + const configured = await AsyncStorage.getItem(AUTOFILL_CONFIGURED_KEY); + setShouldShowAutofillReminder(configured !== 'true'); }, []); /** - * Mark iOS Autofill as configured + * Mark autofill as configured for the current platform */ - const markIosAutofillConfigured = useCallback(async () => { - await AsyncStorage.setItem(IOS_AUTOFILL_CONFIGURED_KEY, 'true'); - setShouldShowIosAutofillReminder(false); + const markAutofillConfigured = useCallback(async () => { + await AsyncStorage.setItem(AUTOFILL_CONFIGURED_KEY, 'true'); + setShouldShowAutofillReminder(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 + // Load 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(); - }, []); + loadAutofillState(); + }, [loadAutofillState]); const contextValue = useMemo(() => ({ isLoggedIn, isInitialized, username, globalMessage, - shouldShowIosAutofillReminder, - shouldShowAndroidAutofillReminder, + shouldShowAutofillReminder, returnUrl, isOffline, getEnabledAuthMethods, @@ -415,8 +377,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children getAutoLockTimeout, setAutoLockTimeout, getBiometricDisplayName, - markIosAutofillConfigured, - markAndroidAutofillConfigured, + markAutofillConfigured, setReturnUrl, verifyPassword, setOfflineMode: setIsOffline, @@ -425,8 +386,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children isInitialized, username, globalMessage, - shouldShowIosAutofillReminder, - shouldShowAndroidAutofillReminder, + shouldShowAutofillReminder, returnUrl, isOffline, getEnabledAuthMethods, @@ -441,8 +401,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children getAutoLockTimeout, setAutoLockTimeout, getBiometricDisplayName, - markIosAutofillConfigured, - markAndroidAutofillConfigured, + markAutofillConfigured, setReturnUrl, verifyPassword, ]); diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index d8db0ccd5..608a5429f 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -125,4 +125,8 @@ [vaultManager setCurrentVaultRevisionNumber:revisionNumber resolver:resolve rejecter:reject]; } +- (void)openAutofillSettingsPage:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [vaultManager openAutofillSettingsPage:resolve rejecter:reject]; +} + @end diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 1dce2f10f..7057d83d9 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -308,6 +308,19 @@ public class VaultManager: NSObject { } } + @objc + func openAutofillSettingsPage(_ resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + if let settingsUrl = URL(string: "App-Prefs:root") { + DispatchQueue.main.async { + UIApplication.shared.open(settingsUrl) + resolve(nil) + } + } else { + reject("SETTINGS_ERROR", "Cannot open settings", nil) + } + } + @objc static func moduleName() -> String! { return "VaultManager" diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index 94483a39b..49cf4b8a1 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -34,6 +34,7 @@ export interface Spec extends TurboModule { setAutoLockTimeout(timeout: number): Promise; getAutoLockTimeout(): Promise; getAuthMethods(): Promise; + openAutofillSettingsPage(): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager');