mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-21 08:02:45 -04:00
Refactor nav structure, make credential create deep link work (#771)
This commit is contained in:
@@ -5,9 +5,11 @@
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "AliasVault",
|
||||
"scheme": "net.aliasvault.app",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"deepLinking": true,
|
||||
"platforms": ["ios", "android", "web"],
|
||||
"ios": {
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "net.aliasvault.app"
|
||||
|
||||
@@ -57,21 +57,21 @@ export default function TabLayout() {
|
||||
}),
|
||||
}}>
|
||||
<Tabs.Screen
|
||||
name="(credentials)"
|
||||
name="credentials"
|
||||
options={{
|
||||
title: 'Credentials',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="key.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="(emails)"
|
||||
name="emails"
|
||||
options={{
|
||||
title: 'Emails',
|
||||
tabBarIcon: ({ color }) => <IconSymbol size={28} name="envelope.fill" color={color} />,
|
||||
}}
|
||||
/>
|
||||
<Tabs.Screen
|
||||
name="(settings)"
|
||||
name="settings"
|
||||
options={{
|
||||
title: 'Settings',
|
||||
tabBarIcon: ({ color }) => (
|
||||
|
||||
@@ -6,15 +6,15 @@ import { ThemedView } from '@/components/ThemedView';
|
||||
import { ThemedSafeAreaView } from '@/components/ThemedSafeAreaView';
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
import { useDb } from '@/context/DbContext';
|
||||
import { Credential, Alias } from '@/utils/types/Credential';
|
||||
import { Credential } from '@/utils/types/Credential';
|
||||
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
|
||||
import Toast from 'react-native-toast-message';
|
||||
import { Gender } from "../../../utils/generators/Identity/types/Gender";
|
||||
import { Gender } from "@/utils/generators/Identity/types/Gender";
|
||||
|
||||
type CredentialMode = 'random' | 'manual';
|
||||
|
||||
export default function AddEditCredentialScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const { id, serviceUrl } = useLocalSearchParams<{ id: string, serviceUrl?: string }>();
|
||||
const router = useRouter();
|
||||
const colors = useColors();
|
||||
const dbContext = useDb();
|
||||
@@ -38,33 +38,60 @@ export default function AddEditCredentialScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
// Set navigation options
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
function extractServiceNameFromUrl(url: string): string {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
const hostParts = urlObj.hostname.split('.');
|
||||
|
||||
// Remove common subdomains
|
||||
const commonSubdomains = ['www', 'app', 'login', 'auth', 'account', 'portal'];
|
||||
while (hostParts.length > 2 && commonSubdomains.includes(hostParts[0].toLowerCase())) {
|
||||
hostParts.shift();
|
||||
}
|
||||
|
||||
// For domains like google.com, return Google.com
|
||||
if (hostParts.length <= 2) {
|
||||
const domain = hostParts.join('.');
|
||||
return domain.charAt(0).toUpperCase() + domain.slice(1);
|
||||
}
|
||||
|
||||
// For domains like app.example.com, return Example.com
|
||||
const mainDomain = hostParts.slice(-2).join('.');
|
||||
return mainDomain.charAt(0).toUpperCase() + mainDomain.slice(1);
|
||||
} catch (e) {
|
||||
// If URL parsing fails, return the original URL
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set navigation options
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerLeft: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={{ padding: 10, paddingLeft: 0 }}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>Cancel</ThemedText>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={{ padding: 10, paddingLeft: 0 }}
|
||||
onPress={handleSave}
|
||||
style={{ padding: 10, paddingRight: 0 }}
|
||||
>
|
||||
<ThemedText style={{ color: colors.primary }}>Cancel</ThemedText>
|
||||
<MaterialIcons
|
||||
name="save"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => (
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<TouchableOpacity
|
||||
onPress={handleSave}
|
||||
style={{ padding: 10, paddingRight: 0 }}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="save"
|
||||
size={24}
|
||||
color={colors.primary}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, credential, mode]);
|
||||
</View>
|
||||
),
|
||||
});
|
||||
}, [navigation, credential, mode]);
|
||||
|
||||
const isEditMode = !!id;
|
||||
|
||||
@@ -74,6 +101,26 @@ export default function AddEditCredentialScreen() {
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
// If serviceUrl is provided, extract the service name from the URL and prefill the form values.
|
||||
// This is used when the user opens the app from a deep link (e.g. from iOS autofill extension).
|
||||
if (serviceUrl) {
|
||||
// Decode the URL-encoded service URL
|
||||
const decodedUrl = decodeURIComponent(serviceUrl);
|
||||
|
||||
// Extract service name from URL
|
||||
const serviceName = extractServiceNameFromUrl(decodedUrl);
|
||||
// Set the form values
|
||||
// Note: You'll need to implement this based on your form state management
|
||||
setCredential(prev => ({
|
||||
...prev,
|
||||
ServiceUrl: decodedUrl,
|
||||
ServiceName: serviceName,
|
||||
// ... other form fields
|
||||
}));
|
||||
}
|
||||
}, [serviceUrl]);
|
||||
|
||||
const loadExistingCredential = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -244,6 +291,8 @@ export default function AddEditCredentialScreen() {
|
||||
},
|
||||
});
|
||||
|
||||
console.log('credential serviceName: ', credential.ServiceName);
|
||||
|
||||
return (
|
||||
<ThemedSafeAreaView style={styles.container}>
|
||||
<ThemedView style={styles.content}>
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import EmailDetailsScreen from '../../(emails)/[id]';
|
||||
import EmailDetailsScreen from '../../emails/[id]';
|
||||
|
||||
/**
|
||||
* CredentialEmailPreviewScreen Component
|
||||
@@ -31,7 +31,7 @@ export default function CredentialsScreen() {
|
||||
const headerButtons = [{
|
||||
icon: 'add' as const,
|
||||
position: 'right' as const,
|
||||
onPress: () => router.push('/(tabs)/(credentials)/add-edit')
|
||||
onPress: () => router.push('/(tabs)/credentials/add-edit')
|
||||
}];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -44,7 +44,7 @@ export default function CredentialsScreen() {
|
||||
});
|
||||
|
||||
const sub = emitter.addListener('tabPress', (routeName: string) => {
|
||||
if (routeName === '(credentials)' && isTabFocused) {
|
||||
if (routeName === 'credentials' && isTabFocused) {
|
||||
console.log('Credentials tab re-pressed while focused: reset screen');
|
||||
setSearchQuery(''); // Reset search
|
||||
setRefreshing(false); // Reset refreshing
|
||||
@@ -168,7 +168,7 @@ export default function EmailDetailsScreen() {
|
||||
|
||||
const handleOpenCredential = () => {
|
||||
if (associatedCredential) {
|
||||
router.push(`/(tabs)/(credentials)/${associatedCredential.Id}`);
|
||||
router.push(`/(tabs)/credentials/${associatedCredential.Id}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function EmailsScreen() {
|
||||
});
|
||||
|
||||
const sub = emitter.addListener('tabPress', (routeName: string) => {
|
||||
if (routeName === '(emails)' && isTabFocused) {
|
||||
if (routeName === 'emails' && isTabFocused) {
|
||||
console.log('Emails tab re-pressed while focused: reset screen');
|
||||
// Scroll to top
|
||||
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
|
||||
@@ -54,15 +54,15 @@ export default function SettingsScreen() {
|
||||
};
|
||||
|
||||
const handleVaultUnlockPress = () => {
|
||||
router.push('/(tabs)/(settings)/vault-unlock');
|
||||
router.push('/(tabs)/settings/vault-unlock');
|
||||
};
|
||||
|
||||
const handleAutoLockPress = () => {
|
||||
router.push('/(tabs)/(settings)/auto-lock');
|
||||
router.push('/(tabs)/settings/auto-lock');
|
||||
};
|
||||
|
||||
const handleIosAutofillPress = () => {
|
||||
router.push('/(tabs)/(settings)/ios-autofill');
|
||||
router.push('/(tabs)/settings/ios-autofill');
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -62,7 +62,7 @@ export default function SyncScreen() {
|
||||
console.log('FaceID unlock successful, navigating to credentials');
|
||||
|
||||
// Navigate to credentials
|
||||
router.replace('/(tabs)/(credentials)');
|
||||
router.replace('/(tabs)/credentials');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function UnlockScreen() {
|
||||
// Initialize the database with the vault response and password
|
||||
if (await testDatabaseConnection(passwordHashBase64)) {
|
||||
// Navigate to credentials
|
||||
router.replace('/(tabs)/(credentials)');
|
||||
router.replace('/(tabs)/credentials');
|
||||
}
|
||||
else {
|
||||
Alert.alert('Error', 'Incorrect password. Please try again.');
|
||||
|
||||
@@ -69,7 +69,7 @@ export function CredentialCard({ credential }: CredentialCardProps) {
|
||||
style={styles.credentialCard}
|
||||
onPress={() => {
|
||||
Keyboard.dismiss();
|
||||
router.push(`/(tabs)/(credentials)/${credential.Id}`);
|
||||
router.push(`/(tabs)/credentials/${credential.Id}`);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
|
||||
@@ -79,7 +79,7 @@ export function EmailCard({ email }: EmailCardProps) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.emailCard}
|
||||
onPress={() => router.push(`/(tabs)/(emails)/${email.id}`)}
|
||||
onPress={() => router.push(`/(tabs)/emails/${email.id}`)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.emailHeader}>
|
||||
|
||||
@@ -193,7 +193,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) => {
|
||||
const emailPrefix = email.split('@')[0];
|
||||
Linking.openURL(`https://spamok.com/${emailPrefix}/${mail.id}`);
|
||||
} else {
|
||||
router.push(`/(tabs)/(credentials)/email/${mail.id}`);
|
||||
router.push(`/(tabs)/credentials/email/${mail.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -88,10 +88,16 @@ class CredentialProviderViewController: ASCredentialProviderViewController {
|
||||
override func prepareCredentialList(for serviceIdentifiers: [ASCredentialServiceIdentifier]) {
|
||||
guard let viewModel = self.viewModel else { return }
|
||||
|
||||
// Instead of directly filtering credentials, just set the search text
|
||||
let matchedDomains = serviceIdentifiers.map { $0.identifier.lowercased() }
|
||||
if let firstDomain = matchedDomains.first {
|
||||
// Set the search text to the first domain which will auto filter the credentials
|
||||
// to show the most likely credentials first as suggestion.
|
||||
viewModel.setSearchFilter(firstDomain)
|
||||
|
||||
// Set the service URL to the first domain which will be used to pass onto the
|
||||
// add credential view when the user taps the "+" button and prefill it with the
|
||||
// domain name.
|
||||
viewModel.serviceUrl = firstDomain
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,10 +51,12 @@ public struct CredentialProviderView: View {
|
||||
if !viewModel.isChoosingTextToInsert {
|
||||
VStack(spacing: 12) {
|
||||
Button(action: {
|
||||
if let url = URL(string: "net.aliasvault.app://addCredential?service=example.com") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
if let serviceUrl = viewModel.serviceUrl {
|
||||
let encodedUrl = serviceUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
if let url = URL(string: "net.aliasvault.app://credentials/add-edit?serviceUrl=\(encodedUrl)") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
//viewModel.showAddCredential = true
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: "plus.circle.fill")
|
||||
@@ -101,10 +103,12 @@ public struct CredentialProviderView: View {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
Button("Add") {
|
||||
if let url = URL(string: "net.aliasvault.app://addCredential?service=example.com") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
if let serviceUrl = viewModel.serviceUrl {
|
||||
let encodedUrl = serviceUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
|
||||
if let url = URL(string: "net.aliasvault.app://credentials/add-edit?serviceUrl=\(encodedUrl)") {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
//viewModel.showAddCredential = true
|
||||
}
|
||||
.foregroundColor(ColorConstants.Light.primary)
|
||||
}
|
||||
@@ -115,14 +119,14 @@ public struct CredentialProviderView: View {
|
||||
}
|
||||
.actionSheet(isPresented: $viewModel.showSelectionOptions) {
|
||||
// Define all text strings
|
||||
|
||||
|
||||
|
||||
|
||||
guard let credential = viewModel.selectedCredential else {
|
||||
return ActionSheet(title: Text("Select Login Method"), message: Text("No credential selected."), buttons: [.cancel()])
|
||||
}
|
||||
|
||||
var buttons: [ActionSheet.Button] = []
|
||||
|
||||
|
||||
if viewModel.isChoosingTextToInsert {
|
||||
if let username = credential.username, !username.isEmpty {
|
||||
buttons.append(.default(Text("Username: \(username)")) {
|
||||
@@ -135,7 +139,7 @@ public struct CredentialProviderView: View {
|
||||
viewModel.selectEmail()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
buttons.append(.default(Text("Password")) {
|
||||
viewModel.selectPassword()
|
||||
})
|
||||
@@ -193,6 +197,7 @@ public class CredentialProviderViewModel: ObservableObject {
|
||||
@Published var showSelectionOptions = false
|
||||
@Published var selectedCredential: Credential?
|
||||
@Published public var isChoosingTextToInsert = false
|
||||
@Published public var serviceUrl: String?
|
||||
|
||||
@Published var newUsername = ""
|
||||
@Published var newPassword = ""
|
||||
@@ -205,11 +210,16 @@ public class CredentialProviderViewModel: ObservableObject {
|
||||
public init(
|
||||
loader: @escaping () async throws -> [Credential],
|
||||
selectionHandler: @escaping (String, String) -> Void,
|
||||
cancelHandler: @escaping () -> Void
|
||||
cancelHandler: @escaping () -> Void,
|
||||
serviceUrl: String? = nil
|
||||
) {
|
||||
self.loader = loader
|
||||
self.selectionHandler = selectionHandler
|
||||
self.cancelHandler = cancelHandler
|
||||
self.serviceUrl = serviceUrl
|
||||
if let url = serviceUrl {
|
||||
self.searchText = url
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -325,19 +335,19 @@ public class CredentialProviderViewModel: ObservableObject {
|
||||
// If we have both options, show selection sheet
|
||||
showSelectionOptions = true
|
||||
}
|
||||
|
||||
|
||||
func selectUsername() {
|
||||
guard let credential = selectedCredential else { return }
|
||||
selectionHandler(credential.username ?? "", "")
|
||||
showSelectionOptions = false
|
||||
}
|
||||
|
||||
|
||||
func selectEmail() {
|
||||
guard let credential = selectedCredential else { return }
|
||||
selectionHandler(credential.alias?.email ?? "", "")
|
||||
showSelectionOptions = false
|
||||
}
|
||||
|
||||
|
||||
func selectPassword() {
|
||||
guard let credential = selectedCredential else { return }
|
||||
selectionHandler(credential.password?.value ?? "", "")
|
||||
@@ -551,7 +561,8 @@ class PreviewCredentialProviderViewModel: CredentialProviderViewModel {
|
||||
return previewCredentials
|
||||
},
|
||||
selectionHandler: { _, _ in },
|
||||
cancelHandler: {}
|
||||
cancelHandler: {},
|
||||
serviceUrl: nil
|
||||
)
|
||||
|
||||
credentials = previewCredentials
|
||||
|
||||
Reference in New Issue
Block a user