mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-06 15:58:09 -05:00
Add native settings page open callback for android (#846)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 "autofill preferred service" to "AliasVault"
|
||||
</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'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 "Passwords and autofill" section in the settings menu. Depending on your device, this option may be under "General management" or "System Settings".
|
||||
</ThemedText>
|
||||
<ThemedText style={styles.instructionStep}>
|
||||
3. Change the "autofill preferred service" to "AliasVault"
|
||||
</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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -125,4 +125,8 @@
|
||||
[vaultManager setCurrentVaultRevisionNumber:revisionNumber resolver:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
- (void)openAutofillSettingsPage:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
||||
[vaultManager openAutofillSettingsPage:resolve rejecter:reject];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user