Compare commits

...

15 Commits

Author SHA1 Message Date
Leendert de Borst
edde5eb825 Bump Android app version to include new autofill fixes (#1332) 2025-11-05 22:21:05 +01:00
Leendert de Borst
7b45d7d652 Improve android autofill matching logic for common usecases (#1332) 2025-11-05 22:19:08 +01:00
Leendert de Borst
005d56f7e4 Add new tests to all autofill credential match/filter logic methods (#1332) 2025-11-05 21:44:54 +01:00
Leendert de Borst
3d3d269514 Tweak android autofill matching logic so all tests pass (#1332) 2025-11-05 21:04:41 +01:00
Leendert de Borst
d8bff8674a Improve Android autofill matching to prevent android packages resulting in false positives (#1332) 2025-11-05 20:38:42 +01:00
Leendert de Borst
f0d278bb93 Add show search title option to Android autofill (#1332) 2025-11-05 20:03:47 +01:00
Leendert de Borst
fa085207cf Update Android autofill to properly detect email type fiels (#1332) 2025-11-05 19:16:47 +01:00
Leendert de Borst
d0c97fc09e Update Android credential provider label (#1332) 2025-11-04 11:47:22 +01:00
Leendert de Borst
224574261b Add Android build script (#1332) 2025-11-04 11:03:44 +01:00
Leendert de Borst
d748ec1134 Bump app build number for unlock screen animation fix (#1332) 2025-11-04 10:52:38 +01:00
Leendert de Borst
f649b18c90 Update unlock loading animation position (#1332) 2025-11-04 10:51:31 +01:00
Leendert de Borst
61bbbfab9f Update GitHub workflow Android gradlew memory (#1332) 2025-11-03 23:50:09 +01:00
Leendert de Borst
227aabe873 Update build-and-submit scripts (#1332) 2025-11-03 23:26:15 +01:00
Leendert de Borst
1f97c65e6b Add iOS fastlane CLI build and submit script (#1332) 2025-11-03 22:46:51 +01:00
Leendert de Borst
fdcb607dba Bump version to 0.24.0 stable (#1332) 2025-11-03 22:45:50 +01:00
37 changed files with 978 additions and 64 deletions

View File

@@ -44,6 +44,18 @@ runs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Configure Gradle JVM memory for CI
run: |
mkdir -p android
cat >> android/gradle.properties <<EOF
org.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.daemon.performance.disable-logging=true
org.gradle.daemon=true
org.gradle.caching=true
EOF
shell: bash
working-directory: apps/mobile-app
- name: Build JS bundle (Expo)
run: |
mkdir -p build

3
.gitignore vendored
View File

@@ -431,3 +431,6 @@ temp
# Android keystore file (for publishing to Google Play)
*.keystore
# Safari extension build files
apps/browser-extension/safari-xcode/AliasVault/build

View File

@@ -1 +1 @@
-beta

View File

@@ -1 +1 @@
0.24.0-beta
0.24.0

View File

@@ -463,7 +463,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -495,7 +495,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "AliasVault Extension/AliasVault_Extension.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -531,7 +531,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -570,7 +570,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env bash
BUNDLE_ID="net.aliasvault.safari.extension"
# Build settings
SCHEME="AliasVault"
PROJECT="AliasVault.xcodeproj"
CONFIG="Release"
ARCHIVE_PATH="$PWD/build/${SCHEME}.xcarchive"
EXPORT_DIR="$PWD/build/export"
EXPORT_PLIST="$PWD/exportOptions.plist"
# Put the fastlane API key in the home directory
API_KEY_PATH="$HOME/APPSTORE_CONNECT_FASTLANE.json"
# ------------------------------------------
if [ ! -f "$API_KEY_PATH" ]; then
echo "❌ API key file '$API_KEY_PATH' does not exist. Please provide the App Store Connect API key at this path."
exit 1
fi
# ------------------------------------------
# Shared function to extract version info
# ------------------------------------------
extract_version_info() {
local pkg_path="$1"
# For .pkg files, we need to expand and find the Info.plist
local temp_dir=$(mktemp -d -t aliasvault-pkg-extract)
trap "rm -rf '$temp_dir'" EXIT
# Expand the pkg to find the app bundle
pkgutil --expand "$pkg_path" "$temp_dir/expanded" 2>/dev/null
# Find the payload and extract it
local payload=$(find "$temp_dir/expanded" -name "Payload" | head -n 1)
if [ -n "$payload" ]; then
mkdir -p "$temp_dir/contents"
cd "$temp_dir/contents"
cat "$payload" | gunzip -dc | cpio -i 2>/dev/null
# Find Info.plist in the extracted contents
local info_plist=$(find "$temp_dir/contents" -name "Info.plist" -path "*/Contents/Info.plist" | head -n 1)
if [ -n "$info_plist" ]; then
# Read version and build from the plist
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$info_plist" 2>/dev/null)
BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$info_plist" 2>/dev/null)
if [ -n "$VERSION" ] && [ -n "$BUILD" ]; then
return 0
fi
fi
fi
# Fallback: try to read from the archive directly if it's in a known location
local archive_plist="$ARCHIVE_PATH/Info.plist"
if [ -f "$archive_plist" ]; then
VERSION=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleShortVersionString" "$archive_plist" 2>/dev/null)
BUILD=$(/usr/libexec/PlistBuddy -c "Print :ApplicationProperties:CFBundleVersion" "$archive_plist" 2>/dev/null)
if [ -n "$VERSION" ] && [ -n "$BUILD" ]; then
return 0
fi
fi
echo "❌ Could not extract version info from package"
exit 1
}
# ------------------------------------------
# Ask if user wants to build or use existing
# ------------------------------------------
echo ""
echo "What do you want to do?"
echo " 1) Build and submit to App Store"
echo " 2) Build only"
echo " 3) Submit existing PKG to App Store"
echo ""
read -p "Enter choice (1, 2, or 3): " -r CHOICE
echo ""
# ------------------------------------------
# Build PKG (for options 1 and 2)
# ------------------------------------------
if [[ $CHOICE == "1" || $CHOICE == "2" ]]; then
echo "Building browser extension..."
cd ../..
npm run build:safari
cd safari-xcode/AliasVault
echo "Building PKG..."
# Clean + archive
xcodebuild \
-project "$PROJECT" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-archivePath "$ARCHIVE_PATH" \
clean archive \
-allowProvisioningUpdates
# Export .pkg
rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportOptionsPlist "$EXPORT_PLIST" \
-exportPath "$EXPORT_DIR" \
-allowProvisioningUpdates
PKG_PATH=$(ls "$EXPORT_DIR"/*.pkg)
# Extract version info from newly built PKG
extract_version_info "$PKG_PATH"
echo "PKG built at: $PKG_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
# Exit if build-only
if [[ $CHOICE == "2" ]]; then
echo "✅ Build complete. Exiting."
exit 0
fi
fi
# ------------------------------------------
# Submit to App Store (for options 1 and 3)
# ------------------------------------------
if [[ $CHOICE == "3" ]]; then
# Use existing PKG
PKG_PATH="$EXPORT_DIR/AliasVault.pkg"
if [ ! -f "$PKG_PATH" ]; then
echo "❌ PKG file not found at: $PKG_PATH"
exit 1
fi
# Extract version info from existing PKG
extract_version_info "$PKG_PATH"
echo "Using existing PKG: $PKG_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
fi
if [[ $CHOICE != "1" && $CHOICE != "3" ]]; then
echo "❌ Invalid choice. Please enter 1, 2, or 3."
exit 1
fi
echo ""
echo "================================================"
echo "Submitting to App Store:"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo "================================================"
echo ""
read -p "Are you sure you want to push this to App Store? (y/n): " -r
echo ""
if [[ ! $REPLY =~ ^([Yy]([Ee][Ss])?|[Yy])$ ]]; then
echo "❌ Submission cancelled"
exit 1
fi
echo "✅ Proceeding with upload..."
fastlane deliver \
--pkg "$PKG_PATH" \
--skip_screenshots \
--skip_metadata \
--api_key_path "$API_KEY_PATH" \
--run_precheck_before_submit=false

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>signingStyle</key>
<string>automatic</string>
<key>destination</key>
<string>export</string>
<key>stripSwiftSymbols</key>
<true/>
<key>compileBitcode</key>
<true/>
<key>manageAppVersionAndBuildNumber</key>
<false/>
</dict>
</plist>

View File

@@ -292,7 +292,31 @@ describe('Filter - Credential URL Matching', () => {
expect(matches[0].ServiceName).toBe('Reddit');
});
// [#20] - Test multi-part TLDs like .com.au don't match incorrectly
/**
* [#20] - Test reversed domain (Android package name) doesn't match on TLD
* Note: Android package name filtering is not applicable to browser extensions.
* This test is included for consistency with Android and iOS test suites but is skipped.
*/
it.skip('should not match credentials based on TLD when filtering reversed domains', () => {
/**
* Android package name detection is not implemented in browser extensions
* since they only deal with web URLs, not Android app contexts.
*/
});
/**
* [#21] - Test Android package names are properly detected and handled
* Note: Android package name filtering is not applicable to browser extensions.
* This test is included for consistency with Android and iOS test suites but is skipped.
*/
it.skip('should properly handle Android package names in filtering', () => {
/**
* Android package name detection is not implemented in browser extensions
* since they only deal with web URLs, not Android app contexts.
*/
});
// [#22] - Test multi-part TLDs like .com.au don't match incorrectly
it('should handle multi-part TLDs correctly without false matches', () => {
// Create test data with different .com.au domains
const australianCredentials = [

View File

@@ -6,7 +6,7 @@ export class AppInfo {
/**
* The current extension version. This should be updated with each release of the extension.
*/
public static readonly VERSION = '0.24.0-beta';
public static readonly VERSION = '0.24.0';
/**
* The API version to send to the server (base semver without stage suffixes).

View File

@@ -93,8 +93,8 @@ android {
applicationId 'net.aliasvault.app'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2400301
versionName "0.24.0-beta"
versionCode 2400902
versionName "0.24.0"
}
signingConfigs {
debug {

View File

@@ -80,7 +80,6 @@ class AutofillService : AutofillService() {
safeCallback()
return
}
launchActivityForAutofill(fieldFinder) { response -> safeCallback(response) }
} catch (e: Exception) {
Log.e(TAG, "Unexpected error in onFillRequest", e)
@@ -96,8 +95,12 @@ class AutofillService : AutofillService() {
val responseBuilder = FillResponse.Builder()
// Add debug dataset showing what we're searching for
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
// Add debug dataset if enabled in settings
val sharedPreferences = getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
val showSearchText = sharedPreferences.getBoolean("autofill_show_search_text", false)
if (showSearchText) {
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
}
// Add failed to retrieve dataset
responseBuilder.addDataset(createFailedToRetrieveDataset(fieldFinder))
@@ -169,8 +172,12 @@ class AutofillService : AutofillService() {
val responseBuilder = FillResponse.Builder()
// Always add debug dataset as first option showing what we're searching for
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
// Add debug dataset if enabled in settings
val sharedPreferences = getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
val showSearchText = sharedPreferences.getBoolean("autofill_show_search_text", false)
if (showSearchText) {
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
}
// If there are no results, return "no matches" placeholder option.
if (filteredCredentials.isEmpty()) {
@@ -186,6 +193,11 @@ class AutofillService : AutofillService() {
createCredentialDataset(fieldFinder, credential),
)
}
// Add "Open app" option at the bottom (when search text is not shown and there are matches)
if (!showSearchText) {
responseBuilder.addDataset(createOpenAppDataset(fieldFinder))
}
}
callback(responseBuilder.build())
@@ -193,7 +205,11 @@ class AutofillService : AutofillService() {
Log.e(TAG, "Error parsing credentials", e)
// Show "Failed to retrieve, open app" option instead of failing
val responseBuilder = FillResponse.Builder()
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
val sharedPreferences = getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
val showSearchText = sharedPreferences.getBoolean("autofill_show_search_text", false)
if (showSearchText) {
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
}
responseBuilder.addDataset(createFailedToRetrieveDataset(fieldFinder))
callback(responseBuilder.build())
}
@@ -203,7 +219,11 @@ class AutofillService : AutofillService() {
Log.e(TAG, "Error getting credentials", e)
// Show "Failed to retrieve, open app" option instead of failing
val responseBuilder = FillResponse.Builder()
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
val sharedPreferences = getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
val showSearchText = sharedPreferences.getBoolean("autofill_show_search_text", false)
if (showSearchText) {
responseBuilder.addDataset(createSearchDebugDataset(fieldFinder, appInfo ?: "unknown"))
}
responseBuilder.addDataset(createFailedToRetrieveDataset(fieldFinder))
callback(responseBuilder.build())
}
@@ -258,7 +278,7 @@ class AutofillService : AutofillService() {
}
}
FieldType.EMAIL -> {
if (credential.alias?.email != null) {
if (credential.alias?.email != null && credential.alias.email.isNotEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.alias.email),
@@ -269,7 +289,7 @@ class AutofillService : AutofillService() {
} else if (!credential.username.isNullOrEmpty()) {
presentationDisplayValue += " (${credential.username})"
}
} else if (credential.username != null) {
} else if (!credential.username.isNullOrEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.username),
@@ -283,7 +303,7 @@ class AutofillService : AutofillService() {
}
}
FieldType.USERNAME -> {
if (credential.username != null) {
if (!credential.username.isNullOrEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.username),
@@ -294,7 +314,7 @@ class AutofillService : AutofillService() {
} else if ((credential.alias?.email ?: "").isNotEmpty()) {
presentationDisplayValue += " (${credential.alias?.email})"
}
} else if (credential.alias?.email != null) {
} else if (credential.alias?.email != null && credential.alias.email.isNotEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.alias.email),
@@ -307,7 +327,7 @@ class AutofillService : AutofillService() {
}
else -> {
// For unknown field types, try both email and username
if (credential.alias?.email != null) {
if (credential.alias?.email != null && credential.alias.email.isNotEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.alias.email),
@@ -316,7 +336,7 @@ class AutofillService : AutofillService() {
if (credential.alias.email.isNotEmpty()) {
presentationDisplayValue += " (${credential.alias.email})"
}
} else if (credential.username != null) {
} else if (!credential.username.isNullOrEmpty()) {
dataSetBuilder.setValue(
field.first,
AutofillValue.forText(credential.username),
@@ -530,4 +550,47 @@ class AutofillService : AutofillService() {
return dataSetBuilder.build()
}
/**
* Create a dataset for the "open app" option.
* @param fieldFinder The field finder
* @return The dataset
*/
private fun createOpenAppDataset(fieldFinder: FieldFinder): Dataset {
// Create presentation for the "open app" option with AliasVault logo
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
presentation.setTextViewText(
R.id.text,
getString(R.string.autofill_open_app),
)
val dataSetBuilder = Dataset.Builder(presentation)
// Create deep link URL to open the credentials page
val appInfo = fieldFinder.getAppInfo()
val encodedUrl = appInfo?.let { java.net.URLEncoder.encode(it, "UTF-8") } ?: ""
val deepLinkUrl = "net.aliasvault.app://credentials?serviceUrl=$encodedUrl"
// Add a click listener to open AliasVault app with deep link
val intent = Intent(Intent.ACTION_VIEW).apply {
data = android.net.Uri.parse(deepLinkUrl)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
this@AutofillService,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
// Add placeholder values to satisfy Android's requirement that at least one value must be set
if (fieldFinder.autofillableFields.isNotEmpty()) {
for (field in fieldFinder.autofillableFields) {
dataSetBuilder.setValue(field.first, AutofillValue.forText(""))
}
}
return dataSetBuilder.build()
}
}

View File

@@ -8,6 +8,46 @@ import net.aliasvault.app.vaultstore.models.Credential
*/
object CredentialMatcher {
/**
* Common top-level domains (TLDs) that should be excluded from matching.
* This prevents false matches when dealing with reversed domain names (App package names).
*/
private val commonTlds = setOf(
// Generic TLDs
"com", "net", "org", "edu", "gov", "mil", "int",
// Country code TLDs
"nl", "de", "uk", "fr", "it", "es", "pl", "be", "ch", "at", "se", "no", "dk", "fi",
"pt", "gr", "cz", "hu", "ro", "bg", "hr", "sk", "si", "lt", "lv", "ee", "ie", "lu",
"us", "ca", "mx", "br", "ar", "cl", "co", "ve", "pe", "ec",
"au", "nz", "jp", "cn", "in", "kr", "tw", "hk", "sg", "my", "th", "id", "ph", "vn",
"za", "eg", "ng", "ke", "ug", "tz", "ma",
"ru", "ua", "by", "kz", "il", "tr", "sa", "ae", "qa", "kw",
// New gTLDs (common ones)
"app", "dev", "io", "ai", "tech", "shop", "store", "online", "site", "website",
"blog", "news", "media", "tv", "video", "music", "pro", "info", "biz", "name",
)
/**
* Check if a string is likely an App package name (reversed domain).
* App package names start with TLD followed by dot (e.g., "com.example", "nl.app").
* @param text The text to check
* @return True if it looks like an App package name
*/
private fun isAppPackageName(text: String): Boolean {
if (!text.contains(".")) {
return false
}
if (text.startsWith("http://") || text.startsWith("https://")) {
return false
}
val firstPart = text.substringBefore(".").lowercase()
// Check if first part is a common TLD - this indicates reversed domain (package name)
return commonTlds.contains(firstPart)
}
/**
* Extract domain from URL, handling both full URLs and partial domains.
* @param urlString URL or domain string
@@ -20,10 +60,17 @@ object CredentialMatcher {
var domain = urlString.lowercase().trim()
// Remove protocol if present
// Check if it starts with a protocol
val hasProtocol = domain.startsWith("http://") || domain.startsWith("https://")
// Remove protocol if present
// If no protocol and starts with TLD + dot, it's likely an App package name
// Return empty string to indicate that domain extraction has failed for this string as
// this is most likely not a real domain that the caller expects
if (!hasProtocol && isAppPackageName(domain)) {
return ""
}
if (hasProtocol) {
domain = domain.replace("https://", "").replace("http://", "")
}
@@ -146,6 +193,9 @@ object CredentialMatcher {
val d1 = extractDomain(domain1)
val d2 = extractDomain(domain2)
// If either extracted domain is empty, early return false.
if (d1.isEmpty() || d2.isEmpty()) return false
// Exact match
if (d1 == d2) return true
@@ -170,8 +220,8 @@ object CredentialMatcher {
}
return text.lowercase()
// Replace common separators and punctuation with spaces
.replace(Regex("[|,;:\\-–—/\\\\()\\[\\]{}'\" ~!@#$%^&*+=<>?]"), " ")
// Replace common separators and punctuation with spaces (including dots)
.replace(Regex("[|,;:\\-–—/\\\\()\\[\\]{}'\" ~!@#$%^&*+=<>?.]"), " ")
.split(Regex("\\s+"))
.filter { word ->
word.length > 3 // Filter out short words
@@ -196,12 +246,30 @@ object CredentialMatcher {
return credentials
}
// Try to parse as URL first
val matches = mutableSetOf<Credential>()
val searchDomain = extractDomain(searchText)
if (searchDomain.isNotEmpty()) {
val matches = mutableSetOf<Credential>()
// Try to parse as App package name first.
if (isAppPackageName(searchText)) {
// Is most likely app package name, do a simple exact match search on URL field
credentials.forEach { credential ->
val serviceUrl = credential.service.url
if (!serviceUrl.isNullOrEmpty()) {
if (searchText == serviceUrl) {
matches.add(credential)
}
}
}
}
// If app package name results in matches, return them immediately.
if (matches.isNotEmpty()) {
return matches.toList()
}
// Try URL second
if (searchDomain.isNotEmpty()) {
// Check for domain matches with priority
credentials.forEach { credential ->
val serviceUrl = credential.service.url

View File

@@ -35,9 +35,9 @@ class FieldFinder(var structure: AssistStructure) {
var foundPasswordField = false
/**
* Whether a username field has been found.
* List of undetected editable fields (not identified as password/username/email).
*/
var lastField: AutofillId? = null
private val unknownFields = mutableListOf<AutofillId>()
/**
* Parse the structure.
@@ -49,6 +49,15 @@ class FieldFinder(var structure: AssistStructure) {
val rootNode = windowNode.rootViewNode
parseNode(rootNode)
}
// If only a password field was found, but there is exactly one other undetected field,
// assume it's the username/email field
if (foundPasswordField && !foundUsernameField && unknownFields.size == 1) {
val unknownFieldId = unknownFields.first()
Log.d(TAG, "Found password field without username - promoting unknown field to USERNAME type")
autofillableFields.add(Pair(unknownFieldId, FieldType.USERNAME))
foundUsernameField = true
}
}
/**
@@ -81,7 +90,7 @@ class FieldFinder(var structure: AssistStructure) {
/**
* Determines if a field is most likely an email field, username field, password field, or unknown.
*/
fun determineFieldType(fieldId: AutofillId): FieldType {
private fun determineFieldType(fieldId: AutofillId): FieldType {
// Find the node in the structure
val node = findNodeById(fieldId) ?: return FieldType.UNKNOWN
@@ -221,12 +230,15 @@ class FieldFinder(var structure: AssistStructure) {
if (fieldType == FieldType.PASSWORD) {
foundPasswordField = true
autofillableFields.add(Pair(viewId, fieldType))
Log.d(TAG, "Found PASSWORD field: ${node.idEntry}")
} else if (fieldType == FieldType.USERNAME || fieldType == FieldType.EMAIL) {
foundUsernameField = true
autofillableFields.add(Pair(viewId, fieldType))
Log.d(TAG, "Found ${fieldType.name} field: ${node.idEntry}")
} else {
// Store the last field we saw in case we need it for username detection
lastField = viewId
// Store undetected editable fields for potential username promotion
unknownFields.add(viewId)
Log.d(TAG, "Found UNKNOWN editable field: ${node.idEntry}, hint=${node.hint}, inputType=${node.inputType}")
}
}
@@ -237,14 +249,56 @@ class FieldFinder(var structure: AssistStructure) {
}
}
/**
* Check if a field should be excluded from autofill (e.g., email composition, search fields).
* @param node The node to check
* @return Whether the node should be excluded from autofill
*/
private fun isExcludedField(node: AssistStructure.ViewNode): Boolean {
val idEntry = node.idEntry?.lowercase()
val hint = node.hint?.lowercase()
val contentDescription = node.contentDescription?.toString()?.lowercase()
// Common patterns for non-login fields that should be excluded
val excludePatterns = listOf(
// Email composition fields
"to", "from", "cc", "bcc", "recipient", "compose", "subject",
// Search fields
"search", "query", "find",
// Other non-login contexts
"comment", "reply", "message", "chat", "filter",
)
// Check if any exclude pattern matches
for (pattern in excludePatterns) {
val idEntryContains = idEntry?.contains(pattern, ignoreCase = false) == true
val hintContains = hint?.contains(pattern, ignoreCase = false) == true
val contentContains = contentDescription?.contains(pattern, ignoreCase = false) == true
if (idEntryContains || hintContains || contentContains) {
return true
}
}
return false
}
/**
* Check if a node is an email field.
* @param node The node to check
* @return Whether the node is an email field
*/
private fun isEmailField(node: AssistStructure.ViewNode): Boolean {
// Check input type for email
if ((node.inputType and android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS) != 0) {
// Exclude non-login email fields (like "To" in email clients)
if (isExcludedField(node)) {
return false
}
// Check input type for email - mask to get only the variation bits
val inputType = node.inputType
if ((inputType and android.text.InputType.TYPE_MASK_CLASS) == android.text.InputType.TYPE_CLASS_TEXT &&
(inputType and android.text.InputType.TYPE_MASK_VARIATION) == android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
) {
return true
}
@@ -294,6 +348,11 @@ class FieldFinder(var structure: AssistStructure) {
* @return Whether the node is a username field
*/
private fun isUsernameField(node: AssistStructure.ViewNode): Boolean {
// Exclude non-login fields
if (isExcludedField(node)) {
return false
}
val searchTerms = listOf("username", "user")
// Check autofill hints

View File

@@ -845,6 +845,39 @@ class NativeVaultManager(reactContext: ReactApplicationContext) :
}
}
/**
* Get the autofill show search text setting.
* @param promise The promise to resolve with boolean result
*/
@ReactMethod
override fun getAutofillShowSearchText(promise: Promise) {
try {
val sharedPreferences = reactApplicationContext.getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
val showSearchText = sharedPreferences.getBoolean("autofill_show_search_text", false)
promise.resolve(showSearchText)
} catch (e: Exception) {
Log.e(TAG, "Error getting autofill show search text setting", e)
promise.reject("ERR_GET_AUTOFILL_SETTING", "Failed to get autofill show search text setting: ${e.message}", e)
}
}
/**
* Set the autofill show search text setting.
* @param showSearchText Whether to show search text in autofill
* @param promise The promise to resolve
*/
@ReactMethod
override fun setAutofillShowSearchText(showSearchText: Boolean, promise: Promise) {
try {
val sharedPreferences = reactApplicationContext.getSharedPreferences("AliasVaultPrefs", android.content.Context.MODE_PRIVATE)
sharedPreferences.edit().putBoolean("autofill_show_search_text", showSearchText).apply()
promise.resolve(null)
} catch (e: Exception) {
Log.e(TAG, "Error setting autofill show search text setting", e)
promise.reject("ERR_SET_AUTOFILL_SETTING", "Failed to set autofill show search text setting: ${e.message}", e)
}
}
/**
* Get the current fragment activity.
* @return The fragment activity

View File

@@ -2,7 +2,7 @@
<!-- Credential Provider Configuration for AliasVault -->
<credential-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:settingsSubtitle="Password, Passkeys &amp; Aliases">
android:settingsSubtitle="Passwords, Passkeys &amp; Aliases">
<capabilities>
<!-- Support for passkeys (WebAuthn public key credentials) -->
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />

View File

@@ -295,7 +295,62 @@ class AutofillTest {
assertEquals("Reddit", matches[0].service.name)
}
// [#20] - Test multi-part TLDs like .com.au don't match incorrectly
// [#20] - Test reversed domain (App package name) doesn't match on TLD
@Test
fun testReversedDomainTldCheck() {
// Test that dumpert.nl credential doesn't match nl.marktplaats.android package
// They both contain "nl" in the name but shouldn't match since "nl" is just a TLD
val reversedDomainCredentials = listOf(
createTestCredential("Dumpert.nl", "", "user@dumpert.nl"),
createTestCredential("Marktplaats.nl", "", "user@marktplaats.nl"),
)
val matches = CredentialMatcher.filterCredentialsByAppInfo(
reversedDomainCredentials,
"nl.marktplaats.android",
)
// Should only match Marktplaats, not Dumpert (even though both have "nl")
assertEquals(1, matches.size)
assertEquals("Marktplaats.nl", matches[0].service.name)
}
// [#21] - Test App package names are properly detected and handled
@Test
fun testAppPackageNameDetection() {
val packageCredentials = listOf(
createTestCredential("Google App", "com.google.android.googlequicksearchbox", "user@google.com"),
createTestCredential("Facebook", "com.facebook.katana", "user@facebook.com"),
createTestCredential("WhatsApp", "com.whatsapp", "user@whatsapp.com"),
createTestCredential("Generic Site", "example.com", "user@example.com"),
)
// Test com.google.android package matches
val googleMatches = CredentialMatcher.filterCredentialsByAppInfo(
packageCredentials,
"com.google.android.googlequicksearchbox",
)
assertEquals(1, googleMatches.size)
assertEquals("Google App", googleMatches[0].service.name)
// Test com.facebook package matches
val facebookMatches = CredentialMatcher.filterCredentialsByAppInfo(
packageCredentials,
"com.facebook.katana",
)
assertEquals(1, facebookMatches.size)
assertEquals("Facebook", facebookMatches[0].service.name)
// Test that web domain doesn't match package name
val webMatches = CredentialMatcher.filterCredentialsByAppInfo(
packageCredentials,
"https://example.com",
)
assertEquals(1, webMatches.size)
assertEquals("Generic Site", webMatches[0].service.name)
}
// [#22] - Test multi-part TLDs like .com.au don't match incorrectly
@Test
fun testMultiPartTldNoFalseMatches() {
// Create test data with different .com.au domains

View File

@@ -0,0 +1,5 @@
# Build Android app in release mode
./gradlew bundleRelease
# Open directory that should contain the .aab file if build was successful
open app/build/outputs/bundle/release

View File

@@ -20,7 +20,7 @@ complexity:
thresholdInObjects: 60
CyclomaticComplexMethod:
active: true
threshold: 30
threshold: 40
NestedBlockDepth:
active: true
threshold: 5

View File

@@ -2,7 +2,7 @@
"expo": {
"name": "AliasVault",
"slug": "AliasVault",
"version": "0.24.0-beta",
"version": "0.24.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "net.aliasvault.app",

View File

@@ -1,6 +1,7 @@
import { router } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { StyleSheet, View, TouchableOpacity, Linking } from 'react-native';
import { StyleSheet, View, TouchableOpacity, Linking, Switch } from 'react-native';
import { useState, useEffect } from 'react';
import { useColors } from '@/hooks/useColorScheme';
@@ -17,6 +18,23 @@ export default function AndroidAutofillScreen() : React.ReactNode {
const colors = useColors();
const { t } = useTranslation();
const { markAutofillConfigured, shouldShowAutofillReminder } = useAuth();
const [advancedOptionsExpanded, setAdvancedOptionsExpanded] = useState(false);
const [showSearchText, setShowSearchText] = useState(false);
/**
* Load the show search text setting on mount.
*/
useEffect(() => {
const loadSettings = async () => {
try {
const value = await NativeVaultManager.getAutofillShowSearchText();
setShowSearchText(value);
} catch (err) {
console.warn('Failed to load autofill settings:', err);
}
};
loadSettings();
}, []);
/**
* Handle the configure press.
@@ -45,10 +63,70 @@ export default function AndroidAutofillScreen() : React.ReactNode {
Linking.openURL('https://docs.aliasvault.net/mobile-apps/android/autofill.html');
};
/**
* Handle toggling the show search text setting.
*/
const handleToggleShowSearchText = async (value: boolean) : Promise<void> => {
try {
await NativeVaultManager.setAutofillShowSearchText(value);
setShowSearchText(value);
} catch (err) {
console.warn('Failed to update show search text setting:', err);
}
};
const styles = StyleSheet.create({
advancedOptionsContainer: {
marginTop: 16,
paddingBottom: 16,
},
advancedOptionsDescription: {
color: colors.textMuted,
fontSize: 13,
lineHeight: 18,
marginBottom: 8,
},
advancedOptionsHeader: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
advancedOptionsTitle: {
color: colors.text,
fontSize: 15,
fontWeight: '600',
},
advancedOptionsToggle: {
alignItems: 'center',
backgroundColor: colors.accentBackground,
borderRadius: 8,
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
padding: 16,
},
advancedOptionsToggleContainer: {
flex: 1,
marginRight: 12,
},
advancedOptionsToggleHeader: {
alignItems: 'center',
flexDirection: 'row',
paddingVertical: 8,
},
advancedOptionsToggleText: {
color: colors.text,
fontSize: 15,
fontWeight: '500',
},
buttonContainer: {
padding: 16,
paddingBottom: 32,
paddingBottom: 16,
},
chevron: {
color: colors.textMuted,
fontSize: 20,
},
configureButton: {
alignItems: 'center',
@@ -92,6 +170,31 @@ export default function AndroidAutofillScreen() : React.ReactNode {
fontSize: 16,
fontWeight: '600',
},
settingRow: {
alignItems: 'center',
backgroundColor: colors.accentBackground,
borderRadius: 10,
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
padding: 16,
},
settingRowDescription: {
color: colors.textMuted,
fontSize: 13,
lineHeight: 18,
marginTop: 4,
},
settingRowText: {
color: colors.text,
flex: 1,
marginRight: 12,
},
settingRowTitle: {
color: colors.text,
fontSize: 15,
fontWeight: '500',
},
tipStep: {
color: colors.textMuted,
fontSize: 13,
@@ -174,6 +277,41 @@ export default function AndroidAutofillScreen() : React.ReactNode {
)}
</View>
</View>
<View style={styles.advancedOptionsContainer}>
<TouchableOpacity
style={styles.advancedOptionsToggleHeader}
onPress={() => setAdvancedOptionsExpanded(!advancedOptionsExpanded)}
>
<ThemedText style={styles.advancedOptionsTitle}>
{t('settings.androidAutofillSettings.advancedOptions')}
</ThemedText>
<ThemedText style={styles.chevron}>
{advancedOptionsExpanded ? '▼' : '▶'}
</ThemedText>
</TouchableOpacity>
{advancedOptionsExpanded && (
<View>
<View style={styles.settingRow}>
<View style={styles.settingRowText}>
<ThemedText style={styles.settingRowTitle}>
{t('settings.androidAutofillSettings.showSearchText')}
</ThemedText>
<ThemedText style={styles.settingRowDescription}>
{t('settings.androidAutofillSettings.showSearchTextDescription')}
</ThemedText>
</View>
<Switch
value={showSearchText}
onValueChange={handleToggleShowSearchText}
trackColor={{ false: colors.accentBackground, true: colors.primary }}
thumbColor={colors.primarySurfaceText}
/>
</View>
</View>
)}
</View>
</ThemedScrollView>
</ThemedContainer>
);

View File

@@ -245,7 +245,9 @@ export default function UnlockScreen() : React.ReactNode {
loadingContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
justifyContent: 'flex-start',
paddingHorizontal: 20,
paddingTop: '40%',
},
logoContainer: {
alignItems: 'center',

View File

@@ -224,7 +224,10 @@
"openAutofillSettings": "Open Autofill Settings",
"buttonTip": "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.",
"step2": "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.",
"alreadyConfigured": "I already configured it"
"alreadyConfigured": "I already configured it",
"advancedOptions": "Advanced Options",
"showSearchText": "Show search text",
"showSearchTextDescription": "Include the text AliasVault receives from Android that it uses to search for a matching credential"
},
"vaultUnlock": "Vault Unlock Method",
"autoLock": "Auto-lock Timeout",

View File

@@ -1292,7 +1292,7 @@
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -1333,7 +1333,7 @@
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = AliasVault/AliasVault.entitlements;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
INFOPLIST_FILE = AliasVault/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AliasVault;
@@ -1494,7 +1494,7 @@
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1530,7 +1530,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1564,7 +1564,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1621,7 +1621,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1674,7 +1674,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1727,7 +1727,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1776,7 +1776,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1811,7 +1811,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -1844,7 +1844,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1897,7 +1897,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1946,7 +1946,7 @@
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -1998,7 +1998,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
DYLIB_COMPATIBILITY_VERSION = 1;
@@ -2049,7 +2049,7 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
@@ -2094,7 +2094,7 @@
CODE_SIGN_ENTITLEMENTS = autofill/autofill.entitlements;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2400301;
CURRENT_PROJECT_VERSION = 2400902;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 8PHW4HN3F7;
ENABLE_USER_SCRIPT_SANDBOXING = YES;

View File

@@ -413,6 +413,21 @@ public class VaultManager: NSObject {
}
}
@objc
func getAutofillShowSearchText(_ resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
// iOS autofill doesn't have this feature, always return false
resolve(false)
}
@objc
func setAutofillShowSearchText(_ showSearchText: Bool,
resolver resolve: @escaping RCTPromiseResolveBlock,
rejecter reject: @escaping RCTPromiseRejectBlock) {
// iOS autofill doesn't have this feature, no-op
resolve(nil)
}
@objc
func copyToClipboardWithExpiration(_ text: String,
expirationSeconds: Double,

View File

@@ -215,6 +215,32 @@ final class CredentialFilterTests: XCTestCase {
XCTAssertEqual(ukMatches.first?.service.name, "UK Site")
}
/**
* [#20] - Test reversed domain (Android package name) doesn't match on TLD
* Note: Android package name filtering is not applicable to iOS autofill in the same way.
* This test is included for consistency with Android test suite but is skipped.
*/
func testReversedDomainTldCheck() throws {
/**
* Android package name detection is implemented in Android-specific autofill code.
* iOS uses a different autofill mechanism and doesn't require the same TLD filtering.
*/
throw XCTSkip("Android package name filtering not applicable to iOS autofill")
}
/**
* [#21] - Test Android package names are properly detected and handled
* Note: Android package name filtering is not applicable to iOS autofill in the same way.
* This test is included for consistency with Android test suite but is skipped.
*/
func testAppPackageNameDetection() throws {
/**
* Android package name detection is implemented in Android-specific autofill code.
* iOS uses a different autofill mechanism and doesn't require the same TLD filtering.
*/
throw XCTSkip("Android package name filtering not applicable to iOS autofill")
}
// MARK: - Shared Test Data
/**

View File

@@ -0,0 +1,175 @@
#!/usr/bin/env bash
BUNDLE_ID="net.aliasvault.app"
SCHEME="AliasVault"
WORKSPACE="AliasVault.xcworkspace"
CONFIG="Release"
ARCHIVE_PATH="$PWD/build/${SCHEME}.xcarchive"
EXPORT_DIR="$PWD/build/export"
EXPORT_PLIST="$PWD/exportOptions.plist"
# Put the fastlane API key in the home directory
API_KEY_PATH="$HOME/APPSTORE_CONNECT_FASTLANE.json"
# ------------------------------------------
if [ ! -f "$API_KEY_PATH" ]; then
echo "❌ API key file '$API_KEY_PATH' does not exist. Please provide the App Store Connect API key at this path."
exit 1
fi
# ------------------------------------------
# Shared function to extract version info
# ------------------------------------------
extract_version_info() {
local ipa_path="$1"
# Extract Info.plist to a temporary file
local temp_plist=$(mktemp)
unzip -p "$ipa_path" "Payload/*.app/Info.plist" > "$temp_plist"
# Read version and build from the plist
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$temp_plist")
BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$temp_plist")
# Clean up temp file
rm -f "$temp_plist"
}
# ------------------------------------------
# Ask if user wants to build or use existing
# ------------------------------------------
echo ""
echo "What do you want to do?"
echo " 1) Build and submit to TestFlight"
echo " 2) Build only"
echo " 3) Submit existing IPA to TestFlight"
echo ""
read -p "Enter choice (1, 2, or 3): " -r CHOICE
echo ""
# ------------------------------------------
# Build IPA (for options 1 and 2)
# ------------------------------------------
if [[ $CHOICE == "1" || $CHOICE == "2" ]]; then
echo "Building IPA..."
# Clean + archive
xcodebuild \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIG" \
-archivePath "$ARCHIVE_PATH" \
clean archive \
-allowProvisioningUpdates
# Export .ipa
rm -rf "$EXPORT_DIR"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE_PATH" \
-exportOptionsPlist "$EXPORT_PLIST" \
-exportPath "$EXPORT_DIR" \
-allowProvisioningUpdates
IPA_PATH=$(ls "$EXPORT_DIR"/*.ipa)
# Extract version info from newly built IPA
extract_version_info "$IPA_PATH"
echo "IPA built at: $IPA_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
# Exit if build-only
if [[ $CHOICE == "2" ]]; then
echo "✅ Build complete. Exiting."
exit 0
fi
fi
# ------------------------------------------
# Submit to TestFlight (for options 1 and 3)
# ------------------------------------------
if [[ $CHOICE == "3" ]]; then
# Use existing IPA
IPA_PATH="$EXPORT_DIR/AliasVault.ipa"
if [ ! -f "$IPA_PATH" ]; then
echo "❌ IPA file not found at: $IPA_PATH"
exit 1
fi
# Extract version info from existing IPA
extract_version_info "$IPA_PATH"
echo "Using existing IPA: $IPA_PATH"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo ""
fi
if [[ $CHOICE != "1" && $CHOICE != "3" ]]; then
echo "❌ Invalid choice. Please enter 1, 2, or 3."
exit 1
fi
echo ""
echo "================================================"
echo "Submitting to TestFlight:"
echo " Version: $VERSION"
echo " Build: $BUILD"
echo "================================================"
echo ""
read -p "Are you sure you want to push this to TestFlight? (y/n): " -r
echo ""
if [[ ! $REPLY =~ ^([Yy]([Ee][Ss])?|[Yy])$ ]]; then
echo "❌ Submission cancelled"
exit 1
fi
echo "Checking if build already exists on TestFlight..."
# Get the latest TestFlight build number for this version
set +e
RAW_OUTPUT=$(fastlane run latest_testflight_build_number \
app_identifier:"$BUNDLE_ID" \
version:"$VERSION" \
api_key_path:"$API_KEY_PATH" \
2>&1)
set -e
# Extract the build number from the output
LATEST=$(echo "$RAW_OUTPUT" | grep -oE "Result: [0-9]+" | grep -oE "[0-9]+" | head -n1)
# Check if we got a valid result
if [ -z "$LATEST" ]; then
echo "❌ Failed to get TestFlight build number. Fastlane output:"
echo "$RAW_OUTPUT"
echo ""
echo "This could mean:"
echo " - No builds exist for version $VERSION on TestFlight (first upload)"
echo " - API authentication failed"
echo " - Network/API error"
exit 1
fi
echo "Latest TestFlight build number for version $VERSION: $LATEST"
# Numeric compare - if latest >= current, it's a duplicate
if [ "$LATEST" -ge "$BUILD" ]; then
echo "🚫 Duplicate detected: TestFlight already has $VERSION with build $LATEST (your build: $BUILD)."
exit 1
fi
echo "✅ No duplicate found. Proceeding with deliver..."
fastlane deliver \
--ipa "$IPA_PATH" \
--skip_screenshots \
--skip_metadata \
--api_key_path "$API_KEY_PATH" \
--run_precheck_before_submit=false

View File

@@ -12,5 +12,7 @@
<true/>
<key>compileBitcode</key>
<true/>
<key>manageAppVersionAndBuildNumber</key>
<false/>
</dict>
</plist>

View File

@@ -40,6 +40,8 @@ export interface Spec extends TurboModule {
getAutoLockTimeout(): Promise<number>;
getAuthMethods(): Promise<string[]>;
openAutofillSettingsPage(): Promise<void>;
getAutofillShowSearchText(): Promise<boolean>;
setAutofillShowSearchText(showSearchText: boolean): Promise<void>;
// Clipboard management
clearClipboardAfterDelay(delayInSeconds: number): Promise<void>;

View File

@@ -8,7 +8,7 @@ export class AppInfo {
/**
* The current mobile app version. This should be updated with each release of the mobile app.
*/
public static readonly VERSION = '0.24.0-beta';
public static readonly VERSION = '0.24.0';
/**
* The API version to send to the server (base semver without stage suffixes).

View File

@@ -35,7 +35,7 @@ public static class AppInfo
/// <summary>
/// Gets the version stage (e.g., "", "-alpha", "-beta", "-rc").
/// </summary>
public const string VersionStage = "-beta";
public const string VersionStage = "";
/// <summary>
/// Gets the minimum supported AliasVault client version. Normally the minimum client version is the same

View File

@@ -108,7 +108,7 @@ The GitHub Actions workflow `Browser Extension Build` will build the browser ext
```bash
./gradlew app:bundleRelease
```
2. The resulting .aapb file will be available in the following location.
2. The resulting .aab file will be available in the following location.
```bash
apps/mobile-ap/android/app/build/outputs/bundle/release
```

View File

@@ -74,7 +74,7 @@ cd android
./gradlew app:bundleRelease
```
The resulting .aapb file will be available in:
The resulting .aab file will be available in:
```bash
app/build/outputs/bundle/release

View File

@@ -0,0 +1,6 @@
- Add passkey support
- Add Brazilian Portugese, Russian and Polish languages
- Add image zoom support to attachment previews
- Improve search logic
- Improve dark mode support
- Fix a bug where multiple private domains were not shown correctly in email domain chooser

View File

@@ -0,0 +1,7 @@
- AliasVault ondersteund nu Passkeys
- Nieuwe talen: Braziliaans Portugees, Russisch en Pools
- Ondersteuning voor snelle autofill
- Afbeelding bijlagen kunnen nu worden ingezoomd
- Verbeterde zoekfunctie
- Verbeterde dark mode support
- Bug opgelost waarbij meerdere email domeinen niet goed werden weergegeven in email veld

View File

@@ -0,0 +1,5 @@
- Add passkey support
- Add Brazilian Portugese, Russian and Polish languages
- Improved search logic
- Improved UI for custom URL settings
- Fix a bug where multiple private domains were not shown correctly in email domain chooser

View File

@@ -0,0 +1,7 @@
- Add passkey support
- Add Brazilian Portugese, Russian and Polish languages
- Add quick autofill feature
- Add image zoom support to attachment previews
- Improved search logic
- Tweak layout on iOS 26
- Fix a bug where multiple private domains were not shown correctly in email domain chooser

View File

@@ -0,0 +1,7 @@
- AliasVault ondersteund nu Passkeys
- Nieuwe talen: Braziliaans Portugees, Russisch en Pools
- Ondersteuning voor snelle autofill
- Afbeelding bijlagen kunnen nu worden ingezoomd
- Verbeterde zoekfunctie
- Layout tweaks voor iOS 26
- Bug opgelost waarbij meerdere email domeinen niet goed werden weergegeven in email veld