From caef74477b36fc4631e38c2ad16eba456bed6b4b Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Tue, 29 Apr 2025 13:37:20 +0200 Subject: [PATCH] Refactor nav structure, make credential create deep link work (#771) --- mobile-app/app.json | 4 +- mobile-app/app/(tabs)/_layout.tsx | 6 +- .../{(credentials) => credentials}/[id].tsx | 0 .../_layout.tsx | 0 .../add-edit.tsx | 103 +++++++++++++----- .../email/[id].tsx | 2 +- .../{(credentials) => credentials}/index.tsx | 4 +- .../app/(tabs)/{(emails) => emails}/[id].tsx | 2 +- .../(tabs)/{(emails) => emails}/_layout.tsx | 0 .../app/(tabs)/{(emails) => emails}/index.tsx | 2 +- .../{(settings) => settings}/_layout.tsx | 0 .../{(settings) => settings}/auto-lock.tsx | 0 .../(tabs)/{(settings) => settings}/index.tsx | 6 +- .../{(settings) => settings}/ios-autofill.tsx | 0 .../{(settings) => settings}/vault-unlock.tsx | 0 mobile-app/app/sync.tsx | 2 +- mobile-app/app/unlock.tsx | 2 +- mobile-app/components/CredentialCard.tsx | 2 +- mobile-app/components/EmailCard.tsx | 2 +- .../credentialDetails/EmailPreview.tsx | 2 +- .../CredentialProviderViewController.swift | 8 +- .../Views/CredentialProviderView.swift | 41 ++++--- 22 files changed, 128 insertions(+), 60 deletions(-) rename mobile-app/app/(tabs)/{(credentials) => credentials}/[id].tsx (100%) rename mobile-app/app/(tabs)/{(credentials) => credentials}/_layout.tsx (100%) rename mobile-app/app/(tabs)/{(credentials) => credentials}/add-edit.tsx (81%) rename mobile-app/app/(tabs)/{(credentials) => credentials}/email/[id].tsx (92%) rename mobile-app/app/(tabs)/{(credentials) => credentials}/index.tsx (98%) rename mobile-app/app/(tabs)/{(emails) => emails}/[id].tsx (99%) rename mobile-app/app/(tabs)/{(emails) => emails}/_layout.tsx (100%) rename mobile-app/app/(tabs)/{(emails) => emails}/index.tsx (99%) rename mobile-app/app/(tabs)/{(settings) => settings}/_layout.tsx (100%) rename mobile-app/app/(tabs)/{(settings) => settings}/auto-lock.tsx (100%) rename mobile-app/app/(tabs)/{(settings) => settings}/index.tsx (98%) rename mobile-app/app/(tabs)/{(settings) => settings}/ios-autofill.tsx (100%) rename mobile-app/app/(tabs)/{(settings) => settings}/vault-unlock.tsx (100%) diff --git a/mobile-app/app.json b/mobile-app/app.json index e5d0cebd9..2556b38fd 100644 --- a/mobile-app/app.json +++ b/mobile-app/app.json @@ -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" diff --git a/mobile-app/app/(tabs)/_layout.tsx b/mobile-app/app/(tabs)/_layout.tsx index 32597f73a..17de12bde 100644 --- a/mobile-app/app/(tabs)/_layout.tsx +++ b/mobile-app/app/(tabs)/_layout.tsx @@ -57,21 +57,21 @@ export default function TabLayout() { }), }}> , }} /> , }} /> ( diff --git a/mobile-app/app/(tabs)/(credentials)/[id].tsx b/mobile-app/app/(tabs)/credentials/[id].tsx similarity index 100% rename from mobile-app/app/(tabs)/(credentials)/[id].tsx rename to mobile-app/app/(tabs)/credentials/[id].tsx diff --git a/mobile-app/app/(tabs)/(credentials)/_layout.tsx b/mobile-app/app/(tabs)/credentials/_layout.tsx similarity index 100% rename from mobile-app/app/(tabs)/(credentials)/_layout.tsx rename to mobile-app/app/(tabs)/credentials/_layout.tsx diff --git a/mobile-app/app/(tabs)/(credentials)/add-edit.tsx b/mobile-app/app/(tabs)/credentials/add-edit.tsx similarity index 81% rename from mobile-app/app/(tabs)/(credentials)/add-edit.tsx rename to mobile-app/app/(tabs)/credentials/add-edit.tsx index 81ab967c5..6a44b4303 100644 --- a/mobile-app/app/(tabs)/(credentials)/add-edit.tsx +++ b/mobile-app/app/(tabs)/credentials/add-edit.tsx @@ -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: () => ( + router.back()} + style={{ padding: 10, paddingLeft: 0 }} + > + Cancel + + ), + headerRight: () => ( + router.back()} - style={{ padding: 10, paddingLeft: 0 }} + onPress={handleSave} + style={{ padding: 10, paddingRight: 0 }} > - Cancel + - ), - headerRight: () => ( - - - - - - ), - }); - }, [navigation, credential, mode]); + + ), + }); + }, [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 ( diff --git a/mobile-app/app/(tabs)/(credentials)/email/[id].tsx b/mobile-app/app/(tabs)/credentials/email/[id].tsx similarity index 92% rename from mobile-app/app/(tabs)/(credentials)/email/[id].tsx rename to mobile-app/app/(tabs)/credentials/email/[id].tsx index 2c2601ae8..d52947b4c 100644 --- a/mobile-app/app/(tabs)/(credentials)/email/[id].tsx +++ b/mobile-app/app/(tabs)/credentials/email/[id].tsx @@ -1,5 +1,5 @@ import React from 'react'; -import EmailDetailsScreen from '../../(emails)/[id]'; +import EmailDetailsScreen from '../../emails/[id]'; /** * CredentialEmailPreviewScreen Component diff --git a/mobile-app/app/(tabs)/(credentials)/index.tsx b/mobile-app/app/(tabs)/credentials/index.tsx similarity index 98% rename from mobile-app/app/(tabs)/(credentials)/index.tsx rename to mobile-app/app/(tabs)/credentials/index.tsx index a38b4d6ac..efdb6b5d2 100644 --- a/mobile-app/app/(tabs)/(credentials)/index.tsx +++ b/mobile-app/app/(tabs)/credentials/index.tsx @@ -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 diff --git a/mobile-app/app/(tabs)/(emails)/[id].tsx b/mobile-app/app/(tabs)/emails/[id].tsx similarity index 99% rename from mobile-app/app/(tabs)/(emails)/[id].tsx rename to mobile-app/app/(tabs)/emails/[id].tsx index 2e5d83e5d..02d3902d3 100644 --- a/mobile-app/app/(tabs)/(emails)/[id].tsx +++ b/mobile-app/app/(tabs)/emails/[id].tsx @@ -168,7 +168,7 @@ export default function EmailDetailsScreen() { const handleOpenCredential = () => { if (associatedCredential) { - router.push(`/(tabs)/(credentials)/${associatedCredential.Id}`); + router.push(`/(tabs)/credentials/${associatedCredential.Id}`); } }; diff --git a/mobile-app/app/(tabs)/(emails)/_layout.tsx b/mobile-app/app/(tabs)/emails/_layout.tsx similarity index 100% rename from mobile-app/app/(tabs)/(emails)/_layout.tsx rename to mobile-app/app/(tabs)/emails/_layout.tsx diff --git a/mobile-app/app/(tabs)/(emails)/index.tsx b/mobile-app/app/(tabs)/emails/index.tsx similarity index 99% rename from mobile-app/app/(tabs)/(emails)/index.tsx rename to mobile-app/app/(tabs)/emails/index.tsx index a3fe3ae98..3e4a14ad5 100644 --- a/mobile-app/app/(tabs)/(emails)/index.tsx +++ b/mobile-app/app/(tabs)/emails/index.tsx @@ -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 }); diff --git a/mobile-app/app/(tabs)/(settings)/_layout.tsx b/mobile-app/app/(tabs)/settings/_layout.tsx similarity index 100% rename from mobile-app/app/(tabs)/(settings)/_layout.tsx rename to mobile-app/app/(tabs)/settings/_layout.tsx diff --git a/mobile-app/app/(tabs)/(settings)/auto-lock.tsx b/mobile-app/app/(tabs)/settings/auto-lock.tsx similarity index 100% rename from mobile-app/app/(tabs)/(settings)/auto-lock.tsx rename to mobile-app/app/(tabs)/settings/auto-lock.tsx diff --git a/mobile-app/app/(tabs)/(settings)/index.tsx b/mobile-app/app/(tabs)/settings/index.tsx similarity index 98% rename from mobile-app/app/(tabs)/(settings)/index.tsx rename to mobile-app/app/(tabs)/settings/index.tsx index 67a046abf..57fe26406 100644 --- a/mobile-app/app/(tabs)/(settings)/index.tsx +++ b/mobile-app/app/(tabs)/settings/index.tsx @@ -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({ diff --git a/mobile-app/app/(tabs)/(settings)/ios-autofill.tsx b/mobile-app/app/(tabs)/settings/ios-autofill.tsx similarity index 100% rename from mobile-app/app/(tabs)/(settings)/ios-autofill.tsx rename to mobile-app/app/(tabs)/settings/ios-autofill.tsx diff --git a/mobile-app/app/(tabs)/(settings)/vault-unlock.tsx b/mobile-app/app/(tabs)/settings/vault-unlock.tsx similarity index 100% rename from mobile-app/app/(tabs)/(settings)/vault-unlock.tsx rename to mobile-app/app/(tabs)/settings/vault-unlock.tsx diff --git a/mobile-app/app/sync.tsx b/mobile-app/app/sync.tsx index a78e60bcb..e997527b9 100644 --- a/mobile-app/app/sync.tsx +++ b/mobile-app/app/sync.tsx @@ -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; } diff --git a/mobile-app/app/unlock.tsx b/mobile-app/app/unlock.tsx index e39dd15ae..4eaf4c8bf 100644 --- a/mobile-app/app/unlock.tsx +++ b/mobile-app/app/unlock.tsx @@ -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.'); diff --git a/mobile-app/components/CredentialCard.tsx b/mobile-app/components/CredentialCard.tsx index dbd388b5e..5241bc823 100644 --- a/mobile-app/components/CredentialCard.tsx +++ b/mobile-app/components/CredentialCard.tsx @@ -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} > diff --git a/mobile-app/components/EmailCard.tsx b/mobile-app/components/EmailCard.tsx index 5d0b07a9d..733b493b0 100644 --- a/mobile-app/components/EmailCard.tsx +++ b/mobile-app/components/EmailCard.tsx @@ -79,7 +79,7 @@ export function EmailCard({ email }: EmailCardProps) { return ( router.push(`/(tabs)/(emails)/${email.id}`)} + onPress={() => router.push(`/(tabs)/emails/${email.id}`)} activeOpacity={0.7} > diff --git a/mobile-app/components/credentialDetails/EmailPreview.tsx b/mobile-app/components/credentialDetails/EmailPreview.tsx index 193fa833f..ffe28595d 100644 --- a/mobile-app/components/credentialDetails/EmailPreview.tsx +++ b/mobile-app/components/credentialDetails/EmailPreview.tsx @@ -193,7 +193,7 @@ export const EmailPreview: React.FC = ({ 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}`); } }} > diff --git a/mobile-app/ios/Autofill/CredentialProviderViewController.swift b/mobile-app/ios/Autofill/CredentialProviderViewController.swift index e8fdc10d9..92ebe2cff 100644 --- a/mobile-app/ios/Autofill/CredentialProviderViewController.swift +++ b/mobile-app/ios/Autofill/CredentialProviderViewController.swift @@ -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 } } diff --git a/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift b/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift index 8e00fbea9..ee17cd232 100644 --- a/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift +++ b/mobile-app/ios/VaultUI/Views/CredentialProviderView.swift @@ -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