Add native settings page open callback for android (#846)

This commit is contained in:
Leendert de Borst
2025-05-28 14:57:55 +02:00
parent 347721a575
commit 84df5b7d98
9 changed files with 102 additions and 87 deletions

View File

@@ -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)
}
}
}

View File

@@ -118,7 +118,7 @@ export default function TabLayout() : React.ReactNode {
tabBarIcon: ({ color }) => (
<View style={styles.iconContainer}>
<IconSymbol size={28} name={IconSymbolName.Gear} color={color} />
{Platform.OS === 'ios' && authContext.shouldShowIosAutofillReminder && (
{authContext.shouldShowAutofillReminder && (
<View style={styles.iconNotificationContainer}>
<ThemedText style={styles.iconNotificationText}>1</ThemedText>
</View>

View File

@@ -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<void> => {
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<void> => {
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 {
<View style={styles.instructionContainer}>
<ThemedText style={styles.instructionTitle}>How to enable:</ThemedText>
<ThemedText style={styles.instructionStep}>
1. Open Android Settings via the button below
1. Open Android Settings via the button below, and change the &quot;autofill preferred service&quot; to &quot;AliasVault&quot;
</ThemedText>
<View style={styles.buttonContainer}>
<TouchableOpacity
@@ -142,21 +148,18 @@ export default function AndroidAutofillScreen() : React.ReactNode {
onPress={handleConfigurePress}
>
<ThemedText style={styles.configureButtonText}>
Open Android Settings
Open Autofill Settings
</ThemedText>
</TouchableOpacity>
<ThemedText style={styles.tipStep}>
If the button above doesn&apos;t work it might be blocked because of security settings. You can manually go to Android Settings General Management Passwords and autofill.
</ThemedText>
</View>
<ThemedText style={styles.instructionStep}>
2. Navigate to the &quot;Passwords and autofill&quot; section in the settings menu. Depending on your device, this option may be under &quot;General management&quot; or &quot;System Settings&quot;.
</ThemedText>
<ThemedText style={styles.instructionStep}>
3. Change the &quot;autofill preferred service&quot; to &quot;AliasVault&quot;
</ThemedText>
<ThemedText style={styles.instructionStep}>
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.
</ThemedText>
<View style={styles.buttonContainer}>
{shouldShowAndroidAutofillReminder && (
{shouldShowAutofillReminder && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleAlreadyConfigured}

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, shouldShowAndroidAutofillReminder } = useAuth();
const { getAuthMethodDisplay, shouldShowAutofillReminder } = useAuth();
const { getAutoLockTimeout } = useAuth();
const scrollY = useRef(new Animated.Value(0)).current;
const scrollViewRef = useRef<ScrollView>(null);
@@ -242,7 +242,7 @@ export default function SettingsScreen() : React.ReactNode {
</View>
<View style={styles.settingItemContent}>
<ThemedText style={styles.settingItemText}>iOS Autofill</ThemedText>
{shouldShowIosAutofillReminder && (
{shouldShowAutofillReminder && (
<View style={styles.settingItemBadge}>
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
</View>
@@ -264,7 +264,7 @@ export default function SettingsScreen() : React.ReactNode {
</View>
<View style={styles.settingItemContent}>
<ThemedText style={styles.settingItemText}>Android Autofill</ThemedText>
{shouldShowAndroidAutofillReminder && (
{shouldShowAutofillReminder && (
<View style={styles.settingItemBadge}>
<ThemedText style={styles.settingItemBadgeText}>1</ThemedText>
</View>

View File

@@ -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<void> => {
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<void> => {
await markIosAutofillConfigured();
await markAutofillConfigured();
router.back();
};
@@ -125,7 +129,7 @@ export default function IosAutofillScreen() : React.ReactNode {
Note: You&apos;ll need to authenticate with Face ID/Touch ID or your device passcode when using autofill.
</ThemedText>
<View style={styles.buttonContainer}>
{shouldShowIosAutofillReminder && (
{shouldShowAutofillReminder && (
<TouchableOpacity
style={styles.secondaryButton}
onPress={handleAlreadyConfigured}

View File

@@ -34,19 +34,15 @@ type AuthContextType = {
getBiometricDisplayName: () => Promise<string>;
setOfflineMode: (isOffline: boolean) => void;
verifyPassword: (password: string) => Promise<string | null>;
// iOS Autofill methods
shouldShowIosAutofillReminder: boolean;
markIosAutofillConfigured: () => Promise<void>;
// Android Autofill methods
shouldShowAndroidAutofillReminder: boolean;
markAndroidAutofillConfigured: () => Promise<void>;
// Autofill methods
shouldShowAutofillReminder: boolean;
markAutofillConfigured: () => 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';
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<string | null>(null);
const [globalMessage, setGlobalMessage] = useState<string | null>(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<void> => {
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<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();
}, []);
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,
]);

View File

@@ -125,4 +125,8 @@
[vaultManager setCurrentVaultRevisionNumber:revisionNumber resolver:resolve rejecter:reject];
}
- (void)openAutofillSettingsPage:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
[vaultManager openAutofillSettingsPage:resolve rejecter:reject];
}
@end

View File

@@ -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"

View File

@@ -34,6 +34,7 @@ export interface Spec extends TurboModule {
setAutoLockTimeout(timeout: number): Promise<void>;
getAutoLockTimeout(): Promise<number>;
getAuthMethods(): Promise<string[]>;
openAutofillSettingsPage(): Promise<void>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('NativeVaultManager');