Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-14 16:03:31 +03:00
commit 76bf94f718
407 changed files with 9866 additions and 3473 deletions

View file

@ -43,9 +43,6 @@ object DemoHelper {
}
private fun getScanResponse(appState: () -> AppState?): ScanResponse? {
val state = appState() ?: return null
return state.globalState.onboardingState.onboardingManager?.scanResponse
?: state.globalState.scanResponse
return appState()?.globalState?.scanResponse
}
}

View file

@ -27,7 +27,6 @@ import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.features.details.ui.cardsettings.CardInfo
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState
import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent
@ -171,6 +170,8 @@ internal class CardSettingsModel @Inject constructor(
}
}
private fun CardDTO.signedHashesCount(): Int = wallets.sumOf { it.totalSignedHashes ?: 0 }
private fun handleClickingItem(item: CardInfo) {
when (item) {
is CardInfo.ChangeAccessCode -> {

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
@ -31,12 +32,18 @@ internal class WalletConnectModel @Inject constructor(
init {
modelScope.launch {
listenToQrScanningUseCase(SourceType.WALLET_CONNECT)
listenToQrScanningUseCase.listen(SourceType.WALLET_CONNECT)
.getOrElse { emptyFlow() }
.map {
.map { result ->
val source = when (result.resultSource) {
QrResultSource.CLIPBOARD -> WalletConnectAction.OpenSession.SourceType.CLIPBOARD
QrResultSource.CAMERA,
QrResultSource.GALLERY,
-> WalletConnectAction.OpenSession.SourceType.QR
}
WalletConnectAction.OpenSession(
wcUri = it,
source = WalletConnectAction.OpenSession.SourceType.QR,
wcUri = result.qrCode,
source = source,
userWalletId = params.userWalletId,
)
}

View file

@ -22,7 +22,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton
@ -61,7 +61,7 @@ internal fun StoriesScreen(
StoriesScreenContent(
modifier = Modifier
.fillMaxSize()
.testTag(TestTags.STORIES_SCREEN),
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentConfig(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,

View file

@ -19,7 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtonsV2
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
@ -57,7 +57,7 @@ internal fun StoriesScreenV2(
StoriesScreenContentV2(
modifier = Modifier
.fillMaxSize()
.testTag(TestTags.STORIES_SCREEN),
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
config = StoriesScreenContentV2Config(
storiesSize = state.stories.lastIndex,
currentStoryIndex = currentStoryIndex,

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R
@Composable
@ -34,7 +34,7 @@ internal fun HomeButtons(
ScanCardButton(
modifier = Modifier
.weight(weight = 1f)
.testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON),
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)
@ -42,7 +42,7 @@ internal fun HomeButtons(
OrderCardButton(
modifier = Modifier
.weight(weight = 1f)
.testTag(TestTags.STORIES_SCREEN_ORDER_BUTTON),
.testTag(StoriesScreenTestTags.ORDER_BUTTON),
onClick = onShopButtonClick,
)
}

View file

@ -18,7 +18,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.wallet.R
@Composable
@ -37,19 +37,19 @@ internal fun HomeButtonsV2(
CreateNewWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_CREATE_NEW_WALLET_BUTTON),
.testTag(StoriesScreenTestTags.CREATE_NEW_WALLET_BUTTON),
onClick = onCreateNewWalletButtonClick,
)
AddExistingWalletButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_ADD_EXISTING_WALLET_BUTTON),
.testTag(StoriesScreenTestTags.ADD_EXISTING_WALLET_BUTTON),
onClick = onAddExistingWalletButtonClick,
)
ScanCardButton(
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.STORIES_SCREEN_SCAN_BUTTON),
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
showProgress = btnScanStateInProgress,
onClick = onScanButtonClick,
)

View file

@ -61,7 +61,6 @@ private fun handleHomeAction(action: Action) {
Analytics.send(IntroductionProcess.ScreenOpened())
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
}
is HomeAction.ReadCard -> {
action.scope.launch {

View file

@ -0,0 +1,53 @@
package com.tangem.tap.features.hot
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import javax.inject.Inject
import javax.inject.Singleton
/**
* Proxy for [TangemHotSdk] to allow lazy initialization and provide a way to access the SDK state from domain layer.
* SDK is initialized in the [com.tangem.tap.routing.component.RoutingComponent] and can be accessed through this proxy.
* Be aware that the SDK is initialized on activity creation, so it may not be available immediately.
*/
@Singleton
class TangemHotSDKProxy @Inject constructor() : TangemHotSdk {
val sdkState = MutableStateFlow<TangemHotSdk?>(null)
override suspend fun importWallet(mnemonic: Mnemonic, passphrase: CharArray?, auth: HotAuth): HotWalletId =
callSdk { importWallet(mnemonic, passphrase, auth) }
override suspend fun generateWallet(auth: HotAuth, mnemonicType: MnemonicType): HotWalletId =
callSdk { generateWallet(auth, mnemonicType) }
override suspend fun exportMnemonic(unlockHotWallet: UnlockHotWallet): SeedPhrasePrivateInfo =
callSdk { exportMnemonic(unlockHotWallet) }
override suspend fun exportBackup(unlockHotWallet: UnlockHotWallet): ByteArray =
callSdk { exportBackup(unlockHotWallet) }
override suspend fun delete(id: HotWalletId) = callSdk { delete(id) }
override suspend fun changeAuth(unlockHotWallet: UnlockHotWallet, auth: HotAuth): HotWalletId =
callSdk { changeAuth(unlockHotWallet, auth) }
override suspend fun derivePublicKey(
unlockHotWallet: UnlockHotWallet,
request: DeriveWalletRequest,
): DerivedPublicKeyResponse = callSdk { derivePublicKey(unlockHotWallet, request) }
override suspend fun signHashes(unlockHotWallet: UnlockHotWallet, dataToSign: List<DataToSign>): List<SignedData> =
callSdk { signHashes(unlockHotWallet, dataToSign) }
private suspend fun <T> callSdk(block: suspend TangemHotSdk.() -> T): T {
return withTimeout(timeMillis = 1000) {
sdkState.filterNotNull().first()
}.block()
}
}

View file

@ -18,11 +18,15 @@ import com.tangem.core.ui.message.BottomSheetMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage
import com.tangem.domain.appcurrency.FetchAppCurrenciesUseCase
import com.tangem.domain.balancehiding.BalanceHidingSettings
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
@ -37,6 +41,7 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase
@ -46,6 +51,9 @@ import com.tangem.features.onramp.deeplink.OnrampDeepLink
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
@ -58,7 +66,7 @@ import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
@HiltViewModel
internal class MainViewModel @Inject constructor(
private val updateBalanceHidingSettingsUseCase: UpdateBalanceHidingSettingsUseCase,
@ -88,6 +96,8 @@ internal class MainViewModel @Inject constructor(
private val sendPushTokenUseCase: SendPushTokenUseCase,
private val apiConfigsManager: ApiConfigsManager,
private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
routingFeatureToggle: RoutingFeatureToggle,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -120,6 +130,8 @@ internal class MainViewModel @Inject constructor(
multiQuoteUpdater.subscribe()
initializeOffRamp()
observeFlips()
displayBalancesHidingStatusToast()
displayHiddenBalancesModalNotification()
@ -189,7 +201,12 @@ internal class MainViewModel @Inject constructor(
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet.requireColdWallet().scanResponse) // TODO [REDACTED_TASK_KEY]
when (userWallet) {
is UserWallet.Cold -> Analytics.setContext(userWallet.scanResponse)
is UserWallet.Hot -> {
// TODO [REDACTED_TASK_KEY]
}
}
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
@ -201,6 +218,28 @@ internal class MainViewModel @Inject constructor(
.onRight { Timber.d("Staking token list was fetched successfully") }
}
private fun initializeOffRamp() {
viewModelScope.launch {
val sellService = makeSellExchangeService(environmentConfig = environmentConfigStorage.getConfigSync())
appStateHolder.sellService = sellService
sellService.update()
}
}
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
val cardProvider: () -> ScanResponse? = {
userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
}
return MoonPayService(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
cardProvider = { cardProvider.invoke()?.card },
)
}
private fun observeFlips() {
listenToFlipsUseCase().launchIn(viewModelScope)
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.onboarding
import com.tangem.domain.models.scan.ScanResponse
@Deprecated("Remove when navigation refactoring will be implemented")
class OnboardingManager(var scanResponse: ScanResponse)

View file

@ -27,7 +27,7 @@ object WalletActivationErrorDialog {
// changed on email support [REDACTED_TASK_KEY]
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
val scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
val scanResponse = store.state.globalState.scanResponse
?: error("ScanResponse must be not null")
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()

View file

@ -1,30 +1,18 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.Analytics
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.onramp.model.OnrampSource
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@Deprecated("Will be removed soon")
@ -46,79 +34,10 @@ object TradeCryptoMiddleware {
when (action) {
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
is TradeCryptoAction.Buy -> proceedBuyAction(action)
is TradeCryptoAction.Sell -> proceedSellAction(action)
}
}
private fun proceedBuyAction(action: TradeCryptoAction.Buy) {
val isOnrampEnabled = store.inject(DaggerGraphState::onrampFeatureToggles).isFeatureEnabled
if (isOnrampEnabled) {
proceedWithOnramp(action.userWallet.walletId, action.cryptoCurrencyStatus.currency, action.source)
} else {
proceedWithLegacyBuyAction(action)
}
}
private fun proceedWithOnramp(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, source: OnrampSource) {
store.dispatchNavigationAction {
push(
AppRoute.Onramp(
userWalletId = userWalletId,
currency = cryptoCurrency,
source = source,
),
)
}
}
private fun proceedWithLegacyBuyAction(action: TradeCryptoAction.Buy) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
?.let(NetworkAddress.Address::value)
?: return
val status = action.cryptoCurrencyStatus
val currency = status.currency
val blockchain = currency.network.toBlockchain()
val exchangeManager = store.state.globalState.exchangeManager
val topUrl = exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
cryptoCurrency = currency,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive,
)
scope.launch {
if (currency is CryptoCurrency.Token && currency.network.isTestnet) {
val walletManager = store.inject(DaggerGraphState::walletManagersFacade)
.getOrCreateWalletManager(
userWalletId = action.userWallet.walletId,
blockchain = blockchain,
derivationPath = currency.network.derivationPath.value,
)
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
return@launch
}
buyErc20TestnetTokens(
card = action.userWallet.requireColdWallet().scanResponse.card, // TODO [REDACTED_TASK_KEY]
walletManager = walletManager,
destinationAddress = currency.contractAddress,
)
return@launch
}
topUrl?.let {
store.dispatchOpenUrl(it)
Analytics.send(Token.Topup.ScreenOpened())
}
}
}
private fun proceedSellAction(action: TradeCryptoAction.Sell) {
val networkAddress = action.cryptoCurrencyStatus.value.networkAddress
?.defaultAddress
@ -126,8 +45,7 @@ object TradeCryptoMiddleware {
?: return
val currency = action.cryptoCurrencyStatus.currency
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl(
cryptoCurrency = currency,
fiatCurrencyName = action.appCurrencyCode,
walletAddress = networkAddress,
@ -140,9 +58,9 @@ object TradeCryptoMiddleware {
private fun openReceiptUrl(transactionId: String) {
store.dispatchNavigationAction(AppRouter::pop)
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
action = CurrencyExchangeManager.Action.Sell,
transactionId = transactionId,
)?.let { store.dispatchOpenUrl(it) }
val sellService = store.inject(DaggerGraphState::appStateHolder).sellService
sellService?.getSellCryptoReceiptUrl(transactionId = transactionId)
?.let(store::dispatchOpenUrl)
}
}

View file

@ -131,6 +131,5 @@ internal class WelcomeModel @Inject constructor(
private fun initGlobalState() {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.ExchangeManager.Init)
}
}