Updated on 2026-08-14
This commit is contained in:
parent
3c59214c76
commit
4c903fc654
73 changed files with 886 additions and 190 deletions
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.tap
|
||||
|
||||
import android.content.Context
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.SystemClock
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class DeviceFlipDetector(context: Context) {
|
||||
|
||||
private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
private var gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)
|
||||
|
||||
private val zAxisThreshold = -6
|
||||
private val throttleTimeMs = 3000
|
||||
private var lastTriggerTime = 0L
|
||||
private var isScreenDown = false
|
||||
|
||||
fun deviceFlipEvents(): Flow<Unit> = callbackFlow {
|
||||
val listener = object : SensorEventListener {
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
|
||||
/* no-op */
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent?) {
|
||||
event?.let {
|
||||
val currentTime = SystemClock.elapsedRealtime()
|
||||
val zAxisValue = it.values[2]
|
||||
|
||||
if (zAxisValue < zAxisThreshold && !isScreenDown) {
|
||||
isScreenDown = true
|
||||
lastTriggerTime = currentTime
|
||||
} else if (zAxisValue >= zAxisThreshold) {
|
||||
if (isScreenDown && currentTime - lastTriggerTime <= throttleTimeMs) {
|
||||
lastTriggerTime = currentTime
|
||||
trySend(Unit)
|
||||
}
|
||||
isScreenDown = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gravitySensor?.let {
|
||||
sensorManager.registerListener(listener, it, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
}
|
||||
|
||||
awaitClose { sensorManager.unregisterListener(listener) }
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ import com.tangem.datasource.config.models.Config
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.common.LogConfig
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
|
|
@ -178,6 +179,9 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
@Inject
|
||||
lateinit var appThemeModeRepository: AppThemeModeRepository
|
||||
|
||||
@Inject
|
||||
lateinit var balanceHidingRepository: BalanceHidingRepository
|
||||
|
||||
@Inject
|
||||
lateinit var detailsFeatureToggles: DetailsFeatureToggles
|
||||
|
||||
|
|
@ -207,6 +211,7 @@ class TapApplication : Application(), ImageLoaderFactory {
|
|||
appStateHolder = appStateHolder,
|
||||
currenciesRepository = currenciesRepository,
|
||||
appThemeModeRepository = appThemeModeRepository,
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
detailsFeatureToggles = detailsFeatureToggles,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.IsUserAlreadyRateAppUseCase
|
||||
import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase
|
||||
import com.tangem.domain.balancehiding.DeviceFlipDetector
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.settings.*
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.tap.domain.TangemSdkManager
|
||||
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
|
||||
|
|
@ -37,4 +39,24 @@ internal object SettingsDomainModule {
|
|||
legacySettingsRepository = DefaultLegacySettingsRepository(tangemSdkManager = tangemSdkManager),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesIsBalanceHiddenUseCase(balanceHidingRepository: BalanceHidingRepository): IsBalanceHiddenUseCase {
|
||||
return IsBalanceHiddenUseCase(
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun providesListenUseCase(
|
||||
flipDetector: DeviceFlipDetector,
|
||||
balanceHidingRepository: BalanceHidingRepository,
|
||||
): ListenToFlipsUseCase {
|
||||
return ListenToFlipsUseCase(
|
||||
flipDetector = flipDetector,
|
||||
balanceHidingRepository = balanceHidingRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -86,6 +86,10 @@ sealed class DetailsAction : Action {
|
|||
val appThemeMode: AppThemeMode,
|
||||
) : AppSettings()
|
||||
|
||||
data class ChangeBalanceHiding(
|
||||
val hideBalance: Boolean,
|
||||
) : AppSettings()
|
||||
|
||||
data class ChangeAppCurrency(
|
||||
val fiatCurrency: FiatCurrency,
|
||||
) : AppSettings()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.balancehiding.BalanceHidingSettings
|
||||
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -207,6 +208,9 @@ class DetailsMiddleware {
|
|||
is DetailsAction.AppSettings.ChangeAppThemeMode -> {
|
||||
changeAppThemeMode(action.appThemeMode)
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeBalanceHiding -> {
|
||||
changeBalanceHiding(action.hideBalance)
|
||||
}
|
||||
is DetailsAction.AppSettings.ChangeAppCurrency -> {
|
||||
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
store.dispatch(DetailsAction.ChangeAppCurrency(action.fiatCurrency))
|
||||
|
|
@ -257,6 +261,19 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun changeBalanceHiding(hideBalance: Boolean) {
|
||||
val repository = store.state.daggerGraphState.get(DaggerGraphState::balanceHidingRepository)
|
||||
|
||||
scope.launch {
|
||||
val newState = BalanceHidingSettings(
|
||||
isHidingEnabledInSettings = hideBalance,
|
||||
isBalanceHidden = false,
|
||||
)
|
||||
|
||||
repository.storeBalanceHidingSettings(newState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch {
|
||||
// Nothing to change
|
||||
if (preferencesStorage.shouldSaveUserWallets == enable) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.tangem.tap.domain.extensions.signedHashesCount
|
|||
import com.tangem.tap.preferencesStorage
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.tap.tangemSdkManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.rekotlin.Action
|
||||
import java.util.EnumSet
|
||||
|
||||
|
|
@ -73,6 +74,10 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsSta
|
|||
saveAccessCodes = preferencesStorage.shouldSaveAccessCodes,
|
||||
selectedFiatCurrency = store.state.globalState.appCurrency,
|
||||
selectedThemeMode = store.state.globalState.appThemeMode,
|
||||
isHidingEnabled = runBlocking {
|
||||
store.state.daggerGraphState
|
||||
.get { balanceHidingRepository }.getBalanceHidingSettings().isHidingEnabledInSettings
|
||||
},
|
||||
darkThemeSwitchEnabled = action.darkThemeSwitchEnabled,
|
||||
),
|
||||
)
|
||||
|
|
@ -222,6 +227,11 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
|
|||
selectedFiatCurrency = action.fiatCurrency,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy(
|
||||
appSettingsState = state.appSettingsState.copy(
|
||||
isHidingEnabled = action.hideBalance,
|
||||
),
|
||||
)
|
||||
is DetailsAction.AppSettings.EnrollBiometrics,
|
||||
is DetailsAction.AppSettings.CheckBiometricsStatus,
|
||||
-> state
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ data class AppSettingsState(
|
|||
val saveAccessCodes: Boolean = false,
|
||||
val isBiometricsAvailable: Boolean = false,
|
||||
val needEnrollBiometrics: Boolean = false,
|
||||
val isHidingEnabled: Boolean = false,
|
||||
val isInProgress: Boolean = false,
|
||||
val selectedFiatCurrency: FiatCurrency = FiatCurrency.Default,
|
||||
val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
|
||||
|
|
|
|||
|
|
@ -48,6 +48,21 @@ internal class AppSettingsItemsFactory {
|
|||
)
|
||||
}
|
||||
|
||||
fun createFlipToHideBalanceSwitch(
|
||||
isChecked: Boolean,
|
||||
isEnabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
): Item.Switch {
|
||||
return Item.Switch(
|
||||
id = "flip_to_hide_balance_switch",
|
||||
title = resourceReference(R.string.details_row_title_flip_to_hide),
|
||||
description = resourceReference(R.string.details_row_description_flip_to_hide),
|
||||
isEnabled = isEnabled,
|
||||
isChecked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
}
|
||||
|
||||
fun createSelectAppCurrencyButton(currentAppCurrencyName: String, onClick: () -> Unit): Item.Button {
|
||||
return Item.Button(
|
||||
id = "select_app_currency_button",
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide
|
|||
itemsFactory.createSelectAppCurrencyButton(currentAppCurrencyName = "US Dollar") {},
|
||||
itemsFactory.createSaveWalletsSwitch(isChecked = true, isEnabled = true, { _ -> }),
|
||||
itemsFactory.createSaveAccessCodeSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||
itemsFactory.createFlipToHideBalanceSwitch(isChecked = false, isEnabled = true) { _ -> },
|
||||
itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -99,6 +99,16 @@ internal class AppSettingsViewModel(
|
|||
).let(::add)
|
||||
}
|
||||
|
||||
itemsFactory.createFlipToHideBalanceSwitch(
|
||||
isChecked = state.isHidingEnabled,
|
||||
isEnabled = true,
|
||||
onCheckedChange = ::onFlipToHideBalanceToggled,
|
||||
).let(::add)
|
||||
|
||||
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
|
||||
showThemeModeSelector(state.selectedThemeMode)
|
||||
}.let(::add)
|
||||
|
||||
if (state.darkThemeSwitchEnabled) {
|
||||
itemsFactory.createSelectThemeModeButton(state.selectedThemeMode) {
|
||||
showThemeModeSelector(state.selectedThemeMode)
|
||||
|
|
@ -178,6 +188,10 @@ internal class AppSettingsViewModel(
|
|||
store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting))
|
||||
}
|
||||
|
||||
private fun onFlipToHideBalanceToggled(enable: Boolean) {
|
||||
store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(hideBalance = enable))
|
||||
}
|
||||
|
||||
private fun dismissDialog() {
|
||||
updateContentState { copy(dialog = null) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ sealed class AmountAction : SendScreenAction {
|
|||
data class SetAmount(val amountCrypto: BigDecimal, val isUserInput: Boolean) : AmountAction()
|
||||
data class SetAmountError(val error: TapError?) : AmountAction()
|
||||
data class SetDecimalSeparator(val separator: String) : AmountAction()
|
||||
data class HideBalance(val hide: Boolean) : AmountAction()
|
||||
}
|
||||
|
||||
// Fee
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.send.redux.reducers
|
||||
|
||||
import com.tangem.common.Strings.STARS
|
||||
import com.tangem.common.extensions.isZero
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
|
|
@ -38,8 +39,8 @@ class AmountReducer : SendInternalReducer {
|
|||
val viewValue = state.restoreDecimalSeparator(fiatToSend.stripZeroPlainString())
|
||||
state.copy(
|
||||
viewAmountValue = InputViewValue(viewValue),
|
||||
viewBalanceValue = rescaledBalance.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, currencyCanBeSwitched),
|
||||
viewBalanceValue = if (state.hideBalance) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
mainCurrency = state.createMainCurrency(currency, true),
|
||||
maxLengthOfAmount = sendState.getDecimals(currency),
|
||||
cursorAtTheSamePosition = false,
|
||||
)
|
||||
|
|
@ -80,6 +81,14 @@ class AmountReducer : SendInternalReducer {
|
|||
}
|
||||
is AmountAction.SetAmountError -> state.copy(error = action.error)
|
||||
is AmountAction.SetDecimalSeparator -> state.copy(decimalSeparator = action.separator)
|
||||
is AmountAction.HideBalance -> {
|
||||
val rescaledBalance = sendState.convertExtractCryptoToFiat(state.balanceCrypto, true)
|
||||
|
||||
state.copy(
|
||||
hideBalance = action.hide,
|
||||
viewBalanceValue = if (action.hide) STARS else rescaledBalance.stripZeroPlainString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
return updateLastState(sendState.copy(amountState = result), result)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ data class AmountState(
|
|||
val mainCurrency: MainCurrency = MainCurrency(MainCurrencyType.FIAT, FiatCurrency.Default.code),
|
||||
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val balanceCrypto: BigDecimal = BigDecimal.ZERO,
|
||||
val hideBalance: Boolean = false,
|
||||
val cursorAtTheSamePosition: Boolean = true,
|
||||
val maxLengthOfAmount: Int = 2,
|
||||
val decimalSeparator: String = ".",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.view.inputmethod.EditorInfo
|
|||
import android.widget.EditText
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
|
|
@ -45,6 +46,7 @@ import com.tangem.tap.mainScope
|
|||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentSendBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -56,8 +58,11 @@ private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
|
|||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(FlowPreview::class)
|
||||
@AndroidEntryPoint
|
||||
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
||||
|
||||
private val viewModel by viewModels<SendViewModel>()
|
||||
|
||||
lateinit var sendBtn: ViewStateWidget
|
||||
|
||||
private lateinit var etAmountToSend: TextInputEditText
|
||||
|
|
@ -70,6 +75,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycle.addObserver(viewModel)
|
||||
Analytics.send(Token.Send.ScreenOpened())
|
||||
}
|
||||
|
||||
|
|
@ -348,6 +354,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
|
|||
|
||||
override fun onDestroy() {
|
||||
store.dispatch(ReleaseSendState)
|
||||
lifecycle.removeObserver(viewModel)
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
package com.tangem.tap.features.send.ui
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import com.tangem.domain.balancehiding.IsBalanceHiddenUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.tap.features.send.redux.AmountAction
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
internal class SendViewModel @Inject constructor(
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val appStateHolder: AppStateHolder,
|
||||
private val isBalanceHiddenUseCase: IsBalanceHiddenUseCase,
|
||||
private val listenToFlipsUseCase: ListenToFlipsUseCase,
|
||||
) : ViewModel(), DefaultLifecycleObserver {
|
||||
|
||||
override fun onCreate(owner: LifecycleOwner) {
|
||||
isBalanceHiddenUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.onEach { isBalanceHidden ->
|
||||
withContext(dispatchers.main) {
|
||||
appStateHolder.mainStore?.dispatch(AmountAction.HideBalance(isBalanceHidden))
|
||||
}
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
|
||||
viewModelScope.launch {
|
||||
listenToFlipsUseCase()
|
||||
.flowWithLifecycle(owner.lifecycle)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.asset.AssetReader
|
|||
import com.tangem.datasource.connection.NetworkConnectionManager
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
|
||||
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.ScanCardUseCase
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
|
|
@ -41,6 +42,7 @@ data class DaggerGraphState(
|
|||
val walletManagersFacade: WalletManagersFacade? = null,
|
||||
val appStateHolder: AppStateHolder? = null,
|
||||
val appThemeModeRepository: AppThemeModeRepository? = null,
|
||||
val balanceHidingRepository: BalanceHidingRepository? = null,
|
||||
val detailsFeatureToggles: DetailsFeatureToggles? = null,
|
||||
|
||||
// FIXME: It is used only for TokensList screen. Remove after refactoring of TokensList
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue