From c6203b9e1991bfb827df566dd864c230ec594fc4 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 13:47:44 +0200 Subject: [PATCH 1/8] Implement native iOS clipboard clear after delay (#1157) --- .../components/credentials/CredentialCard.tsx | 11 ++---- .../credentials/details/TotpSection.tsx | 11 ++---- .../form/FormInputCopyToClipboard.tsx | 20 ++++------ .../RCTNativeVaultManager.mm | 4 ++ .../ios/NativeVaultManager/VaultManager.swift | 31 +++++++++++++++ apps/mobile-app/specs/NativeVaultManager.ts | 1 + apps/mobile-app/utils/ClipboardUtility.ts | 38 +++++++++++++++++++ 7 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 apps/mobile-app/utils/ClipboardUtility.ts diff --git a/apps/mobile-app/components/credentials/CredentialCard.tsx b/apps/mobile-app/components/credentials/CredentialCard.tsx index 3cbd5e1cc..61ac4c43c 100644 --- a/apps/mobile-app/components/credentials/CredentialCard.tsx +++ b/apps/mobile-app/components/credentials/CredentialCard.tsx @@ -1,4 +1,3 @@ -import * as Clipboard from 'expo-clipboard'; import { router } from 'expo-router'; import { useTranslation } from 'react-i18next'; import { StyleSheet, View, Text, TouchableOpacity, Keyboard, Platform, Alert } from 'react-native'; @@ -11,7 +10,7 @@ import { useColors } from '@/hooks/useColorScheme'; import { CredentialIcon } from '@/components/credentials/CredentialIcon'; import { useAuth } from '@/context/AuthContext'; -import NativeVaultManager from '@/specs/NativeVaultManager'; +import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; type CredentialCardProps = { credential: Credential; @@ -66,15 +65,11 @@ export function CredentialCard({ credential, onCredentialDelete }: CredentialCar */ const copyToClipboard = async (text: string): Promise => { try { - await Clipboard.setStringAsync(text); - // Get clipboard clear timeout from settings const timeoutSeconds = await getClipboardClearTimeout(); - // Schedule clipboard clear if timeout is set - if (timeoutSeconds > 0) { - await NativeVaultManager.clearClipboardAfterDelay(timeoutSeconds); - } + // Use centralized clipboard utility + await copyToClipboardWithExpiration(text, timeoutSeconds); } catch (error) { console.error('Failed to copy to clipboard:', error); } diff --git a/apps/mobile-app/components/credentials/details/TotpSection.tsx b/apps/mobile-app/components/credentials/details/TotpSection.tsx index 08983b201..af15d5884 100644 --- a/apps/mobile-app/components/credentials/details/TotpSection.tsx +++ b/apps/mobile-app/components/credentials/details/TotpSection.tsx @@ -1,4 +1,3 @@ -import * as Clipboard from 'expo-clipboard'; import * as OTPAuth from 'otpauth'; import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +12,7 @@ import { ThemedText } from '@/components/themed/ThemedText'; import { ThemedView } from '@/components/themed/ThemedView'; import { useAuth } from '@/context/AuthContext'; import { useDb } from '@/context/DbContext'; -import NativeVaultManager from '@/specs/NativeVaultManager'; +import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; type TotpSectionProps = { credential: Credential; @@ -74,15 +73,11 @@ export const TotpSection: React.FC = ({ credential }) : React. */ const copyToClipboardWithClear = async (code: string): Promise => { try { - await Clipboard.setStringAsync(code); - // Get clipboard clear timeout from settings const timeoutSeconds = await getClipboardClearTimeout(); - // Schedule clipboard clear if timeout is set - if (timeoutSeconds > 0) { - await NativeVaultManager.clearClipboardAfterDelay(timeoutSeconds); - } + // Use centralized clipboard utility + await copyToClipboardWithExpiration(code, timeoutSeconds); if (Platform.OS !== 'android') { // Only show toast on iOS, Android already shows a native toast on clipboard interactions. diff --git a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx index 867747a49..80f32a37e 100644 --- a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx +++ b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx @@ -1,15 +1,14 @@ import { MaterialIcons } from '@expo/vector-icons'; -import * as Clipboard from 'expo-clipboard'; import React, { useState, useRef, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, Easing } from 'react-native'; import Toast from 'react-native-toast-message'; +import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; import { useColors } from '@/hooks/useColorScheme'; import { useAuth } from '@/context/AuthContext'; import { useClipboardCountdown } from '@/context/ClipboardCountdownContext'; -import NativeVaultManager from '@/specs/NativeVaultManager'; type FormInputCopyToClipboardProps = { label: string; @@ -30,7 +29,7 @@ const FormInputCopyToClipboard: React.FC = ({ const { t } = useTranslation(); const { getClipboardClearTimeout } = useAuth(); const { activeFieldId, setActiveField } = useClipboardCountdown(); - + const animatedWidth = useRef(new Animated.Value(0)).current; // Create a stable unique ID based on label and value const fieldId = useRef(`${label}-${value}-${Math.random().toString(36).substring(2, 11)}`).current; @@ -49,7 +48,7 @@ const FormInputCopyToClipboard: React.FC = ({ // This field is now active - reset and start animation animatedWidth.stopAnimation(); animatedWidth.setValue(100); - + // Get timeout and start animation getClipboardClearTimeout().then((timeoutSeconds) => { if (timeoutSeconds > 0 && activeFieldId === fieldId) { @@ -78,20 +77,17 @@ const FormInputCopyToClipboard: React.FC = ({ const copyToClipboard = async () : Promise => { if (value) { try { - // Copy to clipboard using expo-clipboard - await Clipboard.setStringAsync(value); - // Get clipboard clear timeout from settings const timeoutSeconds = await getClipboardClearTimeout(); - // Schedule clipboard clear if timeout is set + // Use centralized clipboard utility + await copyToClipboardWithExpiration(value, timeoutSeconds); + + // Handle animation state if (timeoutSeconds > 0) { // Clear any existing active field first (this will cancel its animation) setActiveField(null); - - // Schedule the clipboard clear - await NativeVaultManager.clearClipboardAfterDelay(timeoutSeconds); - + /* * Now set this field as active - animation will be handled by the effect * Use setTimeout to ensure state update happens in next tick diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index a3e42655f..fc798bc86 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -137,4 +137,8 @@ [vaultManager openAutofillSettingsPage:resolve rejecter:reject]; } +- (void)copyToClipboardWithExpiration:(NSString *)text expirationSeconds:(double)expirationSeconds resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + [vaultManager copyToClipboardWithExpiration:text expirationSeconds:expirationSeconds resolver:resolve rejecter:reject]; +} + @end diff --git a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift index 7a0b7384c..c804bf7d8 100644 --- a/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift +++ b/apps/mobile-app/ios/NativeVaultManager/VaultManager.swift @@ -380,6 +380,37 @@ public class VaultManager: NSObject { } } + @objc + func copyToClipboardWithExpiration(_ text: String, + expirationSeconds: Double, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock) { + NSLog("VaultManager: Copying to clipboard with expiration of %.0f seconds", expirationSeconds) + + DispatchQueue.main.async { + if expirationSeconds > 0 { + // Create expiration date + let expirationDate = Date().addingTimeInterval(expirationSeconds) + + // Set clipboard with expiration and local-only options + UIPasteboard.general.setItems( + [[UIPasteboard.typeAutomatic: text]], + options: [ + .expirationDate: expirationDate, + .localOnly: true // Prevent sync to Universal Clipboard/iCloud + ] + ) + + NSLog("VaultManager: Text copied to clipboard with expiration at %@", expirationDate.description) + } else { + // No expiration, just copy normally + UIPasteboard.general.string = text + NSLog("VaultManager: Text copied to clipboard without expiration") + } + resolve(nil) + } + } + @objc func requiresMainQueueSetup() -> Bool { return false diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index a3b82404d..fa88f437b 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -40,6 +40,7 @@ export interface Spec extends TurboModule { // Clipboard management clearClipboardAfterDelay(delayInSeconds: number): Promise; + copyToClipboardWithExpiration(text: string, expirationSeconds: number): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager'); diff --git a/apps/mobile-app/utils/ClipboardUtility.ts b/apps/mobile-app/utils/ClipboardUtility.ts new file mode 100644 index 000000000..74dc9e64b --- /dev/null +++ b/apps/mobile-app/utils/ClipboardUtility.ts @@ -0,0 +1,38 @@ +import * as Clipboard from 'expo-clipboard'; +import { Platform } from 'react-native'; + +import NativeVaultManager from '@/specs/NativeVaultManager'; + +/** + * Copy text to clipboard with automatic expiration based on platform capabilities. + * On iOS, uses native clipboard expiration. On Android, schedules manual clear. + * + * @param text - The text to copy to clipboard + * @param expirationSeconds - Number of seconds after which clipboard should be cleared (0 = no expiration) + */ +export async function copyToClipboardWithExpiration( + text: string, + expirationSeconds: number +): Promise { + if (Platform.OS === 'ios') { + // Use native iOS method with built-in expiration + await NativeVaultManager.copyToClipboardWithExpiration(text, expirationSeconds); + } else { + // For Android, use expo-clipboard and schedule manual clear + await Clipboard.setStringAsync(text); + + // Schedule clipboard clear if timeout is set + if (expirationSeconds > 0) { + await NativeVaultManager.clearClipboardAfterDelay(expirationSeconds); + } + } +} + +/** + * Copy text to clipboard without expiration. + * + * @param text - The text to copy to clipboard + */ +export async function copyToClipboard(text: string): Promise { + await copyToClipboardWithExpiration(text, 0); +} \ No newline at end of file From 819924c6e2712423bb79cf88c979cfa924ad1210 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 15:33:53 +0200 Subject: [PATCH 2/8] Add android precise alarm timing implementation for clipboard clear (#1157) --- .../android/app/src/main/AndroidManifest.xml | 2 + .../ClipboardClearReceiver.kt | 31 +++ .../nativevaultmanager/NativeVaultManager.kt | 208 +++++++++++++++++- .../app/(tabs)/settings/clipboard-clear.tsx | 150 ++++++++++++- apps/mobile-app/constants/Colors.ts | 4 + apps/mobile-app/i18n/locales/en.json | 6 + apps/mobile-app/specs/NativeVaultManager.ts | 4 + apps/mobile-app/utils/ClipboardUtility.ts | 24 +- 8 files changed, 400 insertions(+), 29 deletions(-) create mode 100644 apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt diff --git a/apps/mobile-app/android/app/src/main/AndroidManifest.xml b/apps/mobile-app/android/app/src/main/AndroidManifest.xml index 76b0f337b..6bfee34f3 100644 --- a/apps/mobile-app/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile-app/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -35,5 +36,6 @@ + diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt new file mode 100644 index 000000000..717f0429b --- /dev/null +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt @@ -0,0 +1,31 @@ +package net.aliasvault.app.nativevaultmanager + +import android.content.BroadcastReceiver +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.util.Log + +/** + * BroadcastReceiver to clear the clipboard when triggered by AlarmManager. + * This ensures clipboard clearing works even when the app is in the background. + */ +class ClipboardClearReceiver : BroadcastReceiver() { + + companion object { + private const val TAG = "ClipboardClearReceiver" + } + + override fun onReceive(context: Context, intent: Intent) { + try { + Log.d(TAG, "Received broadcast to clear clipboard") + + val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboardManager.clearPrimaryClip() + + Log.d(TAG, "Clipboard cleared successfully from broadcast receiver") + } catch (e: Exception) { + Log.e(TAG, "Error clearing clipboard from broadcast receiver", e) + } + } +} 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 08023b9d5..0aaa87337 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 @@ -1,6 +1,7 @@ package net.aliasvault.app.nativevaultmanager import android.content.Intent +import android.net.Uri import android.provider.Settings import android.util.Log import androidx.core.net.toUri @@ -498,23 +499,208 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : return } - val handler = android.os.Handler(android.os.Looper.getMainLooper()) - val delayMs = (delayInSeconds * 1000).toLong() + // Use AlarmManager to ensure execution even if app is backgrounded + try { + val alarmManager = reactApplicationContext.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager - handler.postDelayed({ - try { - Log.d(TAG, "Clearing clipboard after $delayInSeconds seconds delay") - val clipboardManager = reactApplicationContext.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager - clipboardManager.clearPrimaryClip() - Log.d(TAG, "Clipboard cleared successfully") - } catch (e: Exception) { - Log.e(TAG, "Error clearing clipboard", e) + // Check if we can schedule exact alarms (Android 12+/API 31+) + val canScheduleExactAlarms = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + alarmManager.canScheduleExactAlarms() + } else { + true // Pre-Android 12 doesn't require permission } - }, delayMs) + + if (!canScheduleExactAlarms) { + Log.w(TAG, "Cannot schedule exact alarms - permission denied. Falling back to Handler.") + throw SecurityException("Exact alarm permission not granted") + } + + val intent = Intent(reactApplicationContext, ClipboardClearReceiver::class.java) + val pendingIntent = android.app.PendingIntent.getBroadcast( + reactApplicationContext, + 0, + intent, + android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE, + ) + + // Cancel any existing alarm + alarmManager.cancel(pendingIntent) + + // Set new alarm + val triggerTime = System.currentTimeMillis() + (delayInSeconds * 1000).toLong() + + try { + alarmManager.setExactAndAllowWhileIdle( + android.app.AlarmManager.RTC_WAKEUP, + triggerTime, + pendingIntent, + ) + Log.d(TAG, "Scheduled clipboard clear using AlarmManager for $delayInSeconds seconds") + } catch (securityException: SecurityException) { + Log.w(TAG, "SecurityException when scheduling exact alarm: ${securityException.message}") + throw securityException + } + } catch (e: Exception) { + when (e) { + is SecurityException -> { + Log.w(TAG, "Exact alarm permission denied. Using inexact alarm fallback.") + // Try inexact alarm as fallback + try { + val alarmManager = reactApplicationContext.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager + val intent = Intent(reactApplicationContext, ClipboardClearReceiver::class.java) + val pendingIntent = android.app.PendingIntent.getBroadcast( + reactApplicationContext, + 0, + intent, + android.app.PendingIntent.FLAG_UPDATE_CURRENT or android.app.PendingIntent.FLAG_IMMUTABLE, + ) + + val triggerTime = System.currentTimeMillis() + (delayInSeconds * 1000).toLong() + alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent) + Log.d(TAG, "Scheduled inexact clipboard clear using AlarmManager for ~$delayInSeconds seconds") + } catch (fallbackException: Exception) { + Log.e(TAG, "Fallback inexact alarm also failed, using Handler: ${fallbackException.message}") + useHandlerFallback(delayInSeconds) + } + } + else -> { + Log.e(TAG, "Error scheduling clipboard clear with AlarmManager, using Handler: ${e.message}") + useHandlerFallback(delayInSeconds) + } + } + } promise?.resolve(null) } + /** + * Fallback method to clear clipboard using Handler when AlarmManager fails. + */ + private fun useHandlerFallback(delayInSeconds: Double) { + val handler = android.os.Handler(android.os.Looper.getMainLooper()) + val delayMs = (delayInSeconds * 1000).toLong() + handler.postDelayed({ + try { + val clipboardManager = reactApplicationContext.getSystemService( + android.content.Context.CLIPBOARD_SERVICE, + ) as android.content.ClipboardManager + clipboardManager.clearPrimaryClip() + Log.d(TAG, "Clipboard cleared using Handler fallback after $delayInSeconds seconds") + } catch (e: Exception) { + Log.e(TAG, "Error clearing clipboard with Handler fallback", e) + } + }, delayMs) + } + + /** + * Copy text to clipboard with automatic expiration. + * @param text The text to copy to clipboard + * @param expirationSeconds The number of seconds after which to clear the clipboard + * @param promise The promise to resolve + */ + @ReactMethod + override fun copyToClipboardWithExpiration(text: String, expirationSeconds: Double, promise: Promise?) { + try { + Log.d(TAG, "Copying to clipboard with expiration of $expirationSeconds seconds") + + val clipboardManager = reactApplicationContext.getSystemService( + android.content.Context.CLIPBOARD_SERVICE, + ) as android.content.ClipboardManager + val clip = android.content.ClipData.newPlainText("AliasVault", text) + + // Android 13+ handling + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU && expirationSeconds > 0) { + // Mark as sensitive to prevent preview display + val persistableBundle = android.os.PersistableBundle() + persistableBundle.putBoolean(android.content.ClipDescription.EXTRA_IS_SENSITIVE, true) + clip.description.extras = persistableBundle + + // Android 13+ automatically clears clipboard after 1 hour (3600 seconds) + val androidAutoClearSeconds = 3600.0 + + if (expirationSeconds <= androidAutoClearSeconds) { + // For shorter delays, we still need manual clearing for precision + Log.d(TAG, "Using manual clearing for $expirationSeconds seconds (Android 13+ with sensitive flag)") + clipboardManager.setPrimaryClip(clip) + clearClipboardAfterDelay(expirationSeconds, null) + } else { + // For longer delays, rely on Android's automatic clearing + Log.d(TAG, "Relying on Android 13+ automatic clipboard clearing (${androidAutoClearSeconds}s)") + clipboardManager.setPrimaryClip(clip) + // No manual clearing needed - Android will handle it + } + } else { + // Pre-Android 13 or no expiration + clipboardManager.setPrimaryClip(clip) + + if (expirationSeconds > 0) { + Log.d(TAG, "Using manual clearing for $expirationSeconds seconds (pre-Android 13)") + clearClipboardAfterDelay(expirationSeconds, null) + } + } + + Log.d(TAG, "Text copied to clipboard successfully") + promise?.resolve(null) + } catch (e: Exception) { + Log.e(TAG, "Error copying to clipboard", e) + promise?.reject("ERR_CLIPBOARD", "Failed to copy to clipboard: ${e.message}", e) + } + } + + /** + * Check if the app can schedule exact alarms. + * @param promise The promise to resolve with boolean result + */ + @ReactMethod + override fun canScheduleExactAlarms(promise: Promise?) { + try { + val canSchedule = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + val alarmManager = reactApplicationContext.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager + alarmManager.canScheduleExactAlarms() + } else { + true // Pre-Android 12 doesn't require permission + } + Log.d(TAG, "Can schedule exact alarms: $canSchedule") + promise?.resolve(canSchedule) + } catch (e: Exception) { + Log.e(TAG, "Error checking exact alarm permission", e) + promise?.reject("ERR_EXACT_ALARM_CHECK", "Failed to check exact alarm permission: ${e.message}", e) + } + } + + /** + * Request exact alarm permission by opening system settings. + * @param promise The promise to resolve + */ + @ReactMethod + override fun requestExactAlarmPermission(promise: Promise?) { + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + val alarmManager = reactApplicationContext.getSystemService(android.content.Context.ALARM_SERVICE) as android.app.AlarmManager + + if (!alarmManager.canScheduleExactAlarms()) { + Log.d(TAG, "Requesting exact alarm permission via system settings") + val intent = Intent().apply { + action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM + data = Uri.parse("package:${reactApplicationContext.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + reactApplicationContext.startActivity(intent) + promise?.resolve("Permission request sent - user will be taken to settings") + } else { + Log.d(TAG, "Exact alarm permission already granted") + promise?.resolve("Permission already granted") + } + } else { + Log.d(TAG, "Exact alarm permission not required on this Android version") + promise?.resolve("Permission not required on this Android version") + } + } catch (e: Exception) { + Log.e(TAG, "Error requesting exact alarm permission", e) + promise?.reject("ERR_EXACT_ALARM_REQUEST", "Failed to request exact alarm permission: ${e.message}", e) + } + } + /** * Get the auth methods. * @param promise The promise to resolve diff --git a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx index 0d7998850..25b1887d3 100644 --- a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx +++ b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx @@ -1,6 +1,6 @@ import { Ionicons } from '@expo/vector-icons'; -import { useState, useEffect } from 'react'; -import { StyleSheet, View, TouchableOpacity, Platform } from 'react-native'; +import { useState, useEffect, useRef } from 'react'; +import { StyleSheet, View, TouchableOpacity, Platform, AppState } from 'react-native'; import { useColors } from '@/hooks/useColorScheme'; import { useTranslation } from '@/hooks/useTranslation'; @@ -9,6 +9,7 @@ import { ThemedContainer } from '@/components/themed/ThemedContainer'; import { ThemedScrollView } from '@/components/themed/ThemedScrollView'; import { ThemedText } from '@/components/themed/ThemedText'; import { useAuth } from '@/context/AuthContext'; +import NativeVaultManager from '@/specs/NativeVaultManager'; const TIMEOUT_OPTIONS = [ { value: 0, label: 'settings.clipboardClearOptions.never' }, @@ -26,6 +27,8 @@ export default function ClipboardClearScreen(): React.ReactNode { const { t } = useTranslation(); const { getClipboardClearTimeout, setClipboardClearTimeout } = useAuth(); const [selectedTimeout, setSelectedTimeout] = useState(10); + const [canScheduleExactAlarms, setCanScheduleExactAlarms] = useState(true); + const appState = useRef(AppState.currentState); useEffect(() => { /** @@ -36,7 +39,39 @@ export default function ClipboardClearScreen(): React.ReactNode { setSelectedTimeout(timeout); }; + /** + * Check exact alarm permission status on Android. + */ + const checkExactAlarmPermission = async (): Promise => { + if (Platform.OS === 'android') { + try { + const canSchedule = await NativeVaultManager.canScheduleExactAlarms(); + setCanScheduleExactAlarms(canSchedule); + } catch (error) { + console.error('Error checking exact alarm permission:', error); + // Default to true to avoid showing the help section unnecessarily + setCanScheduleExactAlarms(true); + } + } + }; + loadCurrentTimeout(); + checkExactAlarmPermission(); + + // Listen for app state changes to re-check permission when app comes to foreground + const subscription = AppState.addEventListener('change', async (nextAppState) => { + if (appState.current.match(/inactive|background/) && nextAppState === 'active') { + // App coming to foreground, re-check exact alarm permission + if (Platform.OS === 'android') { + await checkExactAlarmPermission(); + } + } + appState.current = nextAppState; + }); + + return (): void => { + subscription.remove(); + }; }, [getClipboardClearTimeout]); /** @@ -47,11 +82,61 @@ export default function ClipboardClearScreen(): React.ReactNode { setSelectedTimeout(timeout); }; + /** + * Handle exact alarm permission request. + */ + const handleRequestExactAlarmPermission = async (): Promise => { + if (Platform.OS !== 'android') { + return; + } + + try { + await NativeVaultManager.requestExactAlarmPermission(); + } catch (error) { + console.error('Error handling exact alarm permission request:', error); + } + }; + const styles = StyleSheet.create({ headerText: { color: colors.textMuted, fontSize: 13, }, + helpButton: { + alignItems: 'center', + backgroundColor: colors.primary, + borderRadius: 8, + flexDirection: 'row', + justifyContent: 'center', + paddingVertical: 12, + }, + helpButtonDisabled: { + backgroundColor: colors.textMuted, + }, + helpButtonText: { + color: colors.background, + fontSize: 14, + fontWeight: '600', + marginLeft: 8, + }, + helpContainer: { + backgroundColor: colors.accentBackground, + borderRadius: 10, + marginTop: 16, + padding: 16, + }, + helpDescription: { + color: colors.textMuted, + fontSize: 13, + lineHeight: 20, + marginBottom: 12, + }, + helpTitle: { + color: colors.text, + fontSize: 16, + fontWeight: '600', + marginBottom: 8, + }, option: { alignItems: 'center', borderBottomColor: colors.accentBorder, @@ -73,6 +158,21 @@ export default function ClipboardClearScreen(): React.ReactNode { flex: 1, fontSize: 16, }, + permissionDenied: { + color: colors.destructive || '#EF4444', + }, + permissionGranted: { + color: colors.success || '#10B981', + }, + permissionStatusContainer: { + alignItems: 'center', + flexDirection: 'row', + marginBottom: 12, + }, + permissionStatusText: { + fontSize: 13, + marginLeft: 8, + }, selectedIcon: { color: colors.primary, marginLeft: 8, @@ -117,6 +217,52 @@ export default function ClipboardClearScreen(): React.ReactNode { )} + {Platform.OS === 'android' && !canScheduleExactAlarms && selectedTimeout > 0 && ( + + + {t('settings.exactAlarmHelpTitle')} + + + + + {t('settings.exactAlarmPermissionDenied')} + + + + {t('settings.exactAlarmHelpDescription')} + + + + + {t('settings.enableExactAlarms')} + + + + )} + {Platform.OS === 'android' && canScheduleExactAlarms && selectedTimeout > 0 && ( + + + + + {t('settings.exactAlarmPermissionGranted')} + + + + {t('settings.exactAlarmEnabledDescription')} + + + )} ); diff --git a/apps/mobile-app/constants/Colors.ts b/apps/mobile-app/constants/Colors.ts index 9ed4e22eb..d61652227 100644 --- a/apps/mobile-app/constants/Colors.ts +++ b/apps/mobile-app/constants/Colors.ts @@ -31,6 +31,8 @@ export const Colors = { red: '#ff0000', black: '#000000', modalBackground: 'rgba(0, 0, 0, 0.5)', + success: '#10B981', + destructive: '#EF4444', }, dark: { white: '#ffffff', @@ -60,6 +62,8 @@ export const Colors = { red: '#ff0000', black: '#000000', modalBackground: 'rgba(88, 88, 88, 0.5)', + success: '#10B981', + destructive: '#EF4444', }, } as const; diff --git a/apps/mobile-app/i18n/locales/en.json b/apps/mobile-app/i18n/locales/en.json index f37a12173..d8ef50fb2 100644 --- a/apps/mobile-app/i18n/locales/en.json +++ b/apps/mobile-app/i18n/locales/en.json @@ -213,6 +213,12 @@ "15seconds": "15 seconds", "30seconds": "30 seconds" }, + "exactAlarmHelpTitle": "Improve Clipboard Clearing Accuracy", + "exactAlarmPermissionDenied": "Missing permission", + "exactAlarmPermissionGranted": "Permission granted", + "exactAlarmHelpDescription": "Some Android devices require special permission for precise background clipboard clearing. Without this permission, AliasVault uses approximate timing which may be less reliable when the app is in the background.", + "exactAlarmEnabledDescription": "AliasVault can clear your clipboard with precise timing, even when the app is in the background.", + "enableExactAlarms": "Open app settings", "identityGenerator": "Identity Generator", "security": "Security", "appVersion": "App version {{version}} ({{url}})", diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index fa88f437b..4ee5b3f2c 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -41,6 +41,10 @@ export interface Spec extends TurboModule { // Clipboard management clearClipboardAfterDelay(delayInSeconds: number): Promise; copyToClipboardWithExpiration(text: string, expirationSeconds: number): Promise; + + // Exact alarm permission management + canScheduleExactAlarms(): Promise; + requestExactAlarmPermission(): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager'); diff --git a/apps/mobile-app/utils/ClipboardUtility.ts b/apps/mobile-app/utils/ClipboardUtility.ts index 74dc9e64b..a2e091f1f 100644 --- a/apps/mobile-app/utils/ClipboardUtility.ts +++ b/apps/mobile-app/utils/ClipboardUtility.ts @@ -1,11 +1,13 @@ -import * as Clipboard from 'expo-clipboard'; -import { Platform } from 'react-native'; - import NativeVaultManager from '@/specs/NativeVaultManager'; /** * Copy text to clipboard with automatic expiration based on platform capabilities. - * On iOS, uses native clipboard expiration. On Android, schedules manual clear. + * + * On iOS: Uses native clipboard expiration via UIPasteboard.setItems with expirationDate. + * On Android: Uses native method that combines clipboard copy with automatic clearing: + * - For delays ≤10 seconds: Uses Handler (reliable for short delays) + * - For delays >10 seconds: Uses AlarmManager (works even when app is backgrounded) + * - Android 13+: Also marks clipboard content as sensitive * * @param text - The text to copy to clipboard * @param expirationSeconds - Number of seconds after which clipboard should be cleared (0 = no expiration) @@ -14,18 +16,8 @@ export async function copyToClipboardWithExpiration( text: string, expirationSeconds: number ): Promise { - if (Platform.OS === 'ios') { - // Use native iOS method with built-in expiration - await NativeVaultManager.copyToClipboardWithExpiration(text, expirationSeconds); - } else { - // For Android, use expo-clipboard and schedule manual clear - await Clipboard.setStringAsync(text); - - // Schedule clipboard clear if timeout is set - if (expirationSeconds > 0) { - await NativeVaultManager.clearClipboardAfterDelay(expirationSeconds); - } - } + // Both platforms now use native methods for reliable clipboard management + await NativeVaultManager.copyToClipboardWithExpiration(text, expirationSeconds); } /** From 056f8e97e9fd3fbd26aed19d302fa12adf8dbe48 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:14:28 +0200 Subject: [PATCH 3/8] Update native vault manager package namespace (#1157) --- .../net/aliasvault/app/nativevaultmanager/NativeVaultManager.kt | 2 +- apps/mobile-app/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 0aaa87337..e3ce85986 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 @@ -6,7 +6,6 @@ import android.provider.Settings import android.util.Log import androidx.core.net.toUri import androidx.fragment.app.FragmentActivity -import com.aliasvault.nativevaultmanager.NativeVaultManagerSpec import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise @@ -19,6 +18,7 @@ import com.facebook.react.turbomodule.core.interfaces.TurboModule import net.aliasvault.app.vaultstore.VaultStore import net.aliasvault.app.vaultstore.keystoreprovider.AndroidKeystoreProvider import net.aliasvault.app.vaultstore.storageprovider.AndroidStorageProvider +import net.aliasvault.nativevaultmanager.NativeVaultManagerSpec import org.json.JSONArray /** diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json index f4127a4de..83551e59c 100644 --- a/apps/mobile-app/package.json +++ b/apps/mobile-app/package.json @@ -20,7 +20,7 @@ "type": "modules", "jsSrcsDir": "specs", "android": { - "javaPackageName": "com.aliasvault.nativevaultmanager" + "javaPackageName": "net.aliasvault.nativevaultmanager" }, "ios": { "modulesProvider": { From ab740c093fbba41ff3640b139fdef8b33a2edc9d Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:39:02 +0200 Subject: [PATCH 4/8] Add ignore battery optimization check for Android clipboard clear (#1157) --- .../android/app/src/main/AndroidManifest.xml | 1 + .../ClipboardClearReceiver.kt | 1 - .../nativevaultmanager/NativeVaultManager.kt | 55 +++++++++++++++++++ .../app/(tabs)/settings/clipboard-clear.tsx | 52 ++++++++---------- apps/mobile-app/i18n/locales/en.json | 11 ++-- apps/mobile-app/specs/NativeVaultManager.ts | 4 ++ 6 files changed, 89 insertions(+), 35 deletions(-) diff --git a/apps/mobile-app/android/app/src/main/AndroidManifest.xml b/apps/mobile-app/android/app/src/main/AndroidManifest.xml index 6bfee34f3..6657e225b 100644 --- a/apps/mobile-app/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile-app/android/app/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt index 717f0429b..eb9c84f93 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/nativevaultmanager/ClipboardClearReceiver.kt @@ -19,7 +19,6 @@ class ClipboardClearReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { try { Log.d(TAG, "Received broadcast to clear clipboard") - val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboardManager.clearPrimaryClip() 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 e3ce85986..674380ae6 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 @@ -2,6 +2,7 @@ package net.aliasvault.app.nativevaultmanager import android.content.Intent import android.net.Uri +import android.os.PowerManager import android.provider.Settings import android.util.Log import androidx.core.net.toUri @@ -701,6 +702,60 @@ class NativeVaultManager(reactContext: ReactApplicationContext) : } } + /** + * Check if the app is ignoring battery optimizations. + * @param promise The promise to resolve with boolean result + */ + @ReactMethod + override fun isIgnoringBatteryOptimizations(promise: Promise?) { + try { + val isIgnoring = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + val powerManager = reactApplicationContext.getSystemService(android.content.Context.POWER_SERVICE) as PowerManager + powerManager.isIgnoringBatteryOptimizations(reactApplicationContext.packageName) + } else { + true // Pre-Android 6.0 doesn't have battery optimization + } + Log.d(TAG, "Is ignoring battery optimizations: $isIgnoring") + promise?.resolve(isIgnoring) + } catch (e: Exception) { + Log.e(TAG, "Error checking battery optimization status", e) + promise?.reject("ERR_BATTERY_OPTIMIZATION_CHECK", "Failed to check battery optimization status: ${e.message}", e) + } + } + + /** + * Request battery optimization exemption by opening system settings. + * @param promise The promise to resolve + */ + @ReactMethod + override fun requestIgnoreBatteryOptimizations(promise: Promise?) { + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { + val powerManager = reactApplicationContext.getSystemService(android.content.Context.POWER_SERVICE) as PowerManager + + if (!powerManager.isIgnoringBatteryOptimizations(reactApplicationContext.packageName)) { + Log.d(TAG, "Requesting battery optimization exemption via system settings") + val intent = Intent().apply { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:${reactApplicationContext.packageName}") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + reactApplicationContext.startActivity(intent) + promise?.resolve("Battery optimization exemption request sent - user will be taken to settings") + } else { + Log.d(TAG, "App is already ignoring battery optimizations") + promise?.resolve("App is already ignoring battery optimizations") + } + } else { + Log.d(TAG, "Battery optimization not applicable on this Android version") + promise?.resolve("Battery optimization not applicable on this Android version") + } + } catch (e: Exception) { + Log.e(TAG, "Error requesting battery optimization exemption", e) + promise?.reject("ERR_BATTERY_OPTIMIZATION_REQUEST", "Failed to request battery optimization exemption: ${e.message}", e) + } + } + /** * Get the auth methods. * @param promise The promise to resolve diff --git a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx index 25b1887d3..75d37190e 100644 --- a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx +++ b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx @@ -27,7 +27,7 @@ export default function ClipboardClearScreen(): React.ReactNode { const { t } = useTranslation(); const { getClipboardClearTimeout, setClipboardClearTimeout } = useAuth(); const [selectedTimeout, setSelectedTimeout] = useState(10); - const [canScheduleExactAlarms, setCanScheduleExactAlarms] = useState(true); + const [isIgnoringBatteryOptimizations, setIsIgnoringBatteryOptimizations] = useState(true); const appState = useRef(AppState.currentState); useEffect(() => { @@ -40,30 +40,30 @@ export default function ClipboardClearScreen(): React.ReactNode { }; /** - * Check exact alarm permission status on Android. + * Check battery optimization exemption status on Android. */ - const checkExactAlarmPermission = async (): Promise => { + const checkBatteryOptimization = async (): Promise => { if (Platform.OS === 'android') { try { - const canSchedule = await NativeVaultManager.canScheduleExactAlarms(); - setCanScheduleExactAlarms(canSchedule); + const isIgnoring = await NativeVaultManager.isIgnoringBatteryOptimizations(); + setIsIgnoringBatteryOptimizations(isIgnoring); } catch (error) { - console.error('Error checking exact alarm permission:', error); + console.error('Error checking battery optimization status:', error); // Default to true to avoid showing the help section unnecessarily - setCanScheduleExactAlarms(true); + setIsIgnoringBatteryOptimizations(true); } } }; loadCurrentTimeout(); - checkExactAlarmPermission(); + checkBatteryOptimization(); // Listen for app state changes to re-check permission when app comes to foreground const subscription = AppState.addEventListener('change', async (nextAppState) => { if (appState.current.match(/inactive|background/) && nextAppState === 'active') { - // App coming to foreground, re-check exact alarm permission + // App coming to foreground, re-check battery optimization if (Platform.OS === 'android') { - await checkExactAlarmPermission(); + await checkBatteryOptimization(); } } appState.current = nextAppState; @@ -83,17 +83,17 @@ export default function ClipboardClearScreen(): React.ReactNode { }; /** - * Handle exact alarm permission request. + * Handle battery optimization exemption request. */ - const handleRequestExactAlarmPermission = async (): Promise => { + const handleRequestBatteryOptimizationExemption = async (): Promise => { if (Platform.OS !== 'android') { return; } try { - await NativeVaultManager.requestExactAlarmPermission(); + await NativeVaultManager.requestIgnoreBatteryOptimizations(); } catch (error) { - console.error('Error handling exact alarm permission request:', error); + console.error('Error handling battery optimization exemption request:', error); } }; @@ -167,7 +167,6 @@ export default function ClipboardClearScreen(): React.ReactNode { permissionStatusContainer: { alignItems: 'center', flexDirection: 'row', - marginBottom: 12, }, permissionStatusText: { fontSize: 13, @@ -217,36 +216,36 @@ export default function ClipboardClearScreen(): React.ReactNode { )} - {Platform.OS === 'android' && !canScheduleExactAlarms && selectedTimeout > 0 && ( + {Platform.OS === 'android' && !isIgnoringBatteryOptimizations && selectedTimeout > 0 && ( - {t('settings.exactAlarmHelpTitle')} + {t('settings.batteryOptimizationHelpTitle')} - {t('settings.exactAlarmPermissionDenied')} + {t('settings.batteryOptimizationActive')} - {t('settings.exactAlarmHelpDescription')} + {t('settings.batteryOptimizationHelpDescription')} - + - {t('settings.enableExactAlarms')} + {t('settings.disableBatteryOptimization')} )} - {Platform.OS === 'android' && canScheduleExactAlarms && selectedTimeout > 0 && ( + {Platform.OS === 'android' && isIgnoringBatteryOptimizations && selectedTimeout > 0 && ( - {t('settings.exactAlarmPermissionGranted')} + {t('settings.batteryOptimizationDisabled')} - - {t('settings.exactAlarmEnabledDescription')} - )} diff --git a/apps/mobile-app/i18n/locales/en.json b/apps/mobile-app/i18n/locales/en.json index d8ef50fb2..b3ec58d8c 100644 --- a/apps/mobile-app/i18n/locales/en.json +++ b/apps/mobile-app/i18n/locales/en.json @@ -213,12 +213,11 @@ "15seconds": "15 seconds", "30seconds": "30 seconds" }, - "exactAlarmHelpTitle": "Improve Clipboard Clearing Accuracy", - "exactAlarmPermissionDenied": "Missing permission", - "exactAlarmPermissionGranted": "Permission granted", - "exactAlarmHelpDescription": "Some Android devices require special permission for precise background clipboard clearing. Without this permission, AliasVault uses approximate timing which may be less reliable when the app is in the background.", - "exactAlarmEnabledDescription": "AliasVault can clear your clipboard with precise timing, even when the app is in the background.", - "enableExactAlarms": "Open app settings", + "batteryOptimizationHelpTitle": "Enable Background Clipboard Clearing", + "batteryOptimizationActive": "Battery optimization is blocking background tasks", + "batteryOptimizationDisabled": "Background clipboard clearing enabled", + "batteryOptimizationHelpDescription": "Android's battery optimization prevents reliable clipboard clearing when the app is in the background. Disabling battery optimization for AliasVault allows precise background clipboard clearing and automatically grants necessary alarm permissions.", + "disableBatteryOptimization": "Disable battery optimization", "identityGenerator": "Identity Generator", "security": "Security", "appVersion": "App version {{version}} ({{url}})", diff --git a/apps/mobile-app/specs/NativeVaultManager.ts b/apps/mobile-app/specs/NativeVaultManager.ts index 4ee5b3f2c..421f1e10c 100644 --- a/apps/mobile-app/specs/NativeVaultManager.ts +++ b/apps/mobile-app/specs/NativeVaultManager.ts @@ -45,6 +45,10 @@ export interface Spec extends TurboModule { // Exact alarm permission management canScheduleExactAlarms(): Promise; requestExactAlarmPermission(): Promise; + + // Battery optimization management + isIgnoringBatteryOptimizations(): Promise; + requestIgnoreBatteryOptimizations(): Promise; } export default TurboModuleRegistry.getEnforcing('NativeVaultManager'); From c4c29b11f3c5aa80577d9d086fc0378629febf9f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:43:05 +0200 Subject: [PATCH 5/8] Add stubs for new NativeVaultManager spec for iOS (#1157) --- .../RCTNativeVaultManager.mm | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm index fc798bc86..0656caceb 100644 --- a/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm +++ b/apps/mobile-app/ios/NativeVaultManager/RCTNativeVaultManager.mm @@ -141,4 +141,26 @@ [vaultManager copyToClipboardWithExpiration:text expirationSeconds:expirationSeconds resolver:resolve rejecter:reject]; } +// MARK: - Android-specific methods (stubs for iOS) + +- (void)canScheduleExactAlarms:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + // Only used by Android, return true. + resolve(@(YES)); +} + +- (void)requestExactAlarmPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + // Only used by Android, return true. + resolve(@"Not applicable on iOS"); +} + +- (void)isIgnoringBatteryOptimizations:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + // Only used by Android, return true. + resolve(@(YES)); +} + +- (void)requestIgnoreBatteryOptimizations:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { + // Only used by Android, return true. + resolve(@"Not applicable on iOS"); +} + @end From fe7da551a497a63e71bdf5ad01be05286c500f16 Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:43:15 +0200 Subject: [PATCH 6/8] Add missing translation (#1157) --- apps/mobile-app/i18n/locales/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mobile-app/i18n/locales/en.json b/apps/mobile-app/i18n/locales/en.json index b3ec58d8c..32ed26e5f 100644 --- a/apps/mobile-app/i18n/locales/en.json +++ b/apps/mobile-app/i18n/locales/en.json @@ -155,7 +155,8 @@ "deleteAttachment": "Delete", "toasts": { "credentialUpdated": "Credential updated successfully", - "credentialCreated": "Credential created successfully" + "credentialCreated": "Credential created successfully", + "credentialDeleted": "Credential deleted successfully" }, "createNewAliasFor": "Create new alias for", "errors": { From 2c98b811110d8bd2bc631a441d185d0abd70bfad Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 16:51:08 +0200 Subject: [PATCH 7/8] Update ClipboardUtility.ts (#1157) --- apps/mobile-app/utils/ClipboardUtility.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/mobile-app/utils/ClipboardUtility.ts b/apps/mobile-app/utils/ClipboardUtility.ts index a2e091f1f..1590534be 100644 --- a/apps/mobile-app/utils/ClipboardUtility.ts +++ b/apps/mobile-app/utils/ClipboardUtility.ts @@ -2,13 +2,12 @@ import NativeVaultManager from '@/specs/NativeVaultManager'; /** * Copy text to clipboard with automatic expiration based on platform capabilities. - * + * * On iOS: Uses native clipboard expiration via UIPasteboard.setItems with expirationDate. * On Android: Uses native method that combines clipboard copy with automatic clearing: - * - For delays ≤10 seconds: Uses Handler (reliable for short delays) - * - For delays >10 seconds: Uses AlarmManager (works even when app is backgrounded) + * - Uses AlarmManager (works even when app is backgrounded) * - Android 13+: Also marks clipboard content as sensitive - * + * * @param text - The text to copy to clipboard * @param expirationSeconds - Number of seconds after which clipboard should be cleared (0 = no expiration) */ @@ -22,9 +21,9 @@ export async function copyToClipboardWithExpiration( /** * Copy text to clipboard without expiration. - * + * * @param text - The text to copy to clipboard */ export async function copyToClipboard(text: string): Promise { await copyToClipboardWithExpiration(text, 0); -} \ No newline at end of file +} From 7314dc3d1d57ee3640f7b01ded23d085f706d74f Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Fri, 29 Aug 2025 18:01:35 +0200 Subject: [PATCH 8/8] Style refactor (#1157) --- apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx | 3 --- apps/mobile-app/components/credentials/CredentialCard.tsx | 2 +- apps/mobile-app/components/credentials/details/TotpSection.tsx | 2 +- apps/mobile-app/components/form/FormInputCopyToClipboard.tsx | 1 + 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx index 75d37190e..6134d48f2 100644 --- a/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx +++ b/apps/mobile-app/app/(tabs)/settings/clipboard-clear.tsx @@ -110,9 +110,6 @@ export default function ClipboardClearScreen(): React.ReactNode { justifyContent: 'center', paddingVertical: 12, }, - helpButtonDisabled: { - backgroundColor: colors.textMuted, - }, helpButtonText: { color: colors.background, fontSize: 14, diff --git a/apps/mobile-app/components/credentials/CredentialCard.tsx b/apps/mobile-app/components/credentials/CredentialCard.tsx index 61ac4c43c..3312b1390 100644 --- a/apps/mobile-app/components/credentials/CredentialCard.tsx +++ b/apps/mobile-app/components/credentials/CredentialCard.tsx @@ -4,13 +4,13 @@ import { StyleSheet, View, Text, TouchableOpacity, Keyboard, Platform, Alert } f import ContextMenu, { OnPressMenuItemEvent } from 'react-native-context-menu-view'; import Toast from 'react-native-toast-message'; +import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; import type { Credential } from '@/utils/dist/shared/models/vault'; import { useColors } from '@/hooks/useColorScheme'; import { CredentialIcon } from '@/components/credentials/CredentialIcon'; import { useAuth } from '@/context/AuthContext'; -import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; type CredentialCardProps = { credential: Credential; diff --git a/apps/mobile-app/components/credentials/details/TotpSection.tsx b/apps/mobile-app/components/credentials/details/TotpSection.tsx index af15d5884..df8d5f2a3 100644 --- a/apps/mobile-app/components/credentials/details/TotpSection.tsx +++ b/apps/mobile-app/components/credentials/details/TotpSection.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import { View, StyleSheet, TouchableOpacity, Platform } from 'react-native'; import Toast from 'react-native-toast-message'; +import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; import type { Credential, TotpCode } from '@/utils/dist/shared/models/vault'; import { useColors } from '@/hooks/useColorScheme'; @@ -12,7 +13,6 @@ import { ThemedText } from '@/components/themed/ThemedText'; import { ThemedView } from '@/components/themed/ThemedView'; import { useAuth } from '@/context/AuthContext'; import { useDb } from '@/context/DbContext'; -import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; type TotpSectionProps = { credential: Credential; diff --git a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx index 80f32a37e..3546b46db 100644 --- a/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx +++ b/apps/mobile-app/components/form/FormInputCopyToClipboard.tsx @@ -5,6 +5,7 @@ import { View, Text, TouchableOpacity, StyleSheet, Platform, Animated, Easing } import Toast from 'react-native-toast-message'; import { copyToClipboardWithExpiration } from '@/utils/ClipboardUtility'; + import { useColors } from '@/hooks/useColorScheme'; import { useAuth } from '@/context/AuthContext';