Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-26 12:44:18 +03:00
commit aa19739725
59 changed files with 570 additions and 365 deletions

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
# Built application files
/build
/buildSrc
# Local configuration file (sdk path, etc)
local.properties

View file

@ -13,9 +13,6 @@
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
<option name="ALLOW_TRAILING_COMMA" value="true" />
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
@ -205,4 +202,4 @@
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View file

@ -27,16 +27,16 @@
</queries>
<application
android:theme="@style/SplashTheme"
android:label="@string/tangem_app_name"
android:name="com.tangem.tap.TapApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:allowBackup="false"
android:fullBackupContent="false"
android:networkSecurityConfig="@xml/network_security_config"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/tangem_app_name"
android:largeHeap="@bool/largeHeap"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent">
@ -45,10 +45,11 @@
android:value="true" />
<activity
android:exported="true"
android:name="com.tangem.tap.MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@ -125,13 +126,17 @@
</activity>
<activity android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity" />
<activity
android:name="com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity"
android:theme="@style/AppTheme" />
<activity
android:name="zendesk.messaging.MessagingActivity"
android:theme="@style/ZendeskTheme" />
<activity android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity" />
<activity
android:name="com.tangem.tap.features.sprinklr.ui.SprinklrActivity"
android:theme="@style/AppTheme" />
<provider
android:name="androidx.core.content.FileProvider"

View file

@ -100,12 +100,19 @@ class DialogManager : StoreSubscriber<GlobalState> {
preparedData = state.dialog.data,
context = context,
)
is WalletConnectDialog.UnsupportedNetwork ->
is WalletConnectDialog.UnsupportedNetwork -> {
val warning = if (state.dialog.networks.isNullOrEmpty()) {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,
messageRes = R.string.wallet_connect_scanner_error_unsupported_network,
message = warning,
context = context,
)
}
is WalletConnectDialog.SessionProposalDialog -> {
SessionProposalDialog.create(
sessionProposal = state.dialog.sessionProposal,

View file

@ -15,6 +15,7 @@ import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
data class GlobalState(
@Deprecated("Use scan response from selected user wallet")
val scanResponse: ScanResponse? = null,
val onboardingState: OnboardingState = OnboardingState(),
val cardVerifiedOnline: Boolean = false,

View file

@ -83,9 +83,9 @@ class TapWalletManager(
}
private fun setupWalletConnectV2(userWallet: UserWallet) {
val cardId = if (userWallet.cardsInWallet.size == 1) {
val cardId = if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else {
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
scope.launch {

View file

@ -118,7 +118,9 @@ internal class BiometricUserWalletsListManager(
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
.map { updatedUserWallet }
}
.flatMap {
get(userWalletId)
}
}

View file

@ -1,13 +1,10 @@
package com.tangem.tap.domain.walletCurrencies.implementation
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.flatMap
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.domain.models.scan.CardDTO
import com.tangem.common.*
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.builders.WalletStoreBuilder
@ -103,7 +100,7 @@ internal class DefaultWalletCurrenciesManager(
.flatMap {
updateWalletStores(userWallet, remainingCurrencies.toBlockchainNetworks())
}
.map {
.doOnResult {
saveUserCurrencies(card, remainingCurrencies)
}
}

View file

@ -7,16 +7,11 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result.Failure
import com.tangem.blockchain.extensions.Result.Success
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.common.*
import com.tangem.common.core.TangemError
import com.tangem.common.flatMap
import com.tangem.common.flatMapOnFailure
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.entities.FiatCurrency
@ -25,15 +20,7 @@ import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.walletStores.WalletStoresError
import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStores
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithDemoAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithRent
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
import com.tangem.tap.domain.walletStores.repository.implementation.utils.*
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
import com.tangem.tap.features.demo.DemoHelper
@ -45,13 +32,8 @@ import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
import kotlin.time.Duration
@ -200,37 +182,40 @@ internal class DefaultWalletAmountsRepository(
walletStore: WalletStoreModel,
walletManager: WalletManager?,
): CompletionResult<Unit> {
val hasMissedDerivations = with(walletStore) {
val isDerivationMissed = with(walletStore) {
derivationPath != null && !scanResponse.hasDerivation(blockchain, derivationPath.rawPath)
}
return when {
hasMissedDerivations -> {
isDerivationMissed -> {
updateWalletStoreWithMissedDerivation(walletStore)
}
walletManager == null -> {
updateWalletStoreWithUnreachable(walletStore)
}
else -> {
updateWalletManager(scanResponse, walletManager).map {
updateWalletManagerInStorage(
userWalletId,
walletManager,
)
}.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
updateWalletManager(scanResponse, walletManager)
.map {
updateWalletManagerInStorage(userWalletId, walletManager)
}
.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}
.flatMap {
fetchWalletStoreRentIfNeeded(walletStore, walletManager)
}
.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
}
}
}

View file

@ -33,7 +33,6 @@ import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTradeOrder
import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTransferOrder
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.store
@ -194,7 +193,12 @@ class WalletConnectSdkHelper {
val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message(), cardId = cardId)
return when (result) {
is CompletionResult.Success -> {
HEX_PREFIX + result.data
HEX_PREFIX + EthereumUtils.prepareTransactionToSend(
signature = result.data.signature,
transactionToSign = dataToSign,
walletPublicKey = data.walletManager.wallet.publicKey,
blockchain = data.walletManager.wallet.blockchain,
).toHexString()
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { Analytics.send(WalletConnect.SignError(it)) }

View file

@ -1,15 +1,22 @@
package com.tangem.tap.domain.walletconnect2.data
import android.app.*
import com.tangem.tap.domain.walletconnect2.domain.*
import android.app.Application
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.walletconnect.android.*
import com.walletconnect.android.relay.*
import com.walletconnect.web3.wallet.client.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.*
import javax.inject.*
import com.walletconnect.android.Core
import com.walletconnect.android.CoreClient
import com.walletconnect.android.relay.ConnectionType
import com.walletconnect.web3.wallet.client.Wallet
import com.walletconnect.web3.wallet.client.Web3Wallet
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class WalletConnectRepositoryImpl @Inject constructor(
private val application: Application,
@ -181,7 +188,7 @@ class WalletConnectRepositoryImpl @Inject constructor(
userNamespaces = userNamespaces,
)
if (missingNetworks.isNotEmpty()) {
Timber.e("Unsupported blockchains: $missingNetworks")
Timber.e("Not added blockchains: $missingNetworks")
scope.launch {
_events.emit(
WalletConnectEvents.SessionApprovalError(

View file

@ -1,13 +1,16 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.walletconnect.*
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.ui.walletconnect.*
import com.tangem.utils.coroutines.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import timber.log.*
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
class WalletConnectInteractor(
private val handler: WalletConnectEventsHandler,
@ -34,11 +37,11 @@ class WalletConnectInteractor(
suspend fun startListening(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
walletConnectRepository.updateSessions()
coroutineScope {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
}
walletConnectRepository.updateSessions()
}
private suspend fun subscribeToEvents() {
@ -47,6 +50,14 @@ class WalletConnectInteractor(
when (wcEvent) {
is WalletConnectEvents.SessionProposal -> {
Timber.d("WC session proposal event received")
val unsupportedNetworks = wcEvent.chainIds
.filter { blockchainHelper.chainIdToNetworkIdOrNull(it) == null }
if (unsupportedNetworks.isNotEmpty()) {
val error = WalletConnectError.ApprovalErrorUnsupportedNetwork(unsupportedNetworks)
handler.onSessionRejected(error)
return@onEach
}
val networksFormatted = wcEvent.chainIds
.mapNotNull { blockchainHelper.chainIdToFullNameOrNull(it) }
.toString()
@ -57,12 +68,7 @@ class WalletConnectInteractor(
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains
.map { blockchainHelper.chainIdToNetworkIdOrNull(it) }
val containsUnsupportedNetworks = missingNetworks.any { it == null }
if (containsUnsupportedNetworks) {
WalletConnectError.ApprovalErrorUnsupportedNetwork
} else {
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
}
else -> wcEvent.error
}

View file

@ -130,7 +130,8 @@ class WcSessionRequestConverter(
): String? {
return sessionsRepository.loadSessions(userWalletId)
.firstOrNull { it.topic == sessionRequest.topic }
?.accounts?.firstOrNull { it.chainId == sessionRequest.chainId && it.walletAddress == walletAddress }
?.derivationPath
?.accounts?.firstOrNull {
it.chainId == sessionRequest.chainId && it.walletAddress.lowercase() == walletAddress?.lowercase()
}?.derivationPath
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.walletconnect2.domain.models
sealed class WalletConnectError : Exception() {
data class ApprovalErrorMissingNetworks(val missingChains: List<String>) : WalletConnectError()
data class ApprovalErrorAddNetwork(val networks: List<String>) : WalletConnectError()
object ApprovalErrorUnsupportedNetwork : WalletConnectError()
data class ApprovalErrorUnsupportedNetwork(val unsupportedNetworks: List<String>) : WalletConnectError()
data class ExternalApprovalError(override val message: String?) : WalletConnectError()
object WrongUserWallet : WalletConnectError()
object UnsupportedMethod : WalletConnectError()

View file

@ -4,9 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.transition.TransitionInflater
@ -25,8 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint
internal class AddCustomTokenFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
@ -41,7 +40,10 @@ internal class AddCustomTokenFragment : Fragment() {
}
TangemTheme {
AddCustomTokenScreen(stateHolder = viewModel.uiState)
AddCustomTokenScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}
}
}

View file

@ -8,11 +8,7 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
@ -33,11 +29,12 @@ import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCu
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
Scaffold(
modifier = modifier,
topBar = {
AddCustomTokenToolbar(
title = state.toolbar.title,

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -15,10 +16,10 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifier: Modifier = Modifier) {
when (stateHolder) {
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(stateHolder, modifier)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(stateHolder, modifier)
}
}

View file

@ -1,31 +1,11 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetScaffoldState
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.Divider
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FabPosition
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -59,7 +39,7 @@ import kotlinx.coroutines.launch
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent) {
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
@ -77,6 +57,7 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
BottomSheetScaffold(
modifier = modifier,
sheetContent = {
SheetContent(
coroutineScope = coroutineScope,

View file

@ -8,7 +8,6 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -161,7 +160,6 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
expanded = isExpanded && isEnabled,
onDismissRequest = { isExpanded = false },
) {
FocusRequester
model.items.forEachIndexed { index, item ->
DropdownMenuItem(
onClick = {

View file

@ -41,6 +41,7 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -207,7 +208,8 @@ internal class AddCustomTokenViewModel @Inject constructor(
val defaultNetwork = createNetworkSelectorItem(blockchain = Blockchain.Unknown)
return listOf(defaultNetwork) + Blockchain.values()
.filter { blockchain ->
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true
reduxStateHolder.scanResponse?.card?.supportedBlockchains()?.contains(blockchain) == true &&
blockchain != Blockchain.Cardano
}
.sortedBy(Blockchain::fullName)
.map(::createNetworkSelectorItem)
@ -270,7 +272,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
type = DerivationPathSelectorType.CUSTOM,
),
) + Blockchain.values()
.filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() }
.filter { blockchain ->
blockchain.isSupportedInApp() && !blockchain.isTestnet() && blockchain != Blockchain.Cardano
}
.sortedBy(Blockchain::fullName)
.map(::createDerivationPathSelectorAdditionalItem)
}
@ -510,7 +514,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
val sameAddress = contractAddress == wrappedCurrency.token.contractAddress
val sameBlockchain =
Blockchain.fromNetworkId(networkSelectorValue.toNetworkId()) == wrappedCurrency.blockchain
val isSameDerivationPath = getDerivationPath()?.rawPath == wrappedCurrency.derivationPath
val isSameDerivationPath = getDerivationPath().isSameDerivationPath(wrappedCurrency.derivationPath)
sameId && sameAddress && sameBlockchain && isSameDerivationPath
}
}
@ -522,10 +526,15 @@ internal class AddCustomTokenViewModel @Inject constructor(
.filterIsInstance<Currency.Blockchain>()
.any {
val networkSelectorValue = uiState.form.networkSelectorField.selectedItem.blockchain
networkSelectorValue == it.blockchain && getDerivationPath()?.rawPath == it.derivationPath
networkSelectorValue == it.blockchain &&
getDerivationPath().isSameDerivationPath(it.derivationPath)
}
}
private fun DerivationPath?.isSameDerivationPath(rawDerivationPath: String?): Boolean {
return this == rawDerivationPath?.let { DerivationPath(it) }
}
private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
when {
isNetworkSelected() && type == AddCustomTokenError.InvalidContractAddress -> {
@ -605,7 +614,11 @@ internal class AddCustomTokenViewModel @Inject constructor(
private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
fun onBackButtonClick() {
featureRouter.popBackStack()
viewModelScope.launch(dispatchers.main) {
// need delay before close, cause crashed in compose PopUpMenu as
delay(timeMillis = 100)
featureRouter.popBackStack()
}
}
fun onContactAddressValueChange(enteredValue: String) {

View file

@ -66,7 +66,7 @@ class WalletConnectMiddleware {
walletConnectManager.restoreSessions(action.scanResponse)
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank() && WalletConnectManager.isCorrectWcUri(action.wcUri)) {
if (!action.wcUri.isNullOrBlank()) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
@ -238,7 +238,7 @@ class WalletConnectMiddleware {
return
}
val blockchain = action.blockchain.guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
return
}
val walletManager = getWalletManager(
@ -298,19 +298,25 @@ class WalletConnectMiddleware {
),
)
}
WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
}
is WalletConnectError.ExternalApprovalError -> {
is WalletConnectError.ApprovalErrorUnsupportedNetwork -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkWarningDialog(
message = action.error.message ?: "",
),
WalletConnectDialog.UnsupportedNetwork(action.error.unsupportedNetworks),
),
)
}
is WalletConnectError.ExternalApprovalError -> {
val message = action.error.message
if (!message.isNullOrEmpty()) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
AppDialog.SimpleOkWarningDialog(
message = message,
),
),
)
}
}
else -> Unit
}
}
@ -327,7 +333,7 @@ class WalletConnectMiddleware {
scope.launch { walletConnectInteractor.continueWithRequest(action.sessionRequest) }
}
is WalletConnectAction.RejectUnsupportedRequest -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
}
}
}
@ -337,7 +343,7 @@ class WalletConnectMiddleware {
chainId = chainId,
peer = session.peerMeta,
).guard {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork))
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork()))
return
}

View file

@ -91,7 +91,7 @@ data class WalletForSession(
sealed class WalletConnectDialog : StateDialog {
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
object UnsupportedCard : WalletConnectDialog()
object UnsupportedNetwork : WalletConnectDialog()
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
data class AddNetwork(val network: String) : WalletConnectDialog()
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()

View file

@ -24,7 +24,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
setFitSystemWindows(fit = true)
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
@ -47,6 +47,11 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
scannerView?.stopCamera()
}
override fun onDestroy() {
super.onDestroy()
setFitSystemWindows(fit = false)
}
override fun onResume() {
super.onResume()
scannerView?.setResultHandler(this)
@ -55,7 +60,7 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
override fun handleResult(result: Result) {
store.dispatch(NavigationAction.PopBackTo())
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
setFitSystemWindows(fit = false)
if (!result.text.isNullOrBlank()) {
store.dispatch(WalletConnectAction.OpenSession(result.text))
}
@ -83,4 +88,10 @@ class QrScanFragment : Fragment(0), ZXingScannerView.ResultHandler {
private fun requestPermission() {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CameraView.PERMISSION_REQUEST_CODE)
}
private fun setFitSystemWindows(fit: Boolean) {
activity?.window?.let {
WindowCompat.setDecorFitsSystemWindows(it, fit)
}
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder.XrpTransactionExtras
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.core.TangemSdkError
@ -168,9 +168,7 @@ private fun sendTransaction(
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) }
transactionExtras.xrpDestinationTag?.tag?.let {
txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it))
}
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionExtras(it)) }
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
@ -250,17 +248,10 @@ private fun sendTransaction(
Analytics.send(
Basic.TransactionSent(
sentFrom = AnalyticsParam.TxSentFrom.Sell,
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
),
)
Analytics.send(
Token.Send.SelectedCurrency(
currency = when (mainCurrencyType) {
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
MainCurrencyType.CRYPTO -> CurrencyType.AppCurrency
},
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId))
} else {
Analytics.send(
@ -270,9 +261,10 @@ private fun sendTransaction(
token = amountToSend.currencySymbol,
feeType = feeType.convertToAnalyticsFeeType(),
),
memoType = if (txData.extras != null) MemoType.Full else MemoType.Empty,
memoType = getMemoType(transactionExtras),
),
)
Analytics.sendSelectedCurrencyEvent(mainCurrencyType)
dispatch(NavigationAction.PopBackTo())
}
scope.launch(Dispatchers.IO) {
@ -336,6 +328,25 @@ private fun sendTransaction(
}
}
private fun getMemoType(transactionExtras: TransactionExtrasState): MemoType {
return when {
transactionExtras.isEmpty() -> MemoType.Empty
transactionExtras.isNull() -> MemoType.Null
else -> MemoType.Full
}
}
private fun Analytics.sendSelectedCurrencyEvent(mainCurrencyType: MainCurrencyType) {
send(
Token.Send.SelectedCurrency(
currency = when (mainCurrencyType) {
MainCurrencyType.FIAT -> CurrencyType.AppCurrency
MainCurrencyType.CRYPTO -> CurrencyType.Token
},
),
)
}
private fun updateFeedbackManagerInfo(
walletManager: WalletManager,
amountToSend: Amount,

View file

@ -34,6 +34,21 @@ data class TransactionExtrasState(
val cosmosMemoState: CosmosMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
fun isNull(): Boolean {
return xlmMemo == null && binanceMemo == null && xrpDestinationTag == null && tonMemoState == null &&
cosmosMemoState == null
}
fun isEmpty(): Boolean {
val isXlmEmpty = xlmMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isBinanceEmpty = binanceMemo?.viewFieldValue?.value?.isEmpty() ?: false
val isXrpEmpty = xrpDestinationTag?.viewFieldValue?.value?.isEmpty() ?: false
val isTonEmpty = tonMemoState?.viewFieldValue?.value?.isEmpty() ?: false
val isCosmosEmpty = cosmosMemoState?.viewFieldValue?.value?.isEmpty() ?: false
return isXlmEmpty || isBinanceEmpty || isXrpEmpty || isTonEmpty || isCosmosEmpty
}
}
enum class XlmMemoType {
@ -105,6 +120,7 @@ data class TonMemoState(
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class CosmosMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,

View file

@ -5,14 +5,16 @@ import android.content.Context
import android.util.AttributeSet
import com.google.android.material.textfield.TextInputEditText
class EditTextCustomPaste @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : TextInputEditText(context, attrs, defStyleAttr) {
class EditTextCustomPaste : TextInputEditText {
private var onSystemPasteButtonClickListener: (() -> Unit)? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun setOnSystemPasteButtonClickListener(callback: () -> Unit) {
onSystemPasteButtonClickListener = callback
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.shop.data
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.ShopResponse
import com.tangem.tap.features.shop.domain.ShopRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber
/**
* Default implementation of shop feature repository
*
* @property tangemTechApi TangemTech API
* @property dispatchers coroutine dispatchers provider
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : ShopRepository {
override suspend fun isShopifyOrderingAvailable(): Boolean {
return runCatching(dispatchers.io) { tangemTechApi.getShopInfo(name = SHOPIFY_NAME) }
.fold(
onSuccess = ShopResponse::isOrderingAvailable,
onFailure = {
Timber.e("Server error. isShopifyOrderingAvailable returns default value (true)")
true
},
)
}
private companion object {
const val SHOPIFY_NAME = "shopify"
}
}

View file

@ -0,0 +1,28 @@
package com.tangem.tap.features.shop.di
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.tap.features.shop.data.DefaultShopRepository
import com.tangem.tap.features.shop.domain.DefaultShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
@Module
@InstallIn(ViewModelComponent::class)
internal object ShopUseCaseModule {
@Provides
@ViewModelScoped
fun provideShopifyOrderingAvailabilityUseCase(
tangemTechApi: TangemTechApi,
dispatchers: CoroutineDispatcherProvider,
): ShopifyOrderingAvailabilityUseCase {
return DefaultShopifyOrderingAvailabilityUseCase(
shopRepository = DefaultShopRepository(tangemTechApi, dispatchers),
)
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.tap.features.shop.domain
/**
* Default implementation of use case to define shopify ordering availability
*
* @property shopRepository shop feature repository
*
[REDACTED_AUTHOR]
*/
internal class DefaultShopifyOrderingAvailabilityUseCase(
private val shopRepository: ShopRepository,
) : ShopifyOrderingAvailabilityUseCase {
override suspend fun invoke() = shopRepository.isShopifyOrderingAvailable()
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.shop.domain
/**
* Shop feature repository
*
[REDACTED_AUTHOR]
*/
internal interface ShopRepository {
/** Get shopify ordering availability */
suspend fun isShopifyOrderingAvailable(): Boolean
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.features.shop.domain
/**
* Use case to define shopify ordering availability
*
[REDACTED_AUTHOR]
*/
internal interface ShopifyOrderingAvailabilityUseCase {
/** Get availability */
suspend operator fun invoke(): Boolean
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.features.shop.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.tap.features.shop.domain.ShopifyOrderingAvailabilityUseCase
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Shop screen view model
*
* @property shopifyOrderingAvailabilityUseCase use case to define shopify ordering availability
* @property dispatchers coroutine dispatchers provider
* @property appStateHolder redux state holder
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class ShopViewModel @Inject constructor(
private val shopifyOrderingAvailabilityUseCase: ShopifyOrderingAvailabilityUseCase,
private val dispatchers: CoroutineDispatcherProvider,
private val appStateHolder: AppStateHolder,
) : ViewModel() {
/** Check ordering delay block visibility */
fun checkOrderingDelayBlockVisibility() {
viewModelScope.launch(dispatchers.main) {
val visibility = runCatching(dispatchers.io) { shopifyOrderingAvailabilityUseCase() }
.fold(onSuccess = { !it }, onFailure = { false })
appStateHolder.mainStore?.dispatch(action = ShopAction.SetOrderingDelayBlockVisibility(visibility))
}
}
}

View file

@ -8,38 +8,40 @@ import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class ShopAction : Action {
sealed interface ShopAction : Action {
object LoadProducts : ShopAction() {
data class Success(val products: List<TangemProduct>) : ShopAction()
object Failure : ShopAction(), NotificationAction {
object LoadProducts : ShopAction {
data class Success(val products: List<TangemProduct>) : ShopAction
object Failure : ShopAction, NotificationAction {
override val messageResource = R.string.common_server_unavailable
}
}
data class ApplyPromoCode(val promoCode: String) : ShopAction() {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction()
object InvalidPromoCode : ShopAction()
data class ApplyPromoCode(val promoCode: String) : ShopAction {
data class Success(val promoCode: String?, val products: List<TangemProduct>) : ShopAction
object InvalidPromoCode : ShopAction
}
object BuyWithGooglePay : ShopAction() {
object UserCancelled : ShopAction()
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction()
object BuyWithGooglePay : ShopAction {
object UserCancelled : ShopAction
data class HandleGooglePayResponse(val resultCode: Int, val data: Intent?) : ShopAction
data class Failure(val exception: Throwable) : ShopAction()
object Success : ShopAction()
data class Failure(val exception: Throwable) : ShopAction
object Success : ShopAction
}
object StartWebCheckout : ShopAction()
object StartWebCheckout : ShopAction
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction() {
object Success : ShopAction()
object Failure : ShopAction()
data class CheckIfGooglePayAvailable(val googlePayService: GooglePayService) : ShopAction {
object Success : ShopAction
object Failure : ShopAction
}
data class SelectProduct(val productType: ProductType) : ShopAction()
data class SelectProduct(val productType: ProductType) : ShopAction
object FinishSuccessfulOrder : ShopAction()
object FinishSuccessfulOrder : ShopAction
object ResetState : ShopAction()
object ResetState : ShopAction
data class SetOrderingDelayBlockVisibility(val visibility: Boolean) : ShopAction
}

View file

@ -6,63 +6,38 @@ object ShopReducer {
fun reduce(action: Action, state: ShopState): ShopState = internalReduce(action, state)
}
@Suppress("ComplexMethod")
private fun internalReduce(action: Action, state: ShopState): ShopState {
if (action !is ShopAction) return state
return when (action) {
is ShopAction.ApplyPromoCode -> state.copy(
promoCode = action.promoCode,
promoCodeLoading = true,
)
ShopAction.BuyWithGooglePay -> state
ShopAction.LoadProducts -> state
is ShopAction.LoadProducts.Success -> {
state.copy(
availableProducts = action.products,
)
}
ShopAction.StartWebCheckout -> state
ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(
promoCode = null,
promoCodeLoading = false,
)
is ShopAction.ApplyPromoCode -> state.copy(promoCode = action.promoCode, promoCodeLoading = true)
is ShopAction.LoadProducts.Success -> state.copy(availableProducts = action.products)
is ShopAction.ApplyPromoCode.InvalidPromoCode -> state.copy(promoCode = null, promoCodeLoading = false)
is ShopAction.ApplyPromoCode.Success -> {
state.copy(
promoCode = action.promoCode,
availableProducts = action.products,
promoCodeLoading = false,
)
}
is ShopAction.SelectProduct -> state.copy(selectedProduct = action.productType)
)
}
is ShopAction.SelectProduct -> {
state.copy(
selectedProduct = action.productType,
)
}
is ShopAction.CheckIfGooglePayAvailable -> {
state
}
ShopAction.CheckIfGooglePayAvailable.Failure -> {
state.copy(isGooglePayAvailable = false)
}
ShopAction.CheckIfGooglePayAvailable.Success -> {
state.copy(isGooglePayAvailable = false) // TODO: change when we add support for GPay
}
is ShopAction.BuyWithGooglePay.Failure -> {
state
}
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse -> {
state
}
ShopAction.BuyWithGooglePay.Success -> {
state
}
ShopAction.BuyWithGooglePay.UserCancelled -> {
state
}
ShopAction.FinishSuccessfulOrder -> state
ShopAction.ResetState -> ShopState()
ShopAction.LoadProducts.Failure -> state
// TODO: change when we add support for GPay
is ShopAction.CheckIfGooglePayAvailable.Failure -> state.copy(isGooglePayAvailable = false)
is ShopAction.CheckIfGooglePayAvailable.Success -> state.copy(isGooglePayAvailable = false)
is ShopAction.ResetState -> ShopState()
is ShopAction.SetOrderingDelayBlockVisibility -> state.copy(isOrderingDelayBlockVisible = action.visibility)
is ShopAction.BuyWithGooglePay,
is ShopAction.LoadProducts,
is ShopAction.StartWebCheckout,
is ShopAction.CheckIfGooglePayAvailable,
is ShopAction.BuyWithGooglePay.Failure,
is ShopAction.BuyWithGooglePay.HandleGooglePayResponse,
is ShopAction.BuyWithGooglePay.Success,
is ShopAction.BuyWithGooglePay.UserCancelled,
is ShopAction.FinishSuccessfulOrder,
is ShopAction.LoadProducts.Failure,
-> state
}
}

View file

@ -10,6 +10,7 @@ data class ShopState(
val promoCode: String? = null,
val promoCodeLoading: Boolean = false,
val isGooglePayAvailable: Boolean = false, // TODO: change when we add support for GPay
val isOrderingDelayBlockVisible: Boolean = false,
) : StateType {
val total: String?
get() = availableProducts.firstOrNull { it.type == selectedProduct }?.totalSum?.finalValue

View file

@ -9,28 +9,35 @@ import android.view.View.OnFocusChangeListener
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.GlobalLayoutStateHandler
import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.extensions.getQuantityString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.shop.presentation.ShopViewModel
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentShopBinding
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
@AndroidEntryPoint
internal class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<ShopState> {
private val binding: FragmentShopBinding by viewBinding(FragmentShopBinding::bind)
private var cardTranslationY = 70f
private lateinit var keyboardObserver: KeyboardObserver
private val viewModel by viewModels<ShopViewModel>()
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
@ -43,6 +50,8 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.checkOrderingDelayBlockVisibility()
activity?.onBackPressedDispatcher?.addCallback(
this,
object : OnBackPressedCallback(true) {
@ -133,6 +142,7 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
animateProductSelection(state.selectedProduct)
handlePriceState(state)
handlePromoCodeState(state)
handleOrderingDelayBlock(isVisible = state.isOrderingDelayBlockVisible)
handleButtonsState(state)
}
@ -173,6 +183,10 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
pbPromoCode.show(state.promoCodeLoading)
}
private fun handleOrderingDelayBlock(isVisible: Boolean) {
if (isVisible) binding.tvSoldOutDesc.show() else binding.tvSoldOutDesc.hide()
}
private fun handleButtonsState(state: ShopState) = with(binding) {
btnPayGooglePay.root.show(state.isGooglePayAvailable)
btnAlternativePayment.show(state.isGooglePayAvailable)

View file

@ -4,9 +4,10 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.transition.TransitionInflater
@ -25,8 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint
internal class TokensListFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)
@ -41,7 +40,10 @@ internal class TokensListFragment : Fragment() {
}
TangemTheme {
TokensListScreen(stateHolder = viewModel.uiState)
TokensListScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}
}
}

View file

@ -56,12 +56,13 @@ import kotlinx.coroutines.flow.flowOf
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokensListScreen(stateHolder: TokensListStateHolder) {
internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modifier = Modifier) {
BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(value = 0.dp) }
Scaffold(
modifier = modifier,
topBar = { TokensListToolbar(state = stateHolder.toolbarState) },
floatingActionButton = {
if (stateHolder is TokensListStateHolder.ManageContent) {

View file

@ -9,7 +9,7 @@ import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -114,7 +114,8 @@ class MultiWalletMiddleware {
)
}
.doOnSuccess { updatedUserWallet ->
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.dispatchWithMain(WalletAction.MultiWallet.AddMissingDerivations(emptyList()))
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedUserWallet.scanResponse))
store.state.globalState.tapWalletManager.loadData(updatedUserWallet, refresh = true)
}
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
@ -97,12 +98,12 @@ class WalletMiddleware {
Timber.e("Unable to create wallet, no user wallet selected")
return@launch
}
val updatedScanResponse = selectedUserWallet.scanResponse.copy(
card = result.data,
)
store.dispatchWithMain(GlobalAction.SaveScanResponse(updatedScanResponse))
userWalletsListManager.update(selectedUserWallet.walletId) { userWallet ->
userWallet.copy(
scanResponse = userWallet.scanResponse.copy(
card = result.data,
),
)
userWallet.copy(scanResponse = updatedScanResponse)
}
}
is CompletionResult.Failure -> Unit

View file

@ -98,7 +98,7 @@ internal fun WalletDataModel.getAvailableActions(
internal fun WalletDataModel.shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list.orEmpty()
return listOfAddresses.size > 1
return listOfAddresses.size > 1 && currency.blockchain != Blockchain.BitcoinCash
}
internal fun WalletDataModel.assembleWarnings(

View file

@ -19,6 +19,7 @@ import javax.inject.Inject
*/
class AppStateHolder @Inject constructor() {
@Deprecated("Use scan response from selected user wallet")
var scanResponse: ScanResponse? = null
var walletState: WalletState? = null
var userTokensRepository: UserTokensRepository? = null

View file

@ -271,7 +271,7 @@
android:layout_marginTop="14dp"
android:background="@drawable/shape_rectangle_rounded_4"
android:padding="16dp"
android:text="@string/shop_sold_out_description_prefix"
android:text="@string/shop_sold_out_description"
android:textColor="@color/text_tertiary"
android:textSize="16sp" />
</LinearLayout>

View file

@ -0,0 +1,5 @@
<resources>
<bool name="largeHeap">false</bool>
</resources>

View file

@ -0,0 +1,5 @@
<resources>
<bool name="largeHeap">true</bool>
</resources>

View file

@ -51,4 +51,7 @@ interface TangemTechApi {
@Header("card_id") cardId: String,
@Body startReferralBody: StartReferralBody,
): ReferralResponse
@GET("shops")
suspend fun getShopInfo(@Query(value = "name") name: String): ShopResponse
}

View file

@ -0,0 +1,14 @@
package com.tangem.datasource.api.tangemTech.models
import com.squareup.moshi.Json
/**
* Shop response
*
* @property isOrderingAvailable ordering availability
*
[REDACTED_AUTHOR]
*/
data class ShopResponse(
@Json(name = "canOrder") val isOrderingAvailable: Boolean,
)

View file

@ -9,7 +9,7 @@
},
{
"name": "NEW_CARD_SCANNING_ENABLED",
"version": "4.7.0"
"version": "4.8.0"
},
{
"name": "REDESIGNED_WALLET_SCREEN_ENABLED",

View file

@ -343,7 +343,7 @@
<string name="shop_i_have_a_promo_code">У меня есть промо-код…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Другие способы оплаты</string>
<string name="shop_sold_out_description_prefix">Из-за большого количества заказов, которые мы получаем, cроки доставки могут быть увеличены.</string>
<string name="shop_sold_out_description">Из-за высокого количества заказов, которые мы получаем, доставка может быть задержана на срок до 5 недель в зависимости от вашего местоположения</string>
<string name="shop_total">Итого</string>
<string name="solana_rent_warning">Сеть Solana взимает арендную плату в размере %1$s каждые 2 дня. Аккаунты, которые не могут позволить себе арендную плату, удаляются из сети. Пополните свой счет более чем на %2$s, чтобы не платить арендную плату.</string>
<string name="story_awe_description">Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте.</string>
@ -463,6 +463,7 @@
<string name="wallet_connect_create_tx_not_enough_funds">Невозможно отправить транзакцию. Недостаточно средств.</string>
<string name="wallet_connect_error_failed_to_connect">Не удалось установить сессию WalletConnect. Пожалуйста, повторите попытку позже.</string>
<string name="wallet_connect_error_timeout">Не удалось установить сессию WalletConnect за отведённое время. Пожалуйста, повторите попытку позже.</string>
<string name="wallet_connect_error_unsupported_blockchains">Запрос на подключение через WalletConnect содержит неподдерживаемые блокчеины. Неподдерживаемые блокчеины:\n</string>
<string name="wallet_connect_error_unsupported_dapp">Cоединение с этим Dapp сервисом не может быть установлено из-за его технической реализации.</string>
<string name="wallet_connect_network_not_found_format">Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново.</string>
<string name="wallet_connect_paste_from_clipboard">Вставить из буфера обмена</string>

View file

@ -436,6 +436,7 @@
<string name="wallet_connect_create_tx_not_enough_funds">無法交易,無足夠資金</string>
<string name="wallet_connect_error_failed_to_connect">未能建立 WalletConnect 連接。請稍後再試</string>
<string name="wallet_connect_error_timeout">無法建立 WalletConnect 連接:超時錯誤。請稍後再試</string>
<string name="wallet_connect_error_unsupported_blockchains">會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:</string>
<string name="wallet_connect_error_unsupported_dapp">由於技術問題,無法與此 Dapp 建立連接</string>
<string name="wallet_connect_network_not_found_format">沒有 %s 網路,請先加入後再試一次</string>
<string name="wallet_connect_paste_from_clipboard">從剪貼板貼上</string>

View file

@ -339,7 +339,7 @@
<string name="shop_i_have_a_promo_code">I have a promo code…</string>
<string name="shop_one_wallet">Tangem Wallet</string>
<string name="shop_other_payment_methods">Other payment methods</string>
<string name="shop_sold_out_description_prefix">Due to the high volume of orders we are receiving, Shipping and Local Delivery orders may be delayed.</string>
<string name="shop_sold_out_description">Due to the high volume of orders we are receiving shipping may be delayed up to 5 weeks depending on your location</string>
<string name="shop_total">Total</string>
<string name="solana_rent_warning">Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
@ -455,6 +455,7 @@
<string name="wallet_connect_create_tx_not_enough_funds">Can\'t send transaction. Not enough funds.</string>
<string name="wallet_connect_error_failed_to_connect">Failed to establish WalletConnect session. Please, try again later.</string>
<string name="wallet_connect_error_timeout">Failed to establish WalletConnect session: timeout error. Please, try again later.</string>
<string name="wallet_connect_error_unsupported_blockchains">Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n</string>
<string name="wallet_connect_error_unsupported_dapp">Connection with this Dapp cannot be established due to its technical implementation.</string>
<string name="wallet_connect_network_not_found_format">%s network not found. Please, add it first and try again.</string>
<string name="wallet_connect_paste_from_clipboard">Paste from clipboard</string>

View file

@ -173,5 +173,6 @@ fun Blockchain.isSupportedInApp(): Boolean {
}
private val excludedBlockchains = listOf(
Blockchain.Ducatus,
Blockchain.Unknown,
)

View file

@ -4,11 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.transition.TransitionInflater
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.presentation.R
import com.tangem.feature.referral.router.ReferralRouter
import com.tangem.feature.referral.ui.ReferralScreen
@ -28,13 +30,17 @@ class ReferralFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
viewModel.setRouter(ReferralRouter(fragmentManager = WeakReference(parentFragmentManager)))
viewModel.onScreenOpened()
return ComposeView(inflater.context).apply {
isTransitionGroup = true
setContent {
ReferralScreen(stateHolder = viewModel.uiState)
TangemTheme {
ReferralScreen(
modifier = Modifier.systemBarsPadding(),
stateHolder = viewModel.uiState,
)
}
}
}
}

View file

@ -5,41 +5,11 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalOverscrollConfiguration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.BottomSheetScaffold
import androidx.compose.material.BottomSheetState
import androidx.compose.material.BottomSheetValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarDuration
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.SnackbarResult
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -63,9 +33,7 @@ import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.referral.models.DemoModeException
import com.tangem.feature.referral.models.ReferralStateHolder
import com.tangem.feature.referral.models.ReferralStateHolder.ErrorSnackbar
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoContentState
import com.tangem.feature.referral.models.ReferralStateHolder.ReferralInfoState
import com.tangem.feature.referral.models.ReferralStateHolder.*
import com.tangem.feature.referral.presentation.R
import com.valentinilk.shimmer.shimmer
import kotlinx.coroutines.launch
@ -77,47 +45,46 @@ import kotlinx.coroutines.launch
*/
@OptIn(ExperimentalMaterialApi::class)
@Composable
internal fun ReferralScreen(stateHolder: ReferralStateHolder) {
internal fun ReferralScreen(stateHolder: ReferralStateHolder, modifier: Modifier = Modifier) {
val coroutineScope = rememberCoroutineScope()
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = BottomSheetState(BottomSheetValue.Collapsed),
)
TangemTheme {
BottomSheetScaffold(
sheetContent = {
AgreementBottomSheetContent(
url = when (val state = stateHolder.referralInfoState) {
is ReferralInfoState.NonParticipantContent -> state.url
is ReferralInfoState.ParticipantContent -> state.url
is ReferralInfoState.Loading -> ""
},
)
},
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
sheetElevation = TangemTheme.dimens.elevation24,
sheetPeekHeight = TangemTheme.dimens.size0,
content = {
ReferralContent(
stateHolder = stateHolder,
onAgreementClick = {
stateHolder.analytics.onAgreementClicked.invoke()
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
BottomSheetScaffold(
modifier = modifier,
sheetContent = {
AgreementBottomSheetContent(
url = when (val state = stateHolder.referralInfoState) {
is ReferralInfoState.NonParticipantContent -> state.url
is ReferralInfoState.ParticipantContent -> state.url
is ReferralInfoState.Loading -> ""
},
)
},
scaffoldState = bottomSheetScaffoldState,
sheetShape = RoundedCornerShape(
topStart = TangemTheme.dimens.radius16,
topEnd = TangemTheme.dimens.radius16,
),
sheetElevation = TangemTheme.dimens.elevation24,
sheetPeekHeight = TangemTheme.dimens.size0,
content = {
ReferralContent(
stateHolder = stateHolder,
onAgreementClick = {
stateHolder.analytics.onAgreementClicked.invoke()
coroutineScope.launch {
if (bottomSheetScaffoldState.bottomSheetState.isCollapsed) {
bottomSheetScaffoldState.bottomSheetState.expand()
} else {
bottomSheetScaffoldState.bottomSheetState.collapse()
}
},
)
},
)
}
}
},
)
},
)
}
@OptIn(ExperimentalFoundationApi::class)
@ -487,7 +454,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
TangemTheme(isDark = false) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.ParticipantContent(
award = "10 USDT",
networkName = "Tron",
@ -499,7 +466,7 @@ private fun Preview_ReferralScreen_Participant_InLightTheme() {
url = "",
),
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
@ -515,7 +482,7 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() {
TangemTheme(isDark = true) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.ParticipantContent(
award = "10 USDT",
networkName = "Tron",
@ -527,7 +494,7 @@ private fun Preview_ReferralScreen_Participant_InDarkTheme() {
url = "",
),
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
@ -543,7 +510,7 @@ private fun Preview_ReferralScreen_NonParticipant_InLightTheme() {
TangemTheme(isDark = false) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
@ -552,7 +519,7 @@ private fun Preview_ReferralScreen_NonParticipant_InLightTheme() {
onParticipateClicked = {},
),
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
@ -568,7 +535,7 @@ private fun Preview_ReferralScreen_NonParticipant_InDarkTheme() {
TangemTheme(isDark = true) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.NonParticipantContent(
award = "10 USDT",
networkName = "Tron",
@ -577,7 +544,7 @@ private fun Preview_ReferralScreen_NonParticipant_InDarkTheme() {
onParticipateClicked = {},
),
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
@ -593,10 +560,10 @@ private fun Preview_ReferralScreen_Loading_InLightTheme() {
TangemTheme(isDark = false) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.Loading,
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},
@ -612,10 +579,10 @@ private fun Preview_ReferralScreen_Loading_InDarkTheme() {
TangemTheme(isDark = true) {
ReferralScreen(
stateHolder = ReferralStateHolder(
headerState = ReferralStateHolder.HeaderState(onBackClicked = {}),
headerState = HeaderState(onBackClicked = {}),
referralInfoState = ReferralInfoState.Loading,
errorSnackbar = null,
analytics = ReferralStateHolder.Analytics(
analytics = Analytics(
onAgreementClicked = {},
onCopyClicked = {},
onShareClicked = {},

View file

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
@ -34,8 +33,6 @@ internal class WalletFragment : Fragment() {
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.slide_right)
exitTransition = inflateTransition(R.transition.fade)

View file

@ -34,10 +34,11 @@ import com.tangem.feature.wallet.presentation.common.component.DraggableNetworkG
import com.tangem.feature.wallet.presentation.common.component.DraggableTokenItem
@Composable
internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder) {
internal fun OrganizeTokensScreen(state: OrganizeTokensStateHolder, modifier: Modifier = Modifier) {
val tokensListState = rememberLazyListState()
Scaffold(
modifier = modifier,
topBar = {
TopBar(state.header, tokensListState)
},

View file

@ -1,13 +1,16 @@
package com.tangem.feature.wallet.presentation.router
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.wallet.presentation.WalletFragment
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen
import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensViewModel
@ -24,22 +27,30 @@ internal class DefaultWalletRouter : InnerWalletRouter {
@Composable
override fun Initialize() {
NavHost(
navController = rememberNavController().apply { navController = this },
startDestination = WalletScreens.WALLET.name,
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
WalletScreen(state = viewModel.uiState)
}
TangemTheme {
NavHost(
navController = rememberNavController().apply { navController = this },
startDestination = WalletScreens.WALLET.name,
) {
composable(WalletScreens.WALLET.name) {
val viewModel = hiltViewModel<WalletViewModel>().apply { router = this@DefaultWalletRouter }
WalletScreen(
modifier = Modifier.systemBarsPadding(),
state = viewModel.uiState,
)
}
composable(WalletScreens.ORGANIZE_TOKENS.name) {
BackHandler(onBack = ::popBackStack)
composable(WalletScreens.ORGANIZE_TOKENS.name) {
BackHandler(onBack = ::popBackStack)
val viewModel: OrganizeTokensViewModel = hiltViewModel<OrganizeTokensViewModel>()
.apply { router = this@DefaultWalletRouter }
val viewModel: OrganizeTokensViewModel = hiltViewModel<OrganizeTokensViewModel>()
.apply { router = this@DefaultWalletRouter }
OrganizeTokensScreen(state = viewModel.uiState)
OrganizeTokensScreen(
modifier = Modifier.systemBarsPadding(),
state = viewModel.uiState,
)
}
}
}
}

View file

@ -36,7 +36,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletTopBar
[REDACTED_AUTHOR]
*/
@Composable
internal fun WalletScreen(state: WalletStateHolder) {
internal fun WalletScreen(state: WalletStateHolder, modifier: Modifier = Modifier) {
BackHandler(onBack = state.onBackClick)
Scaffold(
@ -48,7 +48,7 @@ internal fun WalletScreen(state: WalletStateHolder) {
}
LazyColumn(
modifier = Modifier
modifier = modifier
.padding(scaffoldPaddings)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,

View file

@ -77,7 +77,7 @@ walletConnectWeb3 = "1.8.0"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "develop-246"
tangemBlockchainSdk = "develop-247"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "develop-266"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds