mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-01-14 17:10:50 -05:00
Compare commits
15 Commits
main
...
1332-prepa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edde5eb825 | ||
|
|
7b45d7d652 | ||
|
|
005d56f7e4 | ||
|
|
3d3d269514 | ||
|
|
d8bff8674a | ||
|
|
f0d278bb93 | ||
|
|
fa085207cf | ||
|
|
d0c97fc09e | ||
|
|
224574261b | ||
|
|
d748ec1134 | ||
|
|
f649b18c90 | ||
|
|
61bbbfab9f | ||
|
|
227aabe873 | ||
|
|
1f97c65e6b | ||
|
|
fdcb607dba |
12
.github/actions/build-android-app/action.yml
vendored
12
.github/actions/build-android-app/action.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
-beta
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.24.0-beta
|
||||
0.24.0
|
||||
|
||||
@@ -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;
|
||||
|
||||
179
apps/browser-extension/safari-xcode/AliasVault/build-and-submit.sh
Executable file
179
apps/browser-extension/safari-xcode/AliasVault/build-and-submit.sh
Executable 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
|
||||
@@ -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>
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<!-- Credential Provider Configuration for AliasVault -->
|
||||
<credential-provider
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:settingsSubtitle="Password, Passkeys & Aliases">
|
||||
android:settingsSubtitle="Passwords, Passkeys & Aliases">
|
||||
<capabilities>
|
||||
<!-- Support for passkeys (WebAuthn public key credentials) -->
|
||||
<capability name="androidx.credentials.TYPE_PUBLIC_KEY_CREDENTIAL" />
|
||||
|
||||
@@ -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
|
||||
|
||||
5
apps/mobile-app/android/build.sh
Executable file
5
apps/mobile-app/android/build.sh
Executable 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
|
||||
@@ -20,7 +20,7 @@ complexity:
|
||||
thresholdInObjects: 60
|
||||
CyclomaticComplexMethod:
|
||||
active: true
|
||||
threshold: 30
|
||||
threshold: 40
|
||||
NestedBlockDepth:
|
||||
active: true
|
||||
threshold: 5
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
175
apps/mobile-app/ios/build-and-submit.sh
Executable file
175
apps/mobile-app/ios/build-and-submit.sh
Executable 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
|
||||
@@ -12,5 +12,7 @@
|
||||
<true/>
|
||||
<key>compileBitcode</key>
|
||||
<true/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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>;
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
6
fastlane/metadata/android/en-US/changelogs/2400900.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/2400900.txt
Normal 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
|
||||
7
fastlane/metadata/android/nl-NL/changelogs/2400900.txt
Normal file
7
fastlane/metadata/android/nl-NL/changelogs/2400900.txt
Normal 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
|
||||
@@ -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
|
||||
7
fastlane/metadata/ios/en-US/changelogs/2400900.txt
Normal file
7
fastlane/metadata/ios/en-US/changelogs/2400900.txt
Normal 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
|
||||
7
fastlane/metadata/ios/nl-NL/changelogs/2400900.txt
Normal file
7
fastlane/metadata/ios/nl-NL/changelogs/2400900.txt
Normal 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
|
||||
Reference in New Issue
Block a user