diff --git a/apps/browser-extension/src/i18n/locales/fi.json b/apps/browser-extension/src/i18n/locales/fi.json index 3d270e5b0..f1bbfaef2 100644 --- a/apps/browser-extension/src/i18n/locales/fi.json +++ b/apps/browser-extension/src/i18n/locales/fi.json @@ -57,7 +57,7 @@ "next": "Seuraava", "use": "Käytä", "delete": "Poista", - "save": "Save", + "save": "Tallenna", "or": "Tai", "close": "Sulje", "copied": "Kopioitu!", @@ -242,13 +242,13 @@ "enterEmailPrefix": "Syötä sähköpostin etuliite" }, "totp": { - "addCode": "Add 2FA Code", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "saveToViewCode": "Save to view code", + "addCode": "Lisää 2FA TOTP -koodi", + "instructions": "Syötä salainen avain, joka näkyy sivustossa, jossa haluat lisätä kaksivaiheisen tunnistautumisen", + "nameOptional": "Nimi (valinnainen)", + "secretKey": "Salainen avain", + "saveToViewCode": "Tallenna nähdäksesi koodin", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Virheellinen salatun avaimen muoto." } }, "emails": { diff --git a/apps/browser-extension/src/i18n/locales/pl.json b/apps/browser-extension/src/i18n/locales/pl.json index ffebabdc6..b816d1c89 100644 --- a/apps/browser-extension/src/i18n/locales/pl.json +++ b/apps/browser-extension/src/i18n/locales/pl.json @@ -57,7 +57,7 @@ "next": "Dalej", "use": "Użyj", "delete": "Usuń", - "save": "Save", + "save": "Zapisz", "or": "lub", "close": "Zamknąć", "copied": "Skopiowano", @@ -238,22 +238,22 @@ "publicEmailDescription": "Anonimowa, ale ograniczona prywatność. Treści e-mail są czytelne dla każdego, kto zna adres.", "useDomainChooser": "Użyj wybierania domen", "enterCustomDomain": "Wprowadź własną domenę", - "enterFullEmail": "Wprowadź pełny adres e-mail", + "enterFullEmail": "Wprowadź adres e-mail", "enterEmailPrefix": "Wprowadź prefiks e-mail" }, "totp": { - "addCode": "Add 2FA Code", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "saveToViewCode": "Save to view code", + "addCode": "Dodaj kod 2FA", + "instructions": "Wprowadź tajny klucz wyświetlony na stronie internetowej, na której chcesz dodać uwierzytelnianie dwuskładnikowe.", + "nameOptional": "Nazwa (opcjonalnie)", + "secretKey": "Tajny klucz", + "saveToViewCode": "Zapisz, aby wyświetlić kod", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Nieprawidłowy format tajnego klucza." } }, "emails": { "title": "Skrzynka odbiorcza", - "deleteEmailTitle": "Usuń adres e-mail", + "deleteEmailTitle": "Usuń e-mail", "deleteEmailConfirm": "Czy na pewno chcesz trwale usunąć ten e-mail?", "from": "Od", "to": "Do", diff --git a/apps/browser-extension/src/i18n/locales/ru.json b/apps/browser-extension/src/i18n/locales/ru.json index 41264d8e3..fb20a36b7 100644 --- a/apps/browser-extension/src/i18n/locales/ru.json +++ b/apps/browser-extension/src/i18n/locales/ru.json @@ -57,7 +57,7 @@ "next": "Далее", "use": "Использовать", "delete": "Удалить", - "save": "Save", + "save": "Сохранить", "or": "Или", "close": "Закрыть", "copied": "Скопировано!", @@ -242,13 +242,13 @@ "enterEmailPrefix": "Введите префикс электронной почты" }, "totp": { - "addCode": "Add 2FA Code", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "saveToViewCode": "Save to view code", + "addCode": "Добавить код 2FA", + "instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.", + "nameOptional": "Имя (необязательно)", + "secretKey": "Секретный ключ", + "saveToViewCode": "Сохранить для просмотра кода", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Неверный формат секретного ключа." } }, "emails": { diff --git a/apps/mobile-app/android/app/build.gradle b/apps/mobile-app/android/app/build.gradle index 0c10410d6..119ecf22e 100644 --- a/apps/mobile-app/android/app/build.gradle +++ b/apps/mobile-app/android/app/build.gradle @@ -202,6 +202,9 @@ dependencies { // Add modern SQLite library with VACUUM INTO and backup API support implementation("com.github.requery:sqlite-android:3.49.0") + // Add ZXing library for QR code scanning (F-Droid compatible) + implementation("com.journeyapps:zxing-android-embedded:4.3.0") + // Test dependencies testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-core:4.0.0' diff --git a/apps/mobile-app/android/app/src/main/AndroidManifest.xml b/apps/mobile-app/android/app/src/main/AndroidManifest.xml index d14b7086e..0dd908452 100644 --- a/apps/mobile-app/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile-app/android/app/src/main/AndroidManifest.xml @@ -58,6 +58,13 @@ android:theme="@style/PasskeyRegistrationTheme" android:screenOrientation="portrait" /> + + + diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt index dc98c2176..a282af1de 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/MainActivity.kt @@ -113,6 +113,8 @@ class MainActivity : ReactActivity() { handlePinUnlockResult(resultCode, data) } else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.PIN_SETUP_REQUEST_CODE) { handlePinSetupResult(resultCode, data) + } else if (requestCode == net.aliasvault.app.nativevaultmanager.NativeVaultManager.QR_SCANNER_REQUEST_CODE) { + handleQRScannerResult(resultCode, data) } } @@ -194,4 +196,31 @@ class MainActivity : ReactActivity() { } } } + + /** + * Handle QR scanner result. + * @param resultCode The result code from the QR scanner activity. + * @param data The intent data containing the scanned QR code. + */ + private fun handleQRScannerResult(resultCode: Int, data: Intent?) { + val promise = net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise + net.aliasvault.app.nativevaultmanager.NativeVaultManager.pendingActivityResultPromise = null + + if (promise == null) { + return + } + + when (resultCode) { + RESULT_OK -> { + val scannedData = data?.getStringExtra("SCAN_RESULT") + promise.resolve(scannedData) + } + RESULT_CANCELED -> { + promise.resolve(null) + } + else -> { + promise.resolve(null) + } + } + } } diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt index 0b3b1e44b..f93aa8724 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import net.aliasvault.app.qrscanner.QRScannerActivity import net.aliasvault.app.vaultstore.VaultStore import net.aliasvault.app.vaultstore.VaultSyncError import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider @@ -63,6 +64,11 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : */ const val PIN_SETUP_REQUEST_CODE = 1002 + /** + * Request code for QR scanner activity. + */ + const val QR_SCANNER_REQUEST_CODE = 1003 + /** * Static holder for the pending promise from showPinUnlock. * This allows MainActivity to resolve/reject the promise directly without @@ -1436,6 +1442,42 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : * @param subtitle The subtitle for authentication. If null or empty, uses default. * @param promise The promise to resolve with authentication result. */ + @ReactMethod + override fun scanQRCode(prefixes: ReadableArray?, statusText: String?, promise: Promise) { + CoroutineScope(Dispatchers.Main).launch { + try { + val activity = currentActivity + if (activity == null) { + promise.reject("NO_ACTIVITY", "No activity available", null) + return@launch + } + + // Store promise for later resolution by MainActivity + pendingActivityResultPromise = promise + + // Launch QR scanner activity with optional prefixes and status text + val intent = Intent(activity, QRScannerActivity::class.java) + if (prefixes != null && prefixes.size() > 0) { + val prefixList = ArrayList() + for (i in 0 until prefixes.size()) { + val prefix = prefixes.getString(i) + if (prefix != null) { + prefixList.add(prefix) + } + } + intent.putStringArrayListExtra(QRScannerActivity.EXTRA_PREFIXES, prefixList) + } + if (statusText != null && statusText.isNotEmpty()) { + intent.putExtra(QRScannerActivity.EXTRA_STATUS_TEXT, statusText) + } + activity.startActivityForResult(intent, QR_SCANNER_REQUEST_CODE) + } catch (e: Exception) { + Log.e(TAG, "Failed to launch QR scanner", e) + promise.reject("SCANNER_ERROR", "Failed to launch QR scanner: ${e.message}", e) + } + } + } + @ReactMethod override fun authenticateUser(title: String?, subtitle: String?, promise: Promise) { CoroutineScope(Dispatchers.Main).launch { diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/qrscanner/QRScannerActivity.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/qrscanner/QRScannerActivity.kt new file mode 100644 index 000000000..efa88b29d --- /dev/null +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/qrscanner/QRScannerActivity.kt @@ -0,0 +1,147 @@ +package net.aliasvault.app.qrscanner + +import android.animation.ObjectAnimator +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.View +import com.google.zxing.ResultPoint +import com.journeyapps.barcodescanner.BarcodeCallback +import com.journeyapps.barcodescanner.BarcodeResult +import com.journeyapps.barcodescanner.CaptureManager +import com.journeyapps.barcodescanner.DecoratedBarcodeView + +/** + * Activity for scanning QR codes using ZXing. + */ +class QRScannerActivity : Activity() { + private lateinit var barcodeView: DecoratedBarcodeView + private lateinit var capture: CaptureManager + private var hasScanned = false + private var prefixes: List? = null + + companion object { + /** Intent extra key for prefixes. */ + const val EXTRA_PREFIXES = "EXTRA_PREFIXES" + + /** Intent extra key for status text. */ + const val EXTRA_STATUS_TEXT = "EXTRA_STATUS_TEXT" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // Get prefixes from intent if provided + prefixes = intent.getStringArrayListExtra(EXTRA_PREFIXES) + + // Get status text from intent, default to "Scan QR code" if not provided + val statusText = intent.getStringExtra(EXTRA_STATUS_TEXT)?.takeIf { it.isNotEmpty() } ?: "Scan QR code" + + // Create and configure barcode view + barcodeView = DecoratedBarcodeView(this) + barcodeView.setStatusText(statusText) + setContentView(barcodeView) + + // Initialize capture manager + capture = CaptureManager(this, barcodeView) + capture.initializeFromIntent(intent, savedInstanceState) + + // Set custom callback to add visual feedback + barcodeView.decodeContinuous(object : BarcodeCallback { + override fun barcodeResult(result: BarcodeResult?) { + if (result != null && !hasScanned) { + val scannedText = result.text + + // Check if prefixes filter is enabled + if (prefixes != null && prefixes!!.isNotEmpty()) { + // Check if the scanned code starts with any of the accepted prefixes + val hasValidPrefix = prefixes!!.any { prefix -> + scannedText.startsWith(prefix) + } + + if (!hasValidPrefix) { + // Invalid QR code - continue scanning without setting hasScanned + // Note: ZXing library continues scanning automatically + return + } + } + + // Valid QR code + hasScanned = true + + // Show success animation + showScanSuccessAnimation() + + // Pause scanning + barcodeView.pause() + + // Set result and finish after animation + val resultIntent = Intent() + resultIntent.putExtra("SCAN_RESULT", scannedText) + setResult(RESULT_OK, resultIntent) + + // Delay finish to allow animation to complete + barcodeView.postDelayed({ + finish() + }, 400) // 400ms delay for animation + } + } + + override fun possibleResultPoints(resultPoints: List) { + // No visualization needed + } + }) + } + + /** + * Show a success animation when QR code is scanned. + */ + private fun showScanSuccessAnimation() { + // Flash animation - fade viewfinder quickly + val viewFinder: View? = barcodeView.viewFinder + if (viewFinder != null) { + // Create flash effect by animating alpha + val fadeOut = ObjectAnimator.ofFloat(viewFinder, "alpha", 1f, 0.3f) + fadeOut.duration = 100 + + val fadeIn = ObjectAnimator.ofFloat(viewFinder, "alpha", 0.3f, 1f) + fadeIn.duration = 100 + + fadeOut.start() + fadeOut.addListener(object : android.animation.AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: android.animation.Animator) { + fadeIn.start() + } + }) + } + } + + override fun onResume() { + super.onResume() + capture.onResume() + } + + override fun onPause() { + super.onPause() + capture.onPause() + } + + override fun onDestroy() { + super.onDestroy() + capture.onDestroy() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + capture.onSaveInstanceState(outState) + } + + @Deprecated("Deprecated in Java") + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray, + ) { + capture.onRequestPermissionsResult(requestCode, permissions, grantResults) + } +} diff --git a/apps/mobile-app/android/fdroid/.gitignore b/apps/mobile-app/android/fdroid/.gitignore index 30f68ef89..e8a47d513 100644 --- a/apps/mobile-app/android/fdroid/.gitignore +++ b/apps/mobile-app/android/fdroid/.gitignore @@ -1,2 +1,3 @@ fdroiddata fdroidserver +net.aliasvault.app.yml diff --git a/apps/mobile-app/android/fdroid/docker-compose.yml b/apps/mobile-app/android/fdroid/docker-compose.yml index 3985e118e..52c68d2ab 100644 --- a/apps/mobile-app/android/fdroid/docker-compose.yml +++ b/apps/mobile-app/android/fdroid/docker-compose.yml @@ -14,6 +14,8 @@ services: - ./net.aliasvault.app.yml:/net.aliasvault.app.yml # Add build script to the container - ./scripts/build.sh:/build.sh:Z + # Bind the outputs directory to capture APK build output + - ./outputs:/outputs:rw # Increase memory limits for Gradle builds shm_size: '2gb' mem_limit: 12g diff --git a/apps/mobile-app/android/fdroid/net.aliasvault.app.yml b/apps/mobile-app/android/fdroid/net.aliasvault.app.yml.template similarity index 89% rename from apps/mobile-app/android/fdroid/net.aliasvault.app.yml rename to apps/mobile-app/android/fdroid/net.aliasvault.app.yml.template index 897f2d36a..158054e84 100644 --- a/apps/mobile-app/android/fdroid/net.aliasvault.app.yml +++ b/apps/mobile-app/android/fdroid/net.aliasvault.app.yml.template @@ -13,9 +13,9 @@ RepoType: git Repo: https://github.com/aliasvault/aliasvault.git Builds: - - versionName: 0.1.0 - versionCode: 1 - commit: main + - versionName: __VERSION_NAME__ + versionCode: __VERSION_CODE__ + commit: __COMMIT__ subdir: apps/mobile-app/android/app/ sudo: - sysctl fs.inotify.max_user_watches=524288 || true @@ -26,7 +26,7 @@ Builds: init: - cd ../.. - sed -i -e '/signingConfig /d' android/app/build.gradle - - npm install --build-from-source + - npm install --production --build-from-source gradle: - yes scanignore: @@ -44,8 +44,6 @@ Builds: - apps/mobile-app/node_modules/react-native-context-menu-view/android/build.gradle - apps/mobile-app/node_modules/react-native-get-random-values/android/build.gradle - apps/mobile-app/node_modules/react-native-svg/android/build.gradle - - apps/mobile-app/node_modules/expo-dev-launcher/android/build.gradle - - apps/mobile-app/node_modules/expo-dev-menu/android/build.gradle scandelete: - apps/mobile-app/node_modules/ diff --git a/apps/mobile-app/android/fdroid/run.sh b/apps/mobile-app/android/fdroid/run.sh index 3b843a69d..a5441211d 100755 --- a/apps/mobile-app/android/fdroid/run.sh +++ b/apps/mobile-app/android/fdroid/run.sh @@ -2,11 +2,64 @@ set -e # Exit on any error, except where explicitly ignored trap 'echo "🛑 Interrupted. Exiting..."; exit 130' INT # Handle Ctrl+C cleanly +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BUILD_GRADLE="${SCRIPT_DIR}/../app/build.gradle" +TEMPLATE_FILE="${SCRIPT_DIR}/net.aliasvault.app.yml.template" +OUTPUT_FILE="${SCRIPT_DIR}/net.aliasvault.app.yml" + +# Check if template exists +if [ ! -f "$TEMPLATE_FILE" ]; then + echo "❌ Error: Template file not found: $TEMPLATE_FILE" + exit 1 +fi + +# Check if build.gradle exists +if [ ! -f "$BUILD_GRADLE" ]; then + echo "❌ Error: build.gradle not found: $BUILD_GRADLE" + exit 1 +fi + +# Extract version information from build.gradle +echo "📱 Extracting version information from build.gradle..." +VERSION_CODE=$(grep -E '^\s*versionCode\s+' "$BUILD_GRADLE" | sed -E 's/.*versionCode\s+([0-9]+).*/\1/') +VERSION_NAME=$(grep -E '^\s*versionName\s+' "$BUILD_GRADLE" | sed -E 's/.*versionName\s+"([^"]+)".*/\1/') + +if [ -z "$VERSION_CODE" ] || [ -z "$VERSION_NAME" ]; then + echo "❌ Error: Could not extract version information from build.gradle" + echo " versionCode: ${VERSION_CODE:-not found}" + echo " versionName: ${VERSION_NAME:-not found}" + exit 1 +fi + +# Get current git branch +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main") + +echo "✅ Version information extracted:" +echo " versionCode: $VERSION_CODE" +echo " versionName: $VERSION_NAME" +echo " commit: $CURRENT_BRANCH" + +# Generate the F-Droid metadata file from template +echo "📝 Generating F-Droid metadata file..." +sed -e "s/__VERSION_NAME__/$VERSION_NAME/g" \ + -e "s/__VERSION_CODE__/$VERSION_CODE/g" \ + -e "s/__COMMIT__/$CURRENT_BRANCH/g" \ + "$TEMPLATE_FILE" > "$OUTPUT_FILE" + +echo "✅ Generated: $OUTPUT_FILE" + +# Create outputs bind dir and set correct permissions +mkdir -p outputs +sudo chown -R 1000:1000 outputs + # Build and run the Docker environment -echo "Building Docker images..." +echo "🐳 Building Docker images..." if ! docker compose build; then echo "⚠️ Warning: Docker build failed, continuing..." fi -echo "Running fdroid-buildserver..." -docker compose run --rm fdroid-buildserver \ No newline at end of file +echo "🚀 Running fdroid-buildserver..." +docker compose run --rm fdroid-buildserver + +echo "✅ F-Droid build completed!" diff --git a/apps/mobile-app/android/fdroid/scripts/build.sh b/apps/mobile-app/android/fdroid/scripts/build.sh index 7df37dbd9..8476db36d 100755 --- a/apps/mobile-app/android/fdroid/scripts/build.sh +++ b/apps/mobile-app/android/fdroid/scripts/build.sh @@ -22,5 +22,9 @@ cd /home/vagrant/build fdroid fetchsrclibs net.aliasvault.app --verbose # Format build receipe fdroid rewritemeta net.aliasvault.app +# Lint app +fdroid lint --verbose net.aliasvault.app # Build app and scan for any binary files that are prohibited -fdroid build --verbose --latest --scan-binary --on-server --no-tarball net.aliasvault.app \ No newline at end of file +fdroid build --verbose --test --latest --scan-binary --on-server --no-tarball net.aliasvault.app +# Copy any outputs to the bind mount folder +rsync -avh /home/vagrant/build/build/net.aliasvault.app/apps/mobile-app/android/app/build/outputs/ /outputs/ diff --git a/apps/mobile-app/android/fdroid/scripts/sign-apk.sh b/apps/mobile-app/android/fdroid/scripts/sign-apk.sh new file mode 100755 index 000000000..77341f053 --- /dev/null +++ b/apps/mobile-app/android/fdroid/scripts/sign-apk.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +# ================================ +# This script is used to sign an unsigned F-Droid APK file with the local debug keystore (on MacOS) for testing purposes. +# ================================ +# Flow: +# 1. First do the run.sh / build.sh flow to build the F-Droid APK file on a (Linux) machine with enough memory and CPU power. +# 2. Extract the unsigned APK file from the local (bind-mounted) outputs directory +# 3. Then use this script to sign the APK file with the local debug keystore (on MacOS). +# +# ================================ + +set -euo pipefail + +# --- Colors --- +RED="\033[0;31m" +GREEN="\033[0;32m" +YELLOW="\033[1;33m" +CYAN="\033[0;36m" +RESET="\033[0m" + +info() { echo -e "${CYAN}[INFO]${RESET} $1"; } +ok() { echo -e "${GREEN}[OK]${RESET} $1"; } +error() { echo -e "${RED}[ERROR]${RESET} $1"; } + +echo -e "${YELLOW}=== APK Debug Signer (macOS) ===${RESET}" + +# --- Ask for unsigned APK --- +read -rp "Enter unsigned APK filename (example: app-release-unsigned.apk): " APK_IN + +if [[ ! -f "$APK_IN" ]]; then + error "File not found: $APK_IN" + exit 1 +fi + +info "Input APK: $APK_IN" + +# --- Detect SDK and build-tools --- +SDK_ROOT="${ANDROID_SDK_ROOT:-$HOME/Library/Android/sdk}" +BT_DIR="$SDK_ROOT/build-tools" + +if [[ ! -d "$BT_DIR" ]]; then + error "build-tools not found in: $BT_DIR" + exit 1 +fi + +info "Scanning build-tools..." + +LATEST_BT="$(ls "$BT_DIR" | sort -V | tail -n 1)" + +if [[ -z "$LATEST_BT" ]]; then + error "No build-tools found." + exit 1 +fi + +info "Using build-tools version: ${YELLOW}${LATEST_BT}${RESET}" + +ZIPALIGN="$BT_DIR/$LATEST_BT/zipalign" +APKSIGNER="$BT_DIR/$LATEST_BT/apksigner" + +[[ -x "$ZIPALIGN" ]] || { error "zipalign missing: $ZIPALIGN"; exit 1; } +[[ -x "$APKSIGNER" ]] || { error "apksigner missing: $APKSIGNER"; exit 1; } + +# --- Filenames --- +APK_ALIGNED="${APK_IN%.apk}-aligned-temp.apk" +APK_SIGNED="${APK_IN%.apk}-signed.apk" + +info "Temporary aligned APK: $APK_ALIGNED" +info "Final signed APK: $APK_SIGNED" + +# --- Debug keystore --- +DEBUG_KEYSTORE="$HOME/.android/debug.keystore" +DEBUG_ALIAS="androiddebugkey" +DEBUG_PASS="android" + +[[ -f "$DEBUG_KEYSTORE" ]] || { + error "Debug keystore missing: $DEBUG_KEYSTORE" + exit 1 +} + +info "Using debug keystore: $DEBUG_KEYSTORE" + +# --- Step 1: zipalign --- +echo -e "${YELLOW}=== Step 1: zipalign ===${RESET}" +echo -e "[CMD] \"$ZIPALIGN\" -p -f 4 \"$APK_IN\" \"$APK_ALIGNED\"" + +"$ZIPALIGN" -p -f 4 "$APK_IN" "$APK_ALIGNED" +ok "zipalign complete" + +# --- Step 2: sign --- +echo -e "${YELLOW}=== Step 2: apksigner ===${RESET}" +echo -e "[CMD] \"$APKSIGNER\" sign --ks \"$DEBUG_KEYSTORE\" --out \"$APK_SIGNED\" \"$APK_ALIGNED\"" + +"$APKSIGNER" sign \ + --ks "$DEBUG_KEYSTORE" \ + --ks-key-alias "$DEBUG_ALIAS" \ + --ks-pass "pass:$DEBUG_PASS" \ + --key-pass "pass:$DEBUG_PASS" \ + --out "$APK_SIGNED" \ + "$APK_ALIGNED" + +ok "Signing complete" + +# --- Step 3: verify --- +echo -e "${YELLOW}=== Step 3: Verify ===${RESET}" + +"$APKSIGNER" verify --verbose "$APK_SIGNED" +ok "APK verified" + +# --- Step 4: Cleanup --- +echo -e "${YELLOW}=== Cleanup ===${RESET}" + +if [[ -f "$APK_ALIGNED" ]]; then + rm -f "$APK_ALIGNED" + ok "Removed temporary file: $APK_ALIGNED" +fi + +ok "Cleanup complete" + +echo -e "${GREEN}=== DONE ===${RESET}" +echo -e "Signed APK created → ${YELLOW}$APK_SIGNED${RESET}" +echo -e "Install with:" +echo -e " ${CYAN}adb install -r \"$APK_SIGNED\"${RESET}" + diff --git a/apps/mobile-app/android/gradle.properties b/apps/mobile-app/android/gradle.properties index a0a18dd2a..889455a91 100644 --- a/apps/mobile-app/android/gradle.properties +++ b/apps/mobile-app/android/gradle.properties @@ -52,6 +52,9 @@ expo.webp.animated=false # Enable network inspector EX_DEV_CLIENT_NETWORK_INSPECTOR=true +# Enable VisionCamera code scanner +VisionCamera_enableCodeScanner=true + # Use legacy packaging to compress native libraries in the resulting APK. expo.useLegacyPackaging=false diff --git a/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx b/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx index 68b3b2893..278139892 100644 --- a/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx +++ b/apps/mobile-app/app/(tabs)/settings/qr-scanner.tsx @@ -1,8 +1,6 @@ -import { Ionicons } from '@expo/vector-icons'; -import { CameraView, useCameraPermissions } from 'expo-camera'; import { Href, router, useLocalSearchParams } from 'expo-router'; import { useEffect, useCallback, useRef } from 'react'; -import { View, Alert, StyleSheet } from 'react-native'; +import { View, StyleSheet, Platform, Alert } from 'react-native'; import { useColors } from '@/hooks/useColorScheme'; import { useTranslation } from '@/hooks/useTranslation'; @@ -10,6 +8,7 @@ import { useTranslation } from '@/hooks/useTranslation'; import LoadingIndicator from '@/components/LoadingIndicator'; import { ThemedContainer } from '@/components/themed/ThemedContainer'; import { ThemedText } from '@/components/themed/ThemedText'; +import NativeVaultManager from '@/specs/NativeVaultManager'; // QR Code type prefixes const QR_CODE_PREFIXES = { @@ -54,71 +53,73 @@ function parseQRCode(data: string): ScannedQRCode { export default function QRScannerScreen() : React.ReactNode { const colors = useColors(); const { t } = useTranslation(); - const [permission, requestPermission] = useCameraPermissions(); const { url } = useLocalSearchParams<{ url?: string }>(); const hasProcessedUrl = useRef(false); const processedUrls = useRef(new Set()); - - // Request camera permission on mount - useEffect(() => { - /** - * Request camera permission. - */ - const requestCameraPermission = async () : Promise => { - if (!permission) { - return; // Still loading permission status - } - - if (!permission.granted && permission.canAskAgain) { - // Request permission - await requestPermission(); - } else if (!permission.granted && !permission.canAskAgain) { - // Permission was permanently denied - Alert.alert( - t('settings.qrScanner.cameraPermissionTitle'), - t('settings.qrScanner.cameraPermissionMessage'), - [{ text: t('common.ok'), /** - * Go back to the settings tab. - */ - onPress: (): void => router.back() }] - ); - } - }; - - requestCameraPermission(); - }, [permission, requestPermission, t]); + const hasLaunchedScanner = useRef(false); /* * Handle barcode scanned - parse and navigate to appropriate page. - * Only processes AliasVault QR codes, silently ignores others. + * Native scanner already filters by prefix, so we only get AliasVault QR codes here. * Validation is handled by the destination page. */ - const handleBarcodeScanned = useCallback(({ data }: { data: string }) : void => { + const handleQRCodeScanned = useCallback((data: string) : void => { // Prevent processing the same URL multiple times if (processedUrls.current.has(data)) { return; } - // Parse the QR code to determine its type - const parsedData = parseQRCode(data); - - // Silently ignore non-AliasVault QR codes - if (!parsedData.type) { - return; - } - // Mark this URL as processed processedUrls.current.add(data); + // Parse the QR code to determine its type + const parsedData = parseQRCode(data); + /* * Navigate to the appropriate page based on QR code type - * Validation will be handled by the destination page + * Use push instead of replace to navigate while scanner is still dismissing + * This creates a smoother transition without returning to settings first */ if (parsedData.type === 'MOBILE_UNLOCK') { - router.replace(`/(tabs)/settings/mobile-unlock/${parsedData.payload}` as Href); + router.push(`/(tabs)/settings/mobile-unlock/${parsedData.payload}` as Href); } }, []); + /** + * Launch the native QR scanner. + */ + const launchScanner = useCallback(async () => { + if (hasLaunchedScanner.current) { + return; + } + + hasLaunchedScanner.current = true; + + try { + // Pass prefixes to native scanner for filtering and translated status text + const prefixes = Object.values(QR_CODE_PREFIXES); + const statusText = t('settings.qrScanner.scanningMessage'); + const scannedData = await NativeVaultManager.scanQRCode(prefixes, statusText); + + if (scannedData) { + handleQRCodeScanned(scannedData); + } else { + // User cancelled or scan failed, go back + router.back(); + } + } catch (error) { + console.error('QR scan error:', error); + Alert.alert( + t('common.error'), + 'Failed to scan QR code', + [{ text: t('common.ok'), /** + * Navigate back. + */ + onPress: (): void => router.back() }] + ); + } + }, [handleQRCodeScanned, t]); + /** * Reset hasProcessedUrl when URL changes to allow processing new URLs. */ @@ -132,45 +133,24 @@ export default function QRScannerScreen() : React.ReactNode { useEffect(() => { if (url && typeof url === 'string' && !hasProcessedUrl.current) { hasProcessedUrl.current = true; - handleBarcodeScanned({ data: url }); + handleQRCodeScanned(url); } - }, [url, handleBarcodeScanned]); + }, [url, handleQRCodeScanned]); + + /** + * Launch scanner when component mounts (Android/iOS only). + */ + useEffect(() => { + if (Platform.OS === 'android' || Platform.OS === 'ios') { + launchScanner(); + } + }, [launchScanner]); const styles = StyleSheet.create({ container: { flex: 1, paddingHorizontal: 0, }, - camera: { - flex: 1, - }, - cameraContainer: { - backgroundColor: colors.black, - flex: 1, - }, - cameraOverlay: { - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - bottom: 0, - justifyContent: 'center', - left: 0, - position: 'absolute', - right: 0, - top: 0, - }, - cameraOverlayText: { - color: colors.white, - fontSize: 16, - marginTop: 20, - paddingHorizontal: 40, - textAlign: 'center', - }, - closeButton: { - position: 'absolute', - right: 16, - top: 16, - zIndex: 10, - }, loadingContainer: { alignItems: 'center', flex: 1, @@ -179,35 +159,14 @@ export default function QRScannerScreen() : React.ReactNode { }, }); - // Show permission request screen - if (!permission || !permission.granted) { - return ( - - - - - - ); - } - + // Show loading while scanner is launching return ( - - - - - - {t('settings.qrScanner.scanningMessage')} - - - + + + + {t('settings.qrScanner.scanningMessage')} + ); diff --git a/apps/mobile-app/i18n/locales/fi.json b/apps/mobile-app/i18n/locales/fi.json index 6610d4310..d3cc61ff5 100644 --- a/apps/mobile-app/i18n/locales/fi.json +++ b/apps/mobile-app/i18n/locales/fi.json @@ -20,9 +20,9 @@ "notice": "Huomautus", "enabled": "Otettu käyttöön", "disabled": "Pois käytöstä", - "twoFactorAuthentication": "Two-Factor Authentication", - "deleteItemConfirmTitle": "Delete Item", - "deleteItemConfirmDescription": "Are you sure you want to delete this item?", + "twoFactorAuthentication": "Kaksivaiheinen tunnistautuminen", + "deleteItemConfirmTitle": "Poista kohde", + "deleteItemConfirmDescription": "Haluatko varmasti poistaa tämän kohteen?", "errors": { "unknownError": "Tapahtui tuntematon virhe. Yritä uudelleen.", "unknownErrorTryAgain": "Tapahtui tuntematon virhe. Yritä uudelleen.", @@ -207,13 +207,13 @@ "passkeyWillBeDeleted": "Tämä todennusavain poistetaan, kun tallennat tämän käyttäjätiedon." }, "totp": { - "addCode": "Add 2FA Code", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "saveToViewCode": "Save to view code", + "addCode": "Lisää 2FA TOTP -koodi", + "nameOptional": "Nimi (valinnainen)", + "secretKey": "Salainen avain", + "instructions": "Syötä salainen avain, joka näkyy sivustossa, jossa haluat lisätä kaksivaiheisen tunnistautumisen", + "saveToViewCode": "Tallenna nähdäksesi koodin", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Virheellinen salatun avaimen muoto." } }, "settings": { @@ -328,8 +328,8 @@ "languageDescription": "Aseta kieli, jota käytetään luotaessa uusia henkilöllisyyksiä.", "genderSection": "Sukupuoli", "genderDescription": "Aseta oletussukupuoli uusien henkilöllisyyksien luomiseksi. ", - "ageRangeSection": "Age Range", - "ageRangeDescription": "Set the age range for generating new identities.", + "ageRangeSection": "Ikähaarukka", + "ageRangeDescription": "Aseta ikähaarukka uusia henkilöllisyyksien luomisessa", "genderOptions": { "random": "Satunnainen", "male": "Mies", diff --git a/apps/mobile-app/i18n/locales/fr.json b/apps/mobile-app/i18n/locales/fr.json index 8fb73bda7..0242ca581 100644 --- a/apps/mobile-app/i18n/locales/fr.json +++ b/apps/mobile-app/i18n/locales/fr.json @@ -7,22 +7,22 @@ "yes": "Oui", "no": "Non", "ok": "OK", - "continue": "Continue", - "loading": "Loading", - "error": "Error", - "success": "Success", - "never": "Never", - "copied": "Copied to clipboard", + "continue": "Continuer", + "loading": "Chargement", + "error": "Erreur", + "success": "Succès", + "never": "Jamais", + "copied": "Copier dans le presse-papiers", "loadMore": "Voir plus", - "use": "Use", - "confirm": "Confirm", - "next": "Next", - "notice": "Notice", - "enabled": "Enabled", - "disabled": "Disabled", - "twoFactorAuthentication": "Two-Factor Authentication", - "deleteItemConfirmTitle": "Delete Item", - "deleteItemConfirmDescription": "Are you sure you want to delete this item?", + "use": "Utiliser", + "confirm": "Confirmer", + "next": "Suivant", + "notice": "Notification", + "enabled": "Activé", + "disabled": "Désactivé", + "twoFactorAuthentication": "Authentification à deux facteurs", + "deleteItemConfirmTitle": "Supprimer l'élement", + "deleteItemConfirmDescription": "Êtes-vous certain de vouloir supprimer cet élément?", "errors": { "unknownError": "An unknown error occurred. Please try again.", "unknownErrorTryAgain": "An unknown error occurred. Please try again.", diff --git a/apps/mobile-app/i18n/locales/pl.json b/apps/mobile-app/i18n/locales/pl.json index d465a88b2..6b7b89791 100644 --- a/apps/mobile-app/i18n/locales/pl.json +++ b/apps/mobile-app/i18n/locales/pl.json @@ -20,9 +20,9 @@ "notice": "Uwaga", "enabled": "Włączone", "disabled": "Wyłączone", - "twoFactorAuthentication": "Two-Factor Authentication", - "deleteItemConfirmTitle": "Delete Item", - "deleteItemConfirmDescription": "Are you sure you want to delete this item?", + "twoFactorAuthentication": "Uwierzytelnianie dwuskładnikowe", + "deleteItemConfirmTitle": "Usuń element", + "deleteItemConfirmDescription": "Czy na pewno chcesz usunąć ten element?", "errors": { "unknownError": "Wystąpił nieznany błąd. Spróbuj ponownie.", "unknownErrorTryAgain": "Wystąpił nieznany błąd. Spróbuj ponownie.", @@ -207,13 +207,13 @@ "passkeyWillBeDeleted": "Ten klucz dostępu zostanie usunięty po zapisaniu tych danych." }, "totp": { - "addCode": "Add 2FA Code", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "saveToViewCode": "Save to view code", + "addCode": "Dodaj kod 2FA", + "nameOptional": "Nazwa (opcjonalnie)", + "secretKey": "Tajny klucz", + "instructions": "Wprowadź tajny klucz wyświetlony na stronie internetowej, na której chcesz dodać uwierzytelnianie dwuskładnikowe.", + "saveToViewCode": "Zapisz, aby wyświetlić kod", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Nieprawidłowy format tajnego klucza." } }, "settings": { diff --git a/apps/mobile-app/i18n/locales/ru.json b/apps/mobile-app/i18n/locales/ru.json index 0964e02e7..4cbc03678 100644 --- a/apps/mobile-app/i18n/locales/ru.json +++ b/apps/mobile-app/i18n/locales/ru.json @@ -20,9 +20,9 @@ "notice": "Примечание", "enabled": "Включено", "disabled": "Отключено", - "twoFactorAuthentication": "Two-Factor Authentication", - "deleteItemConfirmTitle": "Delete Item", - "deleteItemConfirmDescription": "Are you sure you want to delete this item?", + "twoFactorAuthentication": "Двухфакторная аутентификация", + "deleteItemConfirmTitle": "Удалить элемент", + "deleteItemConfirmDescription": "Вы уверены, что хотите удалить этот элемент?", "errors": { "unknownError": "Произошла неизвестная ошибка. Пожалуйста, попробуйте снова.", "unknownErrorTryAgain": "Произошла неизвестная ошибка. Попробуйте снова.", @@ -207,13 +207,13 @@ "passkeyWillBeDeleted": "Этот ключ доступа будет удален при сохранении этой учетной записи." }, "totp": { - "addCode": "Add 2FA Code", - "nameOptional": "Name (optional)", - "secretKey": "Secret Key", - "instructions": "Enter the secret key shown by the website where you want to add two-factor authentication.", - "saveToViewCode": "Save to view code", + "addCode": "Добавить код 2FA", + "nameOptional": "Имя (необязательно)", + "secretKey": "Секретный ключ", + "instructions": "Введите секретный ключ, указанный на веб-сайте, где вы хотите добавить двухфакторную аутентификацию.", + "saveToViewCode": "Сохранить для просмотра кода", "errors": { - "invalidSecretKey": "Invalid secret key format." + "invalidSecretKey": "Неверный формат секретного ключа." } }, "settings": { diff --git a/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj b/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj index ffab2b72d..626358c4a 100644 --- a/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj +++ b/apps/mobile-app/ios/AliasVault.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -212,7 +212,7 @@ /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */ = { + CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, @@ -222,84 +222,13 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - CE59C7602E4F47FD0024A246 /* VaultUITests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultUITests; - sourceTree = ""; - }; - CE77825E2EA1822400A75E6F /* VaultUtils */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultUtils; - sourceTree = ""; - }; - CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultStoreKit; - sourceTree = ""; - }; - CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultStoreKitTests; - sourceTree = ""; - }; - CEE4816B2DBE8AC800F4A367 /* VaultUI */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultUI; - sourceTree = ""; - }; - CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = VaultModels; - sourceTree = ""; - }; - CEE909812DA548C7008D568F /* Autofill */ = { - isa = PBXFileSystemSynchronizedRootGroup; - exceptions = ( - CEE9098F2DA548C7008D568F /* Exceptions for "Autofill" folder in "Autofill" target */, - ); - explicitFileTypes = { - }; - explicitFolders = ( - ); - path = Autofill; - sourceTree = ""; - }; + CE59C7602E4F47FD0024A246 /* VaultUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUITests; sourceTree = ""; }; + CE77825E2EA1822400A75E6F /* VaultUtils */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUtils; sourceTree = ""; }; + CEE480882DBE86DC00F4A367 /* VaultStoreKit */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKit; sourceTree = ""; }; + CEE480972DBE86DD00F4A367 /* VaultStoreKitTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultStoreKitTests; sourceTree = ""; }; + CEE4816B2DBE8AC800F4A367 /* VaultUI */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultUI; sourceTree = ""; }; + CEE482AB2DBE8EFE00F4A367 /* VaultModels */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = VaultModels; sourceTree = ""; }; + CEE909812DA548C7008D568F /* Autofill */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (CEE9098F2DA548C7008D568F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Autofill; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1418,10 +1347,7 @@ LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -1475,10 +1401,7 @@ ); LIBRARY_SEARCH_PATHS = "$(SDKROOT)/usr/lib/swift\"$(inherited)\""; MTL_ENABLE_DEBUG_INFO = NO; - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index 5abc17116..7152e3128 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -291,4 +291,10 @@ [vaultManager authenticateUser:title subtitle:subtitle resolver:resolve rejecter:reject]; } +// MARK: - QR Code Scanner + +- (void)scanQRCode:(NSArray *)prefixes statusText:(NSString *)statusText resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [vaultManager scanQRCode:prefixes statusText:statusText resolver:resolve rejecter:reject]; +} + @end diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 1b4e6699f..535b7a4c7 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -5,6 +5,7 @@ import VaultStoreKit import VaultModels import SwiftUI import VaultUI +import AVFoundation /** * This class is used as a bridge to allow React Native to interact with the VaultStoreKit class. @@ -913,6 +914,42 @@ public class VaultManager: NSObject { } } + @objc + func scanQRCode(_ prefixes: [String]?, + statusText: String?, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + DispatchQueue.main.async { + // Get the root view controller from React Native + guard let rootVC = RCTPresentedViewController() else { + reject("NO_VIEW_CONTROLLER", "No view controller available", nil) + return + } + + // Create QR scanner view with optional prefix filtering and custom status text + let scannerView = QRScannerView( + prefixes: prefixes, + statusText: statusText, + onCodeScanned: { code in + // Resolve immediately and dismiss without waiting (matches Android behavior) + resolve(code) + rootVC.dismiss(animated: true) + }, + onCancel: { + // Cancel resolves nil and dismisses + resolve(nil) + rootVC.dismiss(animated: true) + } + ) + + let hostingController = UIHostingController(rootView: scannerView) + + // Present modally as full screen + hostingController.modalPresentationStyle = .fullScreen + rootVC.present(hostingController, animated: true) + } + } + @objc func authenticateUser(_ title: String?, subtitle: String?, diff --git a/apps/mobile-app/ios/Podfile.lock b/apps/mobile-app/ios/Podfile.lock index 42b9df0ad..5923a0124 100644 --- a/apps/mobile-app/ios/Podfile.lock +++ b/apps/mobile-app/ios/Podfile.lock @@ -272,10 +272,6 @@ PODS: - ExpoModulesCore - ExpoBlur (14.1.5): - ExpoModulesCore - - ExpoCamera (16.1.11): - - ExpoModulesCore - - ZXingObjC/OneD - - ZXingObjC/PDF417 - ExpoClipboard (7.1.5): - ExpoModulesCore - ExpoDocumentPicker (13.1.6): @@ -2449,11 +2445,6 @@ PODS: - SwiftLint (0.59.1) - SWXMLHash (7.0.2) - Yoga (0.0.0) - - ZXingObjC/Core (3.6.9) - - ZXingObjC/OneD (3.6.9): - - ZXingObjC/Core - - ZXingObjC/PDF417 (3.6.9): - - ZXingObjC/Core DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) @@ -2468,7 +2459,6 @@ DEPENDENCIES: - expo-dev-menu-interface (from `../node_modules/expo-dev-menu-interface/ios`) - ExpoAsset (from `../node_modules/expo-asset/ios`) - ExpoBlur (from `../node_modules/expo-blur/ios`) - - ExpoCamera (from `../node_modules/expo-camera/ios`) - ExpoClipboard (from `../node_modules/expo-clipboard/ios`) - ExpoDocumentPicker (from `../node_modules/expo-document-picker/ios`) - ExpoFileSystem (from `../node_modules/expo-file-system/ios`) @@ -2582,7 +2572,6 @@ SPEC REPOS: - SQLite.swift - SwiftLint - SWXMLHash - - ZXingObjC EXTERNAL SOURCES: boost: @@ -2609,8 +2598,6 @@ EXTERNAL SOURCES: :path: "../node_modules/expo-asset/ios" ExpoBlur: :path: "../node_modules/expo-blur/ios" - ExpoCamera: - :path: "../node_modules/expo-camera/ios" ExpoClipboard: :path: "../node_modules/expo-clipboard/ios" ExpoDocumentPicker: @@ -2820,7 +2807,6 @@ SPEC CHECKSUMS: expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e ExpoAsset: ef06e880126c375f580d4923fdd1cdf4ee6ee7d6 ExpoBlur: 3c8885b9bf9eef4309041ec87adec48b5f1986a9 - ExpoCamera: e1879906d41184e84b57d7643119f8509414e318 ExpoClipboard: 436f6de6971f14eb75ae160e076d9cb3b19eb795 ExpoDocumentPicker: b263a279685b6640b8c8bc70d71c83067aeaae55 ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 @@ -2925,7 +2911,6 @@ SPEC CHECKSUMS: SwiftLint: 3d48e2fb2a3468fdaccf049e5e755df22fb40c2c SWXMLHash: dd733a457e9c4fe93b1538654057aefae4acb382 Yoga: dc7c21200195acacb62fa920c588e7c2106de45e - ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 PODFILE CHECKSUM: ac288e273086bafdd610cafff08ccca0d164f7c3 diff --git a/apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift b/apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift new file mode 100644 index 000000000..7afa66548 --- /dev/null +++ b/apps/mobile-app/ios/VaultUI/QRScanner/QRScannerView.swift @@ -0,0 +1,240 @@ +import SwiftUI +import AVFoundation + +private let locBundle = Bundle.vaultUI + +/// SwiftUI view for scanning QR codes using AVFoundation +public struct QRScannerView: View { + let onCodeScanned: (String) -> Void + let onCancel: () -> Void + let prefixes: [String]? + let statusText: String + + @State private var hasScanned = false + @State private var showFlash = false + + public init( + prefixes: [String]? = nil, + statusText: String? = nil, + onCodeScanned: @escaping (String) -> Void, + onCancel: @escaping () -> Void + ) { + self.prefixes = prefixes + self.statusText = statusText?.isEmpty == false ? statusText! : "Scan QR code" + self.onCodeScanned = onCodeScanned + self.onCancel = onCancel + } + + public var body: some View { + ZStack { + // Camera preview + QRScannerRepresentable( + prefixes: prefixes, + onCodeScanned: { code in + if !hasScanned { + hasScanned = true + showFlash = true + + // Flash animation then callback + DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { + onCodeScanned(code) + } + } + }, + onCodeRejected: { + // Reset hasScanned to allow scanning again + hasScanned = false + } + ) + .edgesIgnoringSafeArea(.all) + + // Overlay with viewfinder + VStack { + Spacer() + + // Viewfinder frame + Rectangle() + .stroke(Color.white, lineWidth: 3) + .frame(width: 280, height: 280) + .overlay( + // Flash effect + Rectangle() + .fill(Color.white) + .opacity(showFlash ? 0.7 : 0) + .animation(.easeInOut(duration: 0.2), value: showFlash) + ) + + Spacer() + + // Status text + Text(statusText) + .foregroundColor(.white) + .padding() + .background(Color.black.opacity(0.7)) + .cornerRadius(10) + .padding(.bottom, 50) + } + + // Cancel button + VStack { + HStack { + Spacer() + Button(action: onCancel) { + Image(systemName: "xmark.circle.fill") + .font(.system(size: 32)) + .foregroundColor(.white) + .padding() + } + } + Spacer() + } + } + .background(Color.black) + } +} + +/// UIViewControllerRepresentable wrapper for AVFoundation camera +struct QRScannerRepresentable: UIViewControllerRepresentable { + let prefixes: [String]? + let onCodeScanned: (String) -> Void + let onCodeRejected: () -> Void + + func makeUIViewController(context: Context) -> QRScannerViewController { + let controller = QRScannerViewController() + controller.prefixes = prefixes + controller.onCodeScanned = onCodeScanned + controller.onCodeRejected = onCodeRejected + return controller + } + + func updateUIViewController(_ uiViewController: QRScannerViewController, context: Context) { + // No updates needed + } +} + +/// UIViewController that handles AVFoundation QR code scanning +class QRScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { + var captureSession: AVCaptureSession? + var previewLayer: AVCaptureVideoPreviewLayer? + var prefixes: [String]? + var onCodeScanned: ((String) -> Void)? + var onCodeRejected: (() -> Void)? + private var rejectedQRCodes = Set() // Track rejected QR codes to avoid repeated haptic feedback + + override func viewDidLoad() { + super.viewDidLoad() + setupCamera() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if let session = captureSession, !session.isRunning { + DispatchQueue.global(qos: .userInitiated).async { + session.startRunning() + } + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if let session = captureSession, session.isRunning { + DispatchQueue.global(qos: .userInitiated).async { + session.stopRunning() + } + } + } + + private func setupCamera() { + let session = AVCaptureSession() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { + return + } + + let videoInput: AVCaptureDeviceInput + + do { + videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice) + } catch { + return + } + + if session.canAddInput(videoInput) { + session.addInput(videoInput) + } else { + return + } + + let metadataOutput = AVCaptureMetadataOutput() + + if session.canAddOutput(metadataOutput) { + session.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + metadataOutput.metadataObjectTypes = [.qr] + } else { + return + } + + let previewLayer = AVCaptureVideoPreviewLayer(session: session) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + + self.captureSession = session + self.previewLayer = previewLayer + + DispatchQueue.global(qos: .userInitiated).async { + session.startRunning() + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + previewLayer?.frame = view.layer.bounds + } + + func metadataOutput( + _ output: AVCaptureMetadataOutput, + didOutput metadataObjects: [AVMetadataObject], + from connection: AVCaptureConnection + ) { + if let metadataObject = metadataObjects.first, + let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let stringValue = readableObject.stringValue { + + // Check if prefixes filter is enabled + if let prefixes = prefixes, !prefixes.isEmpty { + // Check if the scanned code starts with any of the accepted prefixes + let hasValidPrefix = prefixes.contains { prefix in + stringValue.hasPrefix(prefix) + } + + if !hasValidPrefix { + // Invalid QR code - only give haptic feedback once per unique code + if !rejectedQRCodes.contains(stringValue) { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.warning) + rejectedQRCodes.insert(stringValue) + } + + // Notify that code was rejected (to reset UI state if needed) + onCodeRejected?() + return + } + } + + // Valid QR code - stop scanning + captureSession?.stopRunning() + + // Success haptic feedback + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + + // Callback with scanned code + onCodeScanned?(stringValue) + } + } +} diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json index ec42f7b2c..9734ec7a8 100644 --- a/apps/mobile-app/package-lock.json +++ b/apps/mobile-app/package-lock.json @@ -17,10 +17,8 @@ "@types/jsrsasign": "^10.5.15", "expo": "^53.0.22", "expo-blur": "~14.1.5", - "expo-camera": "^16.1.11", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.7", - "expo-dev-client": "~5.1.8", "expo-document-picker": "~13.1.6", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", @@ -37,8 +35,7 @@ "expo-web-browser": "~14.2.0", "fbemitter": "^3.0.0", "i18next": "^25.3.2", - "jest": "~29.7.0", - "jest-expo": "~53.0.10", + "lodash": "^4.17.21", "otpauth": "^9.4.0", "react": "19.0.0", "react-hook-form": "^7.56.1", @@ -55,6 +52,7 @@ "react-native-safe-area-context": "5.6.1", "react-native-screens": "~4.15.4", "react-native-svg": "15.11.2", + "react-native-svg-transformer": "^1.5.0", "react-native-toast-message": "^2.2.1", "react-native-webview": "13.13.5", "secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0", @@ -77,10 +75,10 @@ "eslint-config-expo": "~9.2.0", "eslint-plugin-jsdoc": "^55.2.0", "eslint-plugin-react-native": "^5.0.0", + "expo-dev-client": "~5.1.8", "globals": "^16.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.0", - "react-native-svg-transformer": "^1.5.0", "typescript": "~5.8.3" }, "engines": { @@ -129,7 +127,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -4100,7 +4097,6 @@ "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.17.tgz", "integrity": "sha512-uEcYWi1NV+2Qe1oELfp9b5hTYekqWATv2cuwcOAg5EvsIsUPtzFrKIasgUXLBRGb9P7yR5ifoJ+ug4u6jdqSTQ==", "license": "MIT", - "peer": true, "dependencies": { "@react-navigation/core": "^7.12.4", "escape-string-regexp": "^4.0.0", @@ -4225,7 +4221,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4242,7 +4237,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4259,7 +4253,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4276,7 +4269,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4293,7 +4285,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4310,7 +4301,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4327,7 +4317,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -4344,7 +4333,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -4361,7 +4349,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "dev": true, "license": "MIT", "dependencies": { "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", @@ -4388,9 +4375,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -4410,7 +4395,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4423,7 +4407,6 @@ "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", @@ -4450,7 +4433,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.21.3", @@ -4468,7 +4450,6 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -4481,7 +4462,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.21.3", @@ -4504,7 +4484,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", - "dev": true, "license": "MIT", "dependencies": { "cosmiconfig": "^8.1.3", @@ -4526,7 +4505,6 @@ "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "dev": true, "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", @@ -4563,7 +4541,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "dev": true, "license": "ISC", "engines": { "node": ">=10.13.0" @@ -4634,6 +4611,7 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -4645,6 +4623,7 @@ "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -4775,7 +4754,6 @@ "integrity": "sha512-ixLZ7zG7j1fM0DijL9hDArwhwcCb4vqmePgwtV0GfnkHRSCUEv4LvzarcTdhoqgyMznUx/EhoTUv31CKZzkQlw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4893,7 +4871,6 @@ "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.43.0", "@typescript-eslint/types": "8.43.0", @@ -5424,6 +5401,7 @@ "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" @@ -5434,21 +5412,24 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.13.2", @@ -5456,6 +5437,7 @@ "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", @@ -5467,7 +5449,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.14.1", @@ -5475,6 +5458,7 @@ "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5488,6 +5472,7 @@ "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -5498,6 +5483,7 @@ "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -5507,7 +5493,8 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.14.1", @@ -5515,6 +5502,7 @@ "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5532,6 +5520,7 @@ "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", @@ -5546,6 +5535,7 @@ "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", @@ -5559,6 +5549,7 @@ "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", @@ -5574,6 +5565,7 @@ "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" @@ -5593,14 +5585,16 @@ "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "peer": true }, "node_modules/abab": { "version": "2.0.6", @@ -5649,7 +5643,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5674,6 +5667,7 @@ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10.13.0" }, @@ -5753,6 +5747,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -5771,6 +5766,7 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -5787,7 +5783,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/anser": { "version": "1.4.10", @@ -6545,7 +6542,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -6691,7 +6687,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -6785,6 +6780,7 @@ "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.0" } @@ -7277,7 +7273,6 @@ "version": "5.0.5", "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "dev": true, "license": "MIT", "dependencies": { "css-tree": "~2.2.0" @@ -7291,7 +7286,6 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.28", @@ -7306,7 +7300,6 @@ "version": "2.0.28", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "dev": true, "license": "CC0-1.0" }, "node_modules/cssom": { @@ -7717,7 +7710,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "dev": true, "license": "MIT", "dependencies": { "no-case": "^3.0.4", @@ -7823,6 +7815,7 @@ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -8028,7 +8021,8 @@ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/es-object-atoms": { "version": "1.1.1", @@ -8144,7 +8138,6 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8329,7 +8322,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -8686,7 +8678,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.22.tgz", "integrity": "sha512-sJ2I4W/e5iiM4u/wYCe3qmW4D7WPCRqByPDD0hJcdYNdjc9HFFFdO4OAudZVyC/MmtoWZEIH5kTJP1cw9FjzYA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "0.24.21", @@ -8756,26 +8747,6 @@ "react-native": "*" } }, - "node_modules/expo-camera": { - "version": "16.1.11", - "resolved": "https://registry.npmjs.org/expo-camera/-/expo-camera-16.1.11.tgz", - "integrity": "sha512-etA5ZKoC6nPBnWWqiTmlX//zoFZ6cWQCCIdmpUHTGHAKd4qZNCkhPvBWbi8o32pDe57lix1V4+TPFgEcvPwsaA==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.4" - }, - "peerDependencies": { - "expo": "*", - "react": "*", - "react-native": "*", - "react-native-web": "*" - }, - "peerDependenciesMeta": { - "react-native-web": { - "optional": true - } - } - }, "node_modules/expo-clipboard": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.1.5.tgz", @@ -8792,7 +8763,6 @@ "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.1.7.tgz", "integrity": "sha512-byBjGsJ6T6FrLlhOBxw4EaiMXrZEn/MlUYIj/JAd+FS7ll5X/S4qVRbIimSJtdW47hXMq0zxPfJX6njtA56hHA==", "license": "MIT", - "peer": true, "dependencies": { "@expo/config": "~11.0.12", "@expo/env": "~1.0.7" @@ -8806,6 +8776,7 @@ "version": "5.1.8", "resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-5.1.8.tgz", "integrity": "sha512-IopYPgBi3JflksO5ieTphbKsbYHy9iIVdT/d69It++y0iBMSm0oBIoDmUijrHKjE3fV6jnrwrm8luU13/mzIQQ==", + "dev": true, "license": "MIT", "dependencies": { "expo-dev-launcher": "5.1.11", @@ -8822,6 +8793,7 @@ "version": "5.1.11", "resolved": "https://registry.npmjs.org/expo-dev-launcher/-/expo-dev-launcher-5.1.11.tgz", "integrity": "sha512-bN0+nv5H038s8Gzf8i16hwCyD3sWDmHp7vb+QbL1i6B3XNnICCKS/H/3VH6H3PRMvCmoLGPlg+ODDqGlf0nu3g==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "8.11.0", @@ -8837,6 +8809,7 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", + "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -8853,12 +8826,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, "license": "MIT" }, "node_modules/expo-dev-menu": { "version": "6.1.10", "resolved": "https://registry.npmjs.org/expo-dev-menu/-/expo-dev-menu-6.1.10.tgz", "integrity": "sha512-LaI0Bw5zzw5XefjYSX6YaMydzk0YBysjqQoxzj6ufDyKgwAfPmFwOLkZ03DOSerc9naezGLNAGgTEN6QTgMmgQ==", + "dev": true, "license": "MIT", "dependencies": { "expo-dev-menu-interface": "1.10.0" @@ -8871,6 +8846,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/expo-dev-menu-interface/-/expo-dev-menu-interface-1.10.0.tgz", "integrity": "sha512-NxtM/qot5Rh2cY333iOE87dDg1S8CibW+Wu4WdLua3UMjy81pXYzAGCZGNOeY7k9GpNFqDPNDXWyBSlk9r2pBg==", + "dev": true, "license": "MIT", "peerDependencies": { "expo": "*" @@ -8900,7 +8876,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.3.2.tgz", "integrity": "sha512-wUlMdpqURmQ/CNKK/+BIHkDA5nGjMqNlYmW0pJFXY/KE/OG80Qcavdu2sHsL4efAIiNGvYdBS10WztuQYU4X0A==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -8922,6 +8897,7 @@ "version": "0.15.0", "resolved": "https://registry.npmjs.org/expo-json-utils/-/expo-json-utils-0.15.0.tgz", "integrity": "sha512-duRT6oGl80IDzH2LD2yEFWNwGIC2WkozsB6HF3cDYNoNNdUvFk6uN3YiwsTsqVM/D0z6LEAQ01/SlYvN+Fw0JQ==", + "dev": true, "license": "MIT" }, "node_modules/expo-keep-awake": { @@ -8950,7 +8926,6 @@ "resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.1.7.tgz", "integrity": "sha512-ZJaH1RIch2G/M3hx2QJdlrKbYFUTOjVVW4g39hfxrE5bPX9xhZUYXqxqQtzMNl1ylAevw9JkgEfWbBWddbZ3UA==", "license": "MIT", - "peer": true, "dependencies": { "expo-constants": "~17.1.7", "invariant": "^2.2.4" @@ -8989,6 +8964,7 @@ "version": "0.16.6", "resolved": "https://registry.npmjs.org/expo-manifests/-/expo-manifests-0.16.6.tgz", "integrity": "sha512-1A+do6/mLUWF9xd3uCrlXr9QFDbjbfqAYmUy8UDLOjof1lMrOhyeC4Yi6WexA/A8dhZEpIxSMCKfn7G4aHAh4w==", + "dev": true, "license": "MIT", "dependencies": { "@expo/config": "~11.0.12", @@ -9140,6 +9116,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/expo-updates-interface/-/expo-updates-interface-1.1.0.tgz", "integrity": "sha512-DeB+fRe0hUDPZhpJ4X4bFMAItatFBUPjw/TVSbJsaf3Exeami+2qbbJhWkcTMoYHOB73nOIcaYcWXYJnCJXO0w==", + "dev": true, "license": "MIT", "peerDependencies": { "expo": "*" @@ -9231,7 +9208,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fast-xml-parser": { "version": "4.5.3", @@ -9755,7 +9733,8 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true, - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.2", @@ -10091,7 +10070,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -10165,7 +10143,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "devOptional": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -10182,7 +10159,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=4" @@ -10937,7 +10913,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -11281,7 +11256,8 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jest-get-type": { "version": "29.6.3", @@ -11954,7 +11930,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "devOptional": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -12336,6 +12311,7 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6.11.5" } @@ -12359,7 +12335,6 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, "license": "MIT" }, "node_modules/lodash.debounce": { @@ -12601,7 +12576,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "dev": true, "license": "MIT", "dependencies": { "tslib": "^2.0.3" @@ -13382,7 +13356,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "dev": true, "license": "MIT", "dependencies": { "lower-case": "^2.0.2", @@ -13916,7 +13889,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "devOptional": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -13939,7 +13911,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "devOptional": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", @@ -13999,7 +13970,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true, "license": "MIT" }, "node_modules/path-exists": { @@ -14061,7 +14031,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -14476,6 +14445,7 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -14534,7 +14504,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -14593,7 +14562,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.62.0.tgz", "integrity": "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14642,7 +14610,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.6.tgz", "integrity": "sha512-kvIWSmf4QPfY41HC25TR285N7Fv0Pyn3DAEK8qRL9dA35usSaxsJkHfw+VqnonqJjXOaoKCEanwudRAJ60TBGA==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.79.6", @@ -14862,7 +14829,6 @@ "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-3.17.5.tgz", "integrity": "sha512-SxBK7wQfJ4UoWoJqQnmIC7ZjuNgVb9rcY5Xc67upXAFKftWg0rnkknTw6vgwnjRcvYThrjzUVti66XoZdDJGtw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/plugin-transform-arrow-functions": "^7.0.0-0", "@babel/plugin-transform-class-properties": "^7.0.0-0", @@ -14898,7 +14864,6 @@ "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz", "integrity": "sha512-/wJE58HLEAkATzhhX1xSr+fostLsK8Q97EfpfMDKo8jlOc1QKESSX/FQrhk7HhQH/2uSaox4Y86sNaI02kteiA==", "license": "MIT", - "peer": true, "peerDependencies": { "react": "*", "react-native": "*" @@ -14909,7 +14874,6 @@ "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.15.4.tgz", "integrity": "sha512-aKHPDScUbpQiZEG9eZssHdG5jEQs4yiJ8eMx6g81Ex/xU7DZkv3911enzdCb+v4eJE79X8waizY0ZhauZJQmrw==", "license": "MIT", - "peer": true, "dependencies": { "react-freeze": "^1.0.0", "react-native-is-edge-to-edge": "^1.2.1", @@ -14925,7 +14889,6 @@ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz", "integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==", "license": "MIT", - "peer": true, "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", @@ -14940,7 +14903,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/react-native-svg-transformer/-/react-native-svg-transformer-1.5.1.tgz", "integrity": "sha512-dFvBNR8A9VPum9KCfh+LE49YiJEF8zUSnEFciKQroR/bEOhlPoZA0SuQ0qNk7m2iZl2w59FYjdRe0pMHWMDl0Q==", - "dev": true, "license": "MIT", "dependencies": { "@svgr/core": "^8.1.0", @@ -14968,7 +14930,6 @@ "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.13.5.tgz", "integrity": "sha512-MfC2B+woL4Hlj2WCzcb1USySKk+SteXnUKmKktOk/H/AQy5+LuVdkPKm8SknJ0/RxaxhZ48WBoTRGaqgR137hw==", "license": "MIT", - "peer": true, "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" @@ -15536,6 +15497,7 @@ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -15574,6 +15536,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -15586,7 +15549,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/secure-remote-password": { "version": "0.3.0", @@ -15696,6 +15660,7 @@ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { "randombytes": "^2.1.0" } @@ -16107,7 +16072,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "dev": true, "license": "MIT", "dependencies": { "dot-case": "^3.0.4", @@ -16678,14 +16642,12 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "dev": true, "license": "MIT" }, "node_modules/svgo": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "dev": true, "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", @@ -16711,7 +16673,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -16721,7 +16682,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, "license": "MIT", "dependencies": { "mdn-data": "2.0.30", @@ -16735,7 +16695,6 @@ "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "dev": true, "license": "CC0-1.0" }, "node_modules/symbol-tree": { @@ -16751,6 +16710,7 @@ "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" }, @@ -16849,6 +16809,7 @@ "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", @@ -16884,6 +16845,7 @@ "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -16899,6 +16861,7 @@ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -17131,7 +17094,6 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -17266,7 +17228,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -17475,6 +17436,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" @@ -17629,6 +17591,7 @@ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -17662,6 +17625,7 @@ "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -17721,6 +17685,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -17735,6 +17700,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json index 9e3990b5f..cc4fa0fcf 100644 --- a/apps/mobile-app/package.json +++ b/apps/mobile-app/package.json @@ -38,10 +38,8 @@ "@types/jsrsasign": "^10.5.15", "expo": "^53.0.22", "expo-blur": "~14.1.5", - "expo-camera": "^16.1.11", "expo-clipboard": "~7.1.5", "expo-constants": "~17.1.7", - "expo-dev-client": "~5.1.8", "expo-document-picker": "~13.1.6", "expo-file-system": "~18.1.11", "expo-font": "~13.3.2", @@ -58,8 +56,7 @@ "expo-web-browser": "~14.2.0", "fbemitter": "^3.0.0", "i18next": "^25.3.2", - "jest": "~29.7.0", - "jest-expo": "~53.0.10", + "lodash": "^4.17.21", "otpauth": "^9.4.0", "react": "19.0.0", "react-hook-form": "^7.56.1", @@ -76,6 +73,7 @@ "react-native-safe-area-context": "5.6.1", "react-native-screens": "~4.15.4", "react-native-svg": "15.11.2", + "react-native-svg-transformer": "^1.5.0", "react-native-toast-message": "^2.2.1", "react-native-webview": "13.13.5", "secure-remote-password": "github:LinusU/secure-remote-password#73e5f732b6ca0cdbdc19da1a0c5f48cdbad2cbf0", @@ -98,10 +96,10 @@ "eslint-config-expo": "~9.2.0", "eslint-plugin-jsdoc": "^55.2.0", "eslint-plugin-react-native": "^5.0.0", + "expo-dev-client": "~5.1.8", "globals": "^16.3.0", "jest": "^29.2.1", "jest-expo": "~53.0.0", - "react-native-svg-transformer": "^1.5.0", "typescript": "~5.8.3" }, "engines": { diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index bec75b1ee..86fa6d072 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -105,6 +105,13 @@ export interface Spec extends TurboModule { // Re-authentication methods // Authenticate user with biometric or PIN. If title/subtitle are null/empty, defaults to "Unlock Vault" context. authenticateUser(title: string | null, subtitle: string | null): Promise; + + // QR code scanner + // Scan a QR code and return the scanned data. Returns null if cancelled or failed. + // If prefixes is provided, only QR codes starting with one of these prefixes will be accepted. + // Scanner will keep scanning until a matching code is found or user cancels. + // statusText is the message to display on the scanner screen (defaults to "Scan QR code" if null/empty). + scanQRCode(prefixes: string[] | null, statusText: string | null): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager'); diff --git a/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/General.fi.resx b/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/General.fi.resx index 263745c2d..5e553e093 100644 --- a/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/General.fi.resx +++ b/apps/server/AliasVault.Client/Resources/Pages/Main/Settings/General.fi.resx @@ -71,11 +71,11 @@ - Identity Generator Settings + Henkilöllisyysgeneraattorin asetukset Title for identity generator settings section - Language + Kieli Label for alias generation language setting @@ -83,7 +83,7 @@ Description for alias generation language setting - Gender + Sukupuoli Label for alias generation gender setting @@ -103,7 +103,7 @@ Female gender option - Age range + Ikähaarukka Label for alias generation age range setting diff --git a/apps/server/AliasVault.Client/Resources/SharedResources.fr.resx b/apps/server/AliasVault.Client/Resources/SharedResources.fr.resx index b69663136..5eef1a724 100644 --- a/apps/server/AliasVault.Client/Resources/SharedResources.fr.resx +++ b/apps/server/AliasVault.Client/Resources/SharedResources.fr.resx @@ -213,7 +213,7 @@ Generic error message - An unknown error occurred. Please try again. + Une erreur inconnue s'est produite. Merci de réessayer. Generic unknown error message @@ -289,7 +289,7 @@ - or + ou Divider text between options