diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 439833b6dc..c9d8acac7e 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -10,6 +10,7 @@ import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -122,4 +123,6 @@ interface ApplicationEntryPoint { fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider fun getExcludedBlockchains(): ExcludedBlockchains + + fun getClipboardManager(): ClipboardManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 1bc3c9a85e..f917e99f42 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -20,6 +20,7 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.WalletForm import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager @@ -193,6 +194,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { private val excludedBlockchains: ExcludedBlockchains get() = entryPoint.getExcludedBlockchains() + + private val clipboardManager: ClipboardManager + get() = entryPoint.getClipboardManager() // endregion override fun onCreate() { @@ -275,6 +279,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory { onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, appPreferencesStore = appPreferencesStore, + clipboardManager = clipboardManager, ), ), ) diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt index 8b7ee07933..3d2ac51e00 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/DefaultClipboardManager.kt @@ -1,12 +1,33 @@ package com.tangem.tap.common.clipboard import android.content.ClipData +import android.content.ClipDescription +import android.os.Build +import android.os.PersistableBundle import com.tangem.core.ui.clipboard.ClipboardManager import android.content.ClipboardManager as AndroidClipboardManager internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager { - override fun setText(label: String, text: String) { - clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text)) + override fun setText(text: String, isSensitive: Boolean, label: String) { + val clip = ClipData.newPlainText(label, text).apply { + if (isSensitive) description.setAsSensitive() + } + + clipboardManager.setPrimaryClip(clip) + } + + private fun ClipDescription.setAsSensitive() { + extras = PersistableBundle().apply { + putBoolean(getExtraIsSensitiveFlag(), true) + } + } + + private fun getExtraIsSensitiveFlag(): String { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ClipDescription.EXTRA_IS_SENSITIVE + } else { + "android.content.extra.IS_SENSITIVE" + } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt index bdab75088f..49b339455c 100644 --- a/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt +++ b/app/src/main/java/com/tangem/tap/common/clipboard/MockClipboardManager.kt @@ -4,8 +4,8 @@ import com.tangem.core.ui.clipboard.ClipboardManager import timber.log.Timber internal object MockClipboardManager : ClipboardManager { - override fun setText(label: String, text: String) { - /** Intentionnaly do nothing */ + + override fun setText(text: String, isSensitive: Boolean, label: String) { Timber.w("Clipboard Manager not available") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt index 8b2cb093dd..67d2dccd98 100644 --- a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt @@ -10,6 +10,7 @@ import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.extensions.* import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.domain.model.WalletAddressData +import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding @@ -51,7 +52,7 @@ internal class AddressInfoBottomSheetDialog( tvAddress.text = data.address btnFlCopyAddress.setOnClickListener { Analytics.send(Token.Receive.ButtonCopyAddress()) - context.copyToClipboard(data.address) + store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true) } btnFlShare.setOnClickListener { Analytics.send(Token.Receive.ButtonShareAddress()) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index db4a2735bd..ef18e4fe77 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -75,4 +76,5 @@ data class DaggerGraphState( val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, val appPreferencesStore: AppPreferencesStore? = null, + val clipboardManager: ClipboardManager? = null, ) : StateType \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt index 2e6354a2e2..0ca96e8d67 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/clipboard/ClipboardManager.kt @@ -9,6 +9,12 @@ import androidx.compose.runtime.Stable @Stable interface ClipboardManager { - /** Copies to clipboard [text] with optional [label] */ - fun setText(label: String = "", text: String) + /** + * Copies to clipboard [text] with optional [label] + * + * @param text text + * @param isSensitive flag that determines whether the text is sensitive + * @param label label + */ + fun setText(text: String, isSensitive: Boolean, label: String = "") } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt index 4a87ade6e2..7b35b7d824 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheet.kt @@ -20,10 +20,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -78,7 +76,7 @@ private fun TokenReceiveBottomSheetContent(content: TokenReceiveBottomSheetConfi address = selectedAddress.value, snackbarHostState = snackbarHostState, onShareClick = content.onShareClick, - onCopyClick = content.onCopyClick, + onCopyClick = { content.onCopyClick(selectedAddress.value) }, ) } } @@ -236,7 +234,6 @@ private fun Buttons( modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val clipboardManager = LocalClipboardManager.current val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val resources = context.resources @@ -253,7 +250,6 @@ private fun Buttons( onCopyClick() hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clipboardManager.setText(AnnotatedString(address)) coroutineScope.launch { snackbarHostState.showSnackbar( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt index 29d19f5300..f51e8efef1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/tokenreceive/TokenReceiveBottomSheetConfig.kt @@ -9,6 +9,6 @@ data class TokenReceiveBottomSheetConfig( val network: String, val addresses: ImmutableList, val showMemoDisclaimer: Boolean, - val onCopyClick: () -> Unit, + val onCopyClick: (String) -> Unit, val onShareClick: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt index 8ed4841f87..cad8517d8c 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUiManager.kt @@ -152,7 +152,7 @@ internal class ManageTokensUiManager( private fun copyContractAddress(source: ManagedCryptoCurrency.SourceNetwork) { if (source is ManagedCryptoCurrency.SourceNetwork.Default) { - clipboardManager.setText(text = source.contractAddress) + clipboardManager.setText(text = source.contractAddress, isSensitive = false) showSnackbarMessage(resourceReference(R.string.contract_address_copied_message)) } } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 34923a5f2d..70e107356a 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -126,7 +126,7 @@ internal class TokenActionsHandler @AssistedInject constructor( .toImmutableList() val defaultAddress = addresses.firstOrNull()?.value ?: return - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 5e15fa4efc..acf4c8236d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -249,7 +249,7 @@ internal class TokenDetailsStateFactory( fun getStateWithReceiveBottomSheet( currency: CryptoCurrency, networkAddress: NetworkAddress, - sendCopyAnalyticsEvent: () -> Unit, + onCopyClick: (String) -> Unit, sendShareAnalyticsEvent: () -> Unit, ): TokenDetailsState { return currentStateProvider().copy( @@ -262,7 +262,7 @@ internal class TokenDetailsStateFactory( network = currency.network.name, addresses = networkAddress.availableAddresses.mapToAddressModels(currency).toImmutableList(), showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, - onCopyClick = sendCopyAnalyticsEvent, + onCopyClick = onCopyClick, onShareClick = sendShareAnalyticsEvent, ), ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 480de2e7f2..99c375e49e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType -import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle @@ -589,8 +588,9 @@ internal class TokenDetailsViewModel @Inject constructor( internalUiState.value = stateFactory.getStateWithReceiveBottomSheet( currency = cryptoCurrency, networkAddress = networkAddress, - sendCopyAnalyticsEvent = { + onCopyClick = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) }, sendShareAnalyticsEvent = { analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol)) @@ -626,7 +626,7 @@ internal class TokenDetailsViewModel @Inject constructor( ) if (extendedKey.isNotBlank()) { vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = extendedKey) + clipboardManager.setText(text = extendedKey, isSensitive = true) internalUiState.value = stateFactory.getStateAndTriggerEvent( state = internalUiState.value, errorMessage = resourceReference(R.string.wallet_notification_address_copied), @@ -864,7 +864,7 @@ internal class TokenDetailsViewModel @Inject constructor( val defaultAddress = addresses.firstOrNull()?.value ?: return null vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 50a1af41c3..1b61007fbb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -12,7 +12,7 @@ internal sealed class WalletEvent { data class ShowAlert(val state: WalletAlertState) : WalletEvent() - data class CopyAddress(val address: String) : WalletEvent() + data object CopyAddress : WalletEvent() data class RateApp(val onDismissClick: () -> Unit) : WalletEvent() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 03bf578454..17da55077b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -5,9 +5,7 @@ import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.AnnotatedString import com.tangem.core.res.getStringSafe import com.tangem.core.ui.event.EventEffect import com.tangem.core.ui.event.StateEvent @@ -32,7 +30,6 @@ internal fun WalletEventEffect( val coroutineScope = rememberCoroutineScope() val context = LocalContext.current val resources = LocalContext.current.resources - val clipboardManager = LocalClipboardManager.current EventEffect( event = event, onTrigger = { value -> @@ -45,7 +42,6 @@ internal fun WalletEventEffect( snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) } is WalletEvent.CopyAddress -> { - clipboardManager.setText(AnnotatedString(value.address)) snackbarHostState.showSnackbar( message = resources.getStringSafe(R.string.wallet_notification_address_copied), duration = SnackbarDuration.Short, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 3b15d7ee6c..f477805fed 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -222,7 +222,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( val defaultAddress = addresses.firstOrNull()?.value ?: return null vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click) - clipboardManager.setText(text = defaultAddress) + clipboardManager.setText(text = defaultAddress, isSensitive = true) analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) return resourceReference(R.string.wallet_notification_address_copied) } @@ -239,6 +239,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showMemoDisclaimer = currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE, onCopyClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) + clipboardManager.setText(text = it, isSensitive = true) }, onShareClick = { analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) @@ -255,12 +256,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( walletManagersFacade.getDefaultAddress( userWalletId = stateHolder.getSelectedWalletId(), network = cryptoCurrencyStatus.currency.network, - )?.let { + )?.let { address -> stateHolder.update(CloseBottomSheetTransformer(userWalletId = stateHolder.getSelectedWalletId())) - walletEventSender.send( - event = WalletEvent.CopyAddress(address = it), - ) + clipboardManager.setText(text = address, isSensitive = true) + walletEventSender.send(event = WalletEvent.CopyAddress) } } }