Updated on 2026-08-14

This commit is contained in:
Tangem 2023-11-02 17:40:18 +03:00
commit 0e61080c25
296 changed files with 4083 additions and 2875 deletions

View file

@ -9,17 +9,28 @@ plugins {
id("configuration")
}
//conflict of dependencies when adding WalletConnectV2.0 library
configurations {
all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
resolutionStrategy {
force("org.bouncycastle:bcpkix-jdk15on:1.70")
configurations.all {
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
exclude(group = "com.github.komputing.kethereum")
resolutionStrategy {
dependencySubstitution {
substitute(module("com.facebook.react:react-native"))
.using(module("com.facebook.react:react-android:0.72.4"))
substitute(module("com.facebook.react:hermes-engine"))
.using(module("com.facebook.react:hermes-android:0.72.4"))
}
exclude(group = "com.github.komputing.kethereum")
force(
"org.bouncycastle:bcpkix-jdk15on:1.70",
"com.facebook.react:react-android:0.72.4",
"com.facebook.react:hermes-android:0.72.4",
)
}
}
dependencies {
implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(projects.domain.legacy)
@ -148,8 +159,6 @@ dependencies {
implementation(deps.appsflyer)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.zendesk.chat)
implementation(deps.zendesk.messaging)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.shopify.buy) {
@ -167,6 +176,10 @@ dependencies {
implementation(deps.walletConnectCore)
implementation(deps.walletConnectWeb3)
implementation(deps.prettyLogger)
implementation("com.facebook.react:react-android:0.72.4")
implementation(deps.sprClient) {
exclude(group = "com.github.stephenc.jcip")
}
/** Testing libraries */
testImplementation(deps.test.junit)

View file

@ -12,7 +12,16 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
@ -40,7 +49,7 @@
android:extractNativeLibs="true"
android:supportsRtl="true"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:allowBackup, android:fullBackupContent">
tools:replace="android:allowBackup, android:fullBackupContent, android:label">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
@ -132,14 +141,6 @@
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"
android:theme="@style/AppTheme" />
<activity
android:name="com.tangem.feature.learn2earn.presentation.webView.Learn2earnWebViewActivity"
android:theme="@style/AppTheme" />

@ -1 +1 @@
Subproject commit b84b1e1ab11b0eb0f6ca6e28f551f9e2ca6809fe
Subproject commit 2b49c08b924c03322f35542a7482e4e9fb1928d3

View file

@ -517,6 +517,17 @@
"networkId": "aleph-zero/test"
}
]
},
{
"id": "near",
"symbol": "NEAR",
"name": "NEAR",
"networks":
[
{
"networkId": "near-protocol/test"
}
]
}
]
}

View file

@ -46,6 +46,7 @@ import com.tangem.tap.common.shop.googlepay.GooglePayService
import com.tangem.tap.common.shop.googlepay.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
@ -276,6 +277,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
setDefaultNightMode(mode)
delegate.localNightMode = mode
}
private fun isDarkTheme(): Boolean {
@ -366,7 +368,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) {
val canSaveWallets = userWalletsListManager is BiometricUserWalletsListManager
val hasSavedWallets = userWalletsListManager.hasUserWallets
if (canSaveWallets && hasSavedWallets) {
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.Welcome,

View file

@ -350,7 +350,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initWithConfigDependency(config: Config) {
shopService = TangemShopService(this, config.shopify!!)
initAnalytics(this, config)
initFeedbackManager(this, preferencesStorage, foregroundActivityObserver, store)
initFeedbackManager(this, foregroundActivityObserver, store)
}
private fun initAnalytics(application: Application, config: Config) {
@ -372,7 +372,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initFeedbackManager(
context: Context,
preferencesStorage: PreferencesDataSource,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {
@ -411,7 +410,7 @@ internal class TapApplication : Application(), ImageLoaderFactory {
val feedbackManager = FeedbackManager(
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(preferencesStorage, foregroundActivityObserver, store),
chatManager = ChatManager(foregroundActivityObserver),
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
@ -53,7 +54,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkErrorDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkWarningDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is StateDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context)
is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context)
@ -105,7 +106,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
context.getString(R.string.wallet_connect_scanner_error_unsupported_network)
} else {
context.getString(R.string.wallet_connect_error_unsupported_blockchains) +
state.dialog.networks
state.dialog.networks.joinToString()
}
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect_title,

View file

@ -21,8 +21,9 @@ sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
object CustomToken : CardBalanceState("Custom Token")
object BlockchainError : CardBalanceState("Blockchain Error")
object NoRate : CardBalanceState("No Rate")
companion object
}

View file

@ -80,7 +80,7 @@ sealed class Settings(
object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
class MainCurrencyChanged(currencyType: String) : MainScreen(
class MainCurrencyChanged(currencyType: String) : AppSettings(
event = "Main Currency Changed",
params = mapOf("Currency Type" to currencyType),
)

View file

@ -46,6 +46,7 @@ class CardContextInterceptor(
ProductType.Twins -> "Twin"
ProductType.Wallet -> "Wallet"
ProductType.Wallet2 -> "Wallet 2.0"
ProductType.Ring -> "Ring"
ProductType.Start2Coin -> "Start2Coin"
else -> if (DemoHelper.isDemoCard(scanResponse)) {
if (DemoHelper.isTestDemoCard(scanResponse)) {

View file

@ -141,6 +141,8 @@ class TopUpController(
DataSourceTopupInfo.CardBalanceState.Empty
AnalyticsParam.CardBalanceState.Full ->
DataSourceTopupInfo.CardBalanceState.Full
AnalyticsParam.CardBalanceState.NoRate ->
DataSourceTopupInfo.CardBalanceState.NoRate
},
)
topupWalletStorage.save(topupInfo)

View file

@ -1,57 +1,23 @@
package com.tangem.tap.common.chat
import android.content.Context
import android.os.Build
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Store
import java.io.File
class ChatManager(
private val preferencesStorage: PreferencesDataSource,
private val foregroundActivityObserver: ForegroundActivityObserver,
private val store: Store<AppState>,
) {
class ChatManager(private val foregroundActivityObserver: ForegroundActivityObserver) {
private val openers = mutableMapOf<ChatConfig, ChatOpener>()
fun open(config: ChatConfig, createLogsFile: (Context) -> File?, createFeedbackFile: (Context) -> File?) {
val opener = openers.getOrPut(config) {
when (config) {
is SprinklrConfig -> SprinklrChatOpener(getSprinklrUserId(), config, store, foregroundActivityObserver)
is ZendeskConfig -> ZendeskChatOpener(getZendeskUserId(), config, foregroundActivityObserver)
is SprinklrConfig -> SprinklrChatOpener(config, foregroundActivityObserver)
}
}
opener.open(
createFeedbackFile = createFeedbackFile,
createLogsFile = createLogsFile,
)
}
private fun getZendeskUserId(): String {
if (preferencesStorage.zendeskFirstLaunchTime == null) {
preferencesStorage.zendeskFirstLaunchTime = System.currentTimeMillis()
}
return getChatUserId(preferencesStorage.zendeskFirstLaunchTime!!)
}
private fun getSprinklrUserId(): String {
if (preferencesStorage.sprinklrFirstLaunchTime == null) {
preferencesStorage.sprinklrFirstLaunchTime = System.currentTimeMillis()
}
return getChatUserId(preferencesStorage.sprinklrFirstLaunchTime!!)
}
private fun getChatUserId(firstLaunchTimeMillis: Long): String {
return "${firstLaunchTimeMillis}${Build.MODEL}".hashCode().toString()
opener.open(createFeedbackFile, createLogsFile)
}
}

View file

@ -1,28 +1,57 @@
package com.tangem.tap.common.chat.opener.implementation
import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.Intent
import android.provider.Settings
import com.spr.messengerclient.config.SPRMessenger
import com.spr.messengerclient.config.bean.SPRMessengerConfig
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.sprinklr.redux.SprinklrAction
import com.tangem.tap.features.sprinklr.ui.SprinklrActivity
import com.tangem.tap.withForegroundActivity
import org.rekotlin.Store
import timber.log.Timber
import java.io.File
import java.util.Locale
internal class SprinklrChatOpener(
private val userId: String,
private val config: SprinklrConfig,
private val store: Store<AppState>,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
store.dispatch(SprinklrAction.Init(userId, config))
foregroundActivityObserver.withForegroundActivity { activity ->
val intent = Intent(activity, SprinklrActivity::class.java)
activity.startActivity(intent)
val messenger = SPRMessenger.shared()
if (messenger.config == null) {
initSprConfig(messenger)
}
messenger.startApplication()
}
private fun initSprConfig(messenger: SPRMessenger) {
val application = foregroundActivityObserver.foregroundActivity?.application.guard {
Timber.e("The SPR chat cannot be opened because there are no activities in foreground")
return
}
messenger.takeOff(application, createSprConfig(application, config))
}
@SuppressLint("HardwareIds")
private fun createSprConfig(application: Application, config: SprinklrConfig): SPRMessengerConfig {
return SPRMessengerConfig().apply {
appId = config.appId
appKey = CHAT_APP_KEY
deviceId = Settings.Secure.getString(application.contentResolver, Settings.Secure.ANDROID_ID)
environment = config.environment
skin = CHAT_SKIN
locale = Locale.getDefault().language
}
}
private companion object {
const val CHAT_APP_KEY = "com.sprinklr.messenger.release"
const val CHAT_SKIN = "MODERN"
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.tap.common.chat.opener.implementation
import android.content.Context
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.domain.common.LogConfig
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
import timber.log.Timber
import zendesk.chat.*
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
internal class ZendeskChatOpener(
private val userId: String,
private val config: ZendeskConfig,
private val foregroundActivityObserver: ForegroundActivityObserver,
) : ChatOpener {
private var isInitialized = false
private var isFilesSent = false
override fun open(createFeedbackFile: (Context) -> File?, createLogsFile: (Context) -> File?) {
foregroundActivityObserver.withForegroundActivity { activity ->
initZendeskIfNeeded(activity.applicationContext)
setChatVisitorInfo()
showMessagingActivity(activity)
sendFeedbackFile(createFeedbackFile(activity))
sendLogsFile(createLogsFile(activity))
}
}
private fun initZendeskIfNeeded(context: Context) {
if (isInitialized) return
isInitialized = true
Chat.INSTANCE.init(context, config.accountKey, config.appId)
Logger.setLoggable(LogConfig.zendesk)
}
private fun setChatVisitorInfo() {
val visitorInfo = VisitorInfo.builder().withName("User $userId").build()
Chat.INSTANCE.chatProvidersConfiguration =
ChatProvidersConfiguration.builder().withVisitorInfo(visitorInfo).build()
}
private fun sendFeedbackFile(feedbackFile: File?) {
if (isInitialized && feedbackFile != null && !isFilesSent) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(feedbackFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
isFilesSent = true
}
}
private fun sendLogsFile(logsFile: File?) {
if (isInitialized && logsFile != null && !isFilesSent) {
Chat.INSTANCE.providers()?.chatProvider()?.sendFile(logsFile) { _, bytesUploaded, _ ->
Timber.d("Log file sent", "bytesUploaded: $bytesUploaded")
}
isFilesSent = true
}
}
private fun showMessagingActivity(context: Context) {
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)
.withBotAvatarDrawable(R.mipmap.ic_launcher)
.withEngines(ChatEngine.engine())
.show(context, buildChatConfig())
}
private fun buildChatConfig(): Configuration {
return ChatConfiguration.builder()
.withOfflineFormEnabled(true)
.withAgentAvailabilityEnabled(true)
.withPreChatFormEnabled(false)
.build()
}
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.common.extensions
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.StateDialog
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.scope

View file

@ -26,10 +26,8 @@ import timber.log.Timber
"or WalletCurrenciesManager.update(...) (to update only one user wallet blockchain and its tokens) instead",
)
@Suppress("MagicNumber")
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
val scanResponse = store.state.globalState.scanResponse
if (scanResponse?.isDemoCard() == true || TestActions.testAmountInjectionForWalletManagerEnabled) {
suspend fun WalletManager.safeUpdate(isDemoCard: Boolean): Result<Wallet> = try {
if (isDemoCard || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)

View file

@ -7,17 +7,24 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import kotlinx.coroutines.flow.*
import java.util.concurrent.CopyOnWriteArrayList
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var derivationPath: String = "",
var outputsCount: String? = null,
var host: String = "",
var addresses: String = "",
var explorerLink: String = "",
val blockchain: Blockchain = Blockchain.Unknown,
val derivationPath: String = "",
val outputsCount: String? = null,
val host: String = "",
val addresses: String = "",
val explorerLink: String = "",
val tokens: List<EmailTokenInfo> = emptyList(),
)
class EmailTokenInfo(
val id: String?,
val name: String,
val contractAddress: String,
)
var appVersion: String = ""
@ -30,20 +37,27 @@ class AdditionalFeedbackInfo {
var userWalletId: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
val walletsInfo = CopyOnWriteArrayList<EmailWalletInfo>()
var onSendErrorWalletInfo: EmailWalletInfo? = null
private set
var signedHashesCount: String = ""
private set
// device
var phoneModel: String = Build.MODEL
private set
var osVersion: String = Build.VERSION.SDK_INT.toString()
private set
// send error
var destinationAddress: String = ""
private set
var amount: String = ""
private set
var fee: String = ""
private set
var token: String = ""
private set
private val Address.name: String
get() = type.javaClass.simpleName
@ -60,26 +74,26 @@ class AdditionalFeedbackInfo {
@Deprecated("Don't use it directly")
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(createEmailWalletInfo(manager))
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
synchronized(walletsInfo) {
walletsInfo.clear()
walletsInfo.addAll(elements = walletManagers.map(::createEmailWalletInfo))
}
}
fun updateOnSendError(
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
feeAmount: Amount?,
destinationAddress: String,
) {
onSendErrorWalletInfo = createEmailWalletInfo(walletManager)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
fee = if (feeAmount != null) {
feeAmount.value?.stripZeroPlainString() ?: "0"
} else {
"Unable to receive"
}
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
@ -91,6 +105,9 @@ class AdditionalFeedbackInfo {
host = walletManager.currentHost,
addresses = formatAddresses(walletManager.wallet),
explorerLink = formatExploreUrls(walletManager.wallet),
tokens = walletManager.cardTokens.map { token ->
EmailTokenInfo(token.id, token.name, token.contractAddress)
},
)
}

View file

@ -33,10 +33,10 @@ class FeedbackDataBuilder(
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Outputs count", it.outputsCount)
infoHolder.tokens[it.blockchain]?.let { tokens ->
if (it.tokens.isNotEmpty()) {
builder.append("Tokens:")
breakLine()
tokens.forEach { token ->
it.tokens.forEach { token ->
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("Contract address", token.contractAddress)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.TestAction
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.wallet.models.Currency
@ -8,8 +9,6 @@ import com.tangem.tap.features.wallet.models.Currency
/**
[REDACTED_AUTHOR]
*/
interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkErrorDialog(val message: String, val onOk: VoidCallback? = null) : AppDialog()
@ -33,8 +32,6 @@ sealed class AppDialog : StateDialog {
val action: VoidCallback? = null,
)
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,
val addressData: WalletDataModel.AddressData,

View file

@ -14,7 +14,6 @@ import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.signin.redux.SignInReducer
import com.tangem.tap.features.sprinklr.redux.SprinklrReducer
import com.tangem.tap.features.tokens.legacy.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
@ -45,7 +44,6 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
welcomeState = WelcomeReducer.reduce(action, state),
saveWalletState = SaveWalletReducer.reduce(action, state),
walletSelectorState = WalletSelectorReducer.reduce(action, state),
sprinklrState = SprinklrReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
)

View file

@ -33,8 +33,6 @@ import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.signin.redux.SignInMiddleware
import com.tangem.tap.features.signin.redux.SignInState
import com.tangem.tap.features.sprinklr.redux.SprinklrMiddleware
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
import com.tangem.tap.features.tokens.legacy.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
@ -67,7 +65,6 @@ data class AppState(
val welcomeState: WelcomeState = WelcomeState(),
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(),
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
@ -109,7 +106,6 @@ data class AppState(
WalletSelectorMiddleware().middleware,
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware,
SprinklrMiddleware().middleware,
SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
LegacyMiddleware.legacyMiddleware,

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.apptheme.model.AppThemeMode
@ -11,7 +12,10 @@ import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager

View file

@ -4,17 +4,18 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -25,16 +26,11 @@ import com.tangem.tap.network.exchangeServices.ExchangeService
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoEnvironment
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletCurrenciesManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import java.util.Locale
@ -45,14 +41,14 @@ object GlobalMiddleware {
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
handleAction(action, appState, dispatch)
handleAction(action, appState)
nextDispatch(action)
}
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleAction(action: Action, appState: () -> AppState?, dispatch: DispatchFunction) {
private fun handleAction(action: Action, appState: () -> AppState?) {
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
@ -61,7 +57,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
store.dispatchDialogShow(StateDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
@ -110,10 +106,10 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
// if config not set -> try to get it based on a scanResponse.productType
val unsafeChatConfig = action.chatConfig ?: config.zendesk
val unsafeChatConfig = action.chatConfig ?: config.sprinklr
val chatConfig = unsafeChatConfig.guard {
store.dispatchDebugErrorNotification("ZendeskConfig not initialized")
store.dispatchDebugErrorNotification("The chat config is not initialized")
return
}
feedbackManager.openChat(chatConfig, action.feedbackData)
@ -131,8 +127,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
store.state.globalState.scanResponse
?: store.state.globalState.onboardingState.onboardingManager?.scanResponse
userWalletsListManager.selectedUserWalletSync?.scanResponse
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
@ -177,22 +172,25 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
walletCurrenciesManager.addListener(action.topUpController)
}
is GlobalAction.UpdateUserWalletsListManager -> {
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
/*
* If UserWalletsListManager's implementation is changed,
* then all selectedUserWallet's observers is became irrelevant
* If implementation of the UserWalletsListManager is changed,
* then all observers of selectedUserWallet become irrelevant.
*/
action.manager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.send(event = Basic.WalletOpened())
Analytics.send(Basic.WalletOpened())
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
infoHolder.setCardInfo(data = userWallet.scanResponse)
infoHolder.setCardInfo(userWallet.scanResponse)
store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
.getAll(userWalletId = userWallet.walletId)
walletManagersFacade
.getAll(userWallet.walletId)
.distinctUntilChanged()
.onEach(infoHolder::setWalletsInfo)
.launchIn(scope)
.launchIn(mainScope)
}
}
.flowOn(Dispatchers.IO)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux.global
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
@ -7,7 +8,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.onboarding.OnboardingManager

View file

@ -8,10 +8,7 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.activityResultCaller
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.store
import org.rekotlin.Middleware
@ -60,6 +57,7 @@ val navigationMiddleware: Middleware<AppState> = { _, state ->
intent.data = action.url
navState?.activity?.get()?.startActivity(intent)
}
is NavigationAction.OpenDialog -> store.dispatchDialogShow(action.stateDialog)
is NavigationAction.OpenBiometricsSettings -> {
val settingsAction = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {

View file

@ -63,9 +63,9 @@ internal object TokensDomainModule {
@ViewModelScoped
fun provideRemoveCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
dispatchers: CoroutineDispatcherProvider,
walletManagersFacade: WalletManagersFacade,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(currenciesRepository, dispatchers)
return RemoveCurrencyUseCase(currenciesRepository, walletManagersFacade)
}
@Provides
@ -167,9 +167,19 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(rampStateManager, marketCryptoCurrencyRepository, dispatchers)
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
dispatchers = dispatchers,
)
}
@Provides

View file

@ -1,6 +1,9 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
@ -21,7 +24,15 @@ internal object WalletManagersFacadeModule {
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
): WalletManagersFacade {
return DefaultWalletManagersFacade(walletManagersStore, userWalletsStore, configManager)
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
assetReader = assetReader,
moshi = moshi,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.repository.WalletsRepository
@ -58,8 +59,11 @@ internal object WalletsDomainModule {
@Provides
@ViewModelScoped
fun providesSelectWalletUseCase(walletsStateHolder: WalletsStateHolder): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesSelectWalletUseCase(
walletsStateHolder: WalletsStateHolder,
reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder, reduxStateHolder)
}
@Provides

View file

@ -1,74 +0,0 @@
package com.tangem.tap.domain
import com.tangem.common.services.Result
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.ThrottlerWithValues
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class RatesRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val throttler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
suspend fun loadFiatRate(currencyId: String, coinsList: List<Currency>): Result<RatesResult> =
withContext(dispatchers.io) {
// get and submit previous result of equivalents.
val throttledResult = coinsList.filter { throttler.isStillThrottled(it) }.map {
Pair(it, throttler.geValue(it))
}
val currenciesToUpdate = coinsList.filter { !throttler.isStillThrottled(it) }
val coinIds = currenciesToUpdate.mapNotNull { it.coinId }.distinct()
if (coinIds.isEmpty()) return@withContext handleFiatRatesResult(throttledResult.toMap())
runCatching { tangemTechApi.getRates(currencyId.lowercase(), coinIds.joinToString(",")) }
.onSuccess { response ->
val ratesResultList: Map<String, Result<BigDecimal>> = response.rates.mapValues {
Result.Success(it.value.toBigDecimal())
}
val updatedCurrencies = throttledResult.toMap().toMutableMap()
coinsList.forEach { currency ->
ratesResultList[currency.coinId]?.let {
updatedCurrencies[currency] = it
throttler.updateThrottlingTo(currency)
throttler.setValue(currency, it)
}
}
return@withContext handleFiatRatesResult(updatedCurrencies)
}
.onFailure { Result.Failure(it) }
error("Unreachable code because runCatching must return result")
}
private fun handleFiatRatesResult(rates: Map<Currency, Result<BigDecimal>?>): Result.Success<RatesResult> {
val success = mutableMapOf<Currency, BigDecimal>()
val failures = mutableMapOf<Currency, Throwable>()
rates.mapNotNull { (currency, priceResult) ->
when (priceResult) {
is Result.Success -> success[currency] = priceResult.data
is Result.Failure -> failures[currency] = priceResult.error
else -> null
}
}
return Result.Success(success to failures)
}
fun clear() {
throttler.clear()
}
}
typealias RatesResult = Pair<MutableMap<Currency, BigDecimal>, MutableMap<Currency, Throwable>>
val RatesResult.loadedRates
get() = this.first
val RatesResult.failedRates
get() = this.second

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
@ -87,12 +88,13 @@ class TangemSdkManager(
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<CreateProductWalletTaskResponse> {
return runTaskAsync(
CreateProductWalletTask(
runnable = CreateProductWalletTask(
cardTypesResolver = scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
),
scanResponse.card.cardId,
Message(resources.getString(R.string.initial_message_create_wallet_body)),
cardId = scanResponse.card.cardId,
initialMessage = Message(resources.getString(R.string.initial_message_create_wallet_body)),
iconScanRes = if (scanResponse.cardTypesResolver.isRing()) R.drawable.img_hand_scan_ring else null,
)
}
@ -100,7 +102,7 @@ class TangemSdkManager(
scanResponse: ScanResponse,
mnemonic: String,
): CompletionResult<CreateProductWalletTaskResponse> {
val mnemonic = try {
val defaultMnemonic = try {
DefaultMnemonic(mnemonic, tangemSdk.wordlist)
} catch (e: TangemSdkError.MnemonicException) {
return CompletionResult.Failure(e)
@ -109,7 +111,7 @@ class TangemSdkManager(
CreateProductWalletTask(
scanResponse.cardTypesResolver,
derivationStyleProvider = scanResponse.derivationStyleProvider,
mnemonic,
defaultMnemonic,
),
scanResponse.card.cardId,
Message(resources.getString(R.string.initial_message_create_wallet_body)),
@ -221,9 +223,10 @@ class TangemSdkManager(
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,
@DrawableRes iconScanRes: Int? = null,
): CompletionResult<T> = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode) { result ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result ->
if (continuation.isActive) continuation.resume(result)
}
}

View file

@ -209,9 +209,7 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val isWallet2 = card.settings.isKeysImportAllowed || card.firmwareVersion >= FirmwareVersion.KeysImportAvailable
val productType = if (isWallet2) ProductType.Wallet2 else ProductType.Wallet
val productType = getWalletProductType(card)
val config = CardConfig.createConfig(card)
scope.launch {
val scanResponse = ScanResponse(
@ -239,6 +237,19 @@ private class ScanWalletProcessor(
}
}
private fun getWalletProductType(card: CardDTO): ProductType {
if (card.batchId == CardDTO.RING_BATCH_ID) {
return ProductType.Ring
}
return if (card.firmwareVersion >= FirmwareVersion.Ed25519Slip0010Available &&
card.settings.isKeysImportAllowed
) {
ProductType.Wallet2
} else {
ProductType.Wallet
}
}
private suspend fun collectDerivations(
card: CardDTO,
config: CardConfig,

View file

@ -108,9 +108,9 @@ class UserTokensRepository(
.fold(
onSuccess = { response ->
response.getOrThrow()
.also { storageService.saveUserTokens(userWalletId, it) }
.tokens
.mapNotNull(Currency.Companion::fromTokenResponse)
.also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) }
.distinct()
},
onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },

View file

@ -1,7 +1,6 @@
package com.tangem.tap.domain.userWalletList.implementation
import com.tangem.common.*
import com.tangem.common.extensions.guard
import com.tangem.domain.wallets.legacy.UserWalletsListError
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
@ -16,6 +15,7 @@ import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import timber.log.Timber
@OptIn(ExperimentalCoroutinesApi::class)
internal class BiometricUserWalletsListManager(
@ -56,12 +56,30 @@ internal class BiometricUserWalletsListManager(
override val walletsCount: Int
get() = state.value.userWallets.size
override suspend fun unlock(): CompletionResult<UserWallet> {
return unlockWithBiometryInternal().mapUnlockResult()
}
override suspend fun unlock(throwIfNotAllWalletsUnlocked: Boolean): CompletionResult<UserWallet> {
return unlockWithBiometryInternal()
.mapFailure { error ->
Timber.e(error, "Unable to unlock user wallets")
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(error)
}
}
.map {
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
Timber.e("Not all user wallets have been unlocked")
throw UserWalletsListError.NotAllUserWalletsUnlocked
}
override suspend fun unlockAndSelect(selectedWalletId: UserWalletId): CompletionResult<UserWallet> {
return unlockWithBiometryInternal(selectedWalletId = selectedWalletId).mapUnlockResult()
val selectedUserWallet = selectedUserWalletSync
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
Timber.e("Unable to find selected user wallet")
throw UserWalletsListError.NoUserWalletSelected
} else {
selectedUserWallet
}
}
}
override fun lock() {
@ -86,7 +104,7 @@ internal class BiometricUserWalletsListManager(
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return if (canOverride) {
saveInternal(userWallet, changeSelectedUserWallet = true)
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
} else {
val isWalletSaved = state.value.userWallets
.any {
@ -96,7 +114,7 @@ internal class BiometricUserWalletsListManager(
if (isWalletSaved) {
CompletionResult.Failure(UserWalletsListError.WalletAlreadySaved)
} else {
saveInternal(userWallet, changeSelectedUserWallet = true)
saveInternal(userWallet, changeSelectedUserWallet = true, canOverridePublicInfo = false)
}
}
}
@ -110,7 +128,7 @@ internal class BiometricUserWalletsListManager(
update(storedUserWallet)
}
.flatMap { updatedUserWallet ->
saveInternal(updatedUserWallet, changeSelectedUserWallet = false)
saveInternal(updatedUserWallet, changeSelectedUserWallet = false, canOverridePublicInfo = true)
}
.flatMap {
get(userWalletId)
@ -164,10 +182,11 @@ internal class BiometricUserWalletsListManager(
private suspend fun saveInternal(
userWallet: UserWallet,
changeSelectedUserWallet: Boolean,
canOverridePublicInfo: Boolean,
): CompletionResult<Unit> {
return saveEncryptionKeyIfNotNull(userWallet)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
.flatMap { publicInformationRepository.save(userWallet) }
.flatMap { publicInformationRepository.save(userWallet, canOverridePublicInfo) }
.map {
if (changeSelectedUserWallet) {
selectedUserWalletRepository.set(userWallet.walletId)
@ -188,7 +207,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun unlockWithBiometryInternal(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun unlockWithBiometryInternal(): CompletionResult<Unit> {
return keysRepository.getAll()
.map { keys ->
state.update { prevState ->
@ -197,7 +216,7 @@ internal class BiometricUserWalletsListManager(
)
}
}
.flatMap { loadModels(selectedWalletId = selectedWalletId) }
.flatMap { loadModels() }
.map {
state.update { prevState ->
val hasLockedUserWallets = prevState.userWallets.any { it.isLocked }
@ -206,24 +225,6 @@ internal class BiometricUserWalletsListManager(
}
}
private fun CompletionResult<Unit>.mapUnlockResult(): CompletionResult<UserWallet> {
return this
.mapFailure { error ->
if (error is UserWalletsListError) {
error
} else {
UserWalletsListError.UnableToUnlockUserWallets(cause = error)
}
}
.map {
selectedUserWalletSync.guard {
throw UserWalletsListError.UnableToUnlockUserWallets(
cause = IllegalStateException("No user wallet selected"),
)
}
}
}
private suspend fun saveEncryptionKeyIfNotNull(userWallet: UserWallet): CompletionResult<ByteArray?> {
val encryptionKey = userWallet.scanResponse.card.encryptionKey
?.let { UserWalletEncryptionKey(userWallet.walletId, it) }
@ -245,7 +246,7 @@ internal class BiometricUserWalletsListManager(
}
}
private suspend fun loadModels(selectedWalletId: UserWalletId? = null): CompletionResult<Unit> {
private suspend fun loadModels(): CompletionResult<Unit> {
return getSavedUserWallets()
.map { userWallets ->
if (userWallets.isNotEmpty()) {
@ -255,7 +256,7 @@ internal class BiometricUserWalletsListManager(
prevState.copy(
userWallets = wallets,
selectedUserWalletId = findOrSetSelectedUserWalletId(
prevSelectedWalletId = selectedWalletId ?: prevState.selectedUserWalletId,
prevSelectedWalletId = prevState.selectedUserWalletId,
userWallets = wallets,
),
)
@ -316,7 +317,7 @@ internal class BiometricUserWalletsListManager(
private fun findSelectedUserWallet(userWallets: List<UserWallet> = state.value.userWallets): UserWallet? {
return userWallets.firstOrNull {
it.walletId == state.value.selectedUserWalletId && !it.isLocked
it.walletId == state.value.selectedUserWalletId
}
}

View file

@ -6,7 +6,7 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation
internal interface UserWalletsPublicInformationRepository {
suspend fun save(userWallet: UserWallet): CompletionResult<Unit>
suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit>
suspend fun getAll(): CompletionResult<List<UserWalletPublicInformation>>

View file

@ -38,7 +38,7 @@ internal class BiometricUserWalletsKeysRepository(
is TangemSdkError.AuthenticationPermanentLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
is TangemSdkError.KeystoreInvalidated ->
UserWalletsListError.EncryptionKeyInvalidated
UserWalletsListError.AllKeysInvalidated
is TangemSdkError.AuthenticationUnavailable ->
UserWalletsListError.BiometricsAuthenticationDisabled
else -> error

View file

@ -24,18 +24,23 @@ internal class DefaultUserWalletsPublicInformationRepository(
Types.newParameterizedType(List::class.java, UserWalletPublicInformation::class.java),
)
override suspend fun save(userWallet: UserWallet): CompletionResult<Unit> {
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return withContext(Dispatchers.IO) {
getAll()
.flatMap { savedInformation ->
val infoToSave = withContext(Dispatchers.Default) {
savedInformation.addOrReplace(userWallet.publicInformation) {
userWallet.walletId == it.walletId
}
getAll().flatMap { savedWalletsInformation ->
val infoToSave = if (canOverride) {
savedWalletsInformation.addOrReplace(userWallet.publicInformation) {
it.walletId == userWallet.walletId
}
} else {
if (savedWalletsInformation.none { it.walletId == userWallet.walletId }) {
savedWalletsInformation + userWallet.publicInformation
} else {
return@withContext CompletionResult.Success(Unit)
}
save(infoToSave)
}
save(infoToSave)
}
}
}

View file

@ -16,7 +16,6 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -31,12 +30,14 @@ import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
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.demo.isDemoCard
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage.WCSignType.*
import timber.log.Timber
import java.math.BigDecimal
@ -49,7 +50,7 @@ class WalletConnectSdkHelper {
val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null
val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null
walletManager.safeUpdate()
walletManager.safeUpdate(isDemoCard())
val wallet = walletManager.wallet
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
@ -108,13 +109,20 @@ class WalletConnectSdkHelper {
)
}
private fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val blockchainNetwork = BlockchainNetwork(
fun isDemoCard(): Boolean {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false
return userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null
val walletManagerFacade = store.state.daggerGraphState
.get(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = derivationPath,
tokens = emptyList(),
)
return store.state.walletState.getWalletManager(blockchainNetwork)
}
suspend fun completeTransaction(data: WcTransactionData, cardId: String?): String? {

View file

@ -1,23 +1,28 @@
package com.tangem.tap.domain.walletconnect2.app
import com.tangem.blockchain.common.*
import com.tangem.domain.common.extensions.*
import com.tangem.tap.domain.walletconnect2.domain.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
class TangemWcBlockchainHelper : WcBlockchainHelper {
override fun chainIdToNetworkIdOrNull(chainId: String): String? {
val parsed = chainId.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
when {
parsed.first() == EVM_NAMESPACE -> {
val chainIdInt = parsed[1].toIntOrNull() ?: return null
return Blockchain.fromChainId(chainIdInt)?.toNetworkId()
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.toNetworkId()
}
override fun chainIdToMissingNetworkNameOrNull(chainId: String): String? {
val parsedId = chainId.parseId() ?: return null
val blockchain = parsedId.chainIdToBlockchain()
return blockchain?.fullName
?: if (parsedId.first == EVM_NAMESPACE) {
chainId
} else {
parsedId.first.replaceFirstChar(Char::titlecase)
}
else -> {
val blockchain = Blockchain.fromNetworkId(parsed.first())
return if (supportedNonEvmBlockchains.contains(blockchain)) blockchain?.toNetworkId() else null
}
}
}
override fun networkIdToChainIdOrNull(networkId: String): String? {
@ -57,6 +62,26 @@ private fun Blockchain.getCaip2Namespace(): String? {
}
}
private fun String.parseId(): Pair<String, String>? {
val parsed = this.split(CHAIN_SEPARATOR)
if (parsed.size != 2) return null
return parsed[0] to parsed[1]
}
private fun Pair<String, String>.chainIdToBlockchain(): Blockchain? {
return when (first) {
EVM_NAMESPACE -> {
second.toIntOrNull()
?.let(Blockchain::fromChainId)
}
else -> {
Blockchain.fromNetworkId(networkId = first)
.takeIf(supportedNonEvmBlockchains::contains)
}
}
}
private val supportedNonEvmBlockchains = emptySet<Blockchain>() // TODO: add supported networks
private const val EVM_NAMESPACE = "eip155"

View file

@ -75,9 +75,11 @@ class WalletConnectInteractor(
is WalletConnectEvents.SessionApprovalError -> {
val error = when (wcEvent.error) {
is WalletConnectError.ApprovalErrorMissingNetworks -> {
val missingNetworks = wcEvent.error.missingChains
.map { blockchainHelper.chainIdToNetworkIdOrNull(it) }
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks.filterNotNull())
val missingNetworks = wcEvent.error.missingChains.mapNotNull {
blockchainHelper.chainIdToMissingNetworkNameOrNull(it)
}
WalletConnectError.ApprovalErrorAddNetwork(missingNetworks)
}
else -> wcEvent.error
}

View file

@ -3,6 +3,8 @@ package com.tangem.tap.domain.walletconnect2.domain
interface WcBlockchainHelper {
fun chainIdToNetworkIdOrNull(chainId: String): String?
fun chainIdToMissingNetworkNameOrNull(chainId: String): String?
fun networkIdToChainIdOrNull(networkId: String): String?
fun getNamespaceFromFullChainIdOrNull(chainId: String): String?

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.core.TangemError
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
@ -15,5 +16,6 @@ interface CustomTokenInteractor {
suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
/** Save token [customCurrency] */
@Throws(TangemError::class)
suspend fun saveToken(customCurrency: CustomCurrency)
}

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
@ -16,6 +17,7 @@ import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
@ -47,6 +49,14 @@ class DefaultCustomTokenInteractor(
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
) : CustomTokenInteractor {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
return featureRepository.findToken(
address = address,
@ -60,8 +70,12 @@ class DefaultCustomTokenInteractor(
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(userWallet, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(userWallet = userWallet, currencyList = listOf(currency)) {
submitAdd(userWallet = userWallet, currency = currency)
deriveMissingBlockchains(
userWallet = userWallet,
currencyList = listOf(currency),
onSuccess = { submitAdd(userWallet = userWallet.copy(scanResponse = it), currency = currency) },
) {
throw it
}
} else {
submitAdd(userWallet, currency)
@ -77,6 +91,7 @@ class DefaultCustomTokenInteractor(
userWallet: UserWallet,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
onFailure: suspend (TangemError) -> Unit,
) {
val scanResponse = userWallet.scanResponse
val config = CardConfig.createConfig(scanResponse.card)
@ -121,6 +136,7 @@ class DefaultCustomTokenInteractor(
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
onFailure.invoke(result.error)
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
@ -219,21 +235,13 @@ class DefaultCustomTokenInteractor(
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
val networks = currencyList.map { it.network }.toSet()
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = networks,
refresh = true,
)
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}
}

View file

@ -55,7 +55,7 @@ internal object AddCustomTokenPreviewData {
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
),

View file

@ -270,7 +270,7 @@ internal class AddCustomTokenViewModel @Inject constructor(
value = "",
onValueChange = actionsHandler::onTokenSymbolValueChange,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title_old),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
)
@ -898,7 +898,9 @@ internal class AddCustomTokenViewModel @Inject constructor(
viewModelScope.launch(dispatchers.io) {
runCatching { featureInteractor.saveToken(currency) }
.onSuccess { featureRouter.openWalletScreen() }
.onFailure(Timber::e)
.onFailure {
Timber.e(it)
}
}
}
}

View file

@ -558,15 +558,10 @@ class DetailsMiddleware {
return userWalletsListManager.save(userWallet)
.doOnSuccess {
store.onUserWalletSelected(userWallet)
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
val walletFeatureToggles = store.state.daggerGraphState
.get(DaggerGraphState::walletFeatureToggles)
if (!walletFeatureToggles.isRedesignedScreenEnabled) {
store.onUserWalletSelected(userWallet)
}
}
}

View file

@ -93,6 +93,8 @@ private fun handlePrepareCardSettingsScreen(
cardTypesResolver: CardTypesResolver,
state: DetailsState,
): DetailsState {
val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2()
val isShowPasswordResetRadioButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active
val cardSettingsState = CardSettingsState(
cardInfo = card.toCardInfo(cardTypesResolver),
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
@ -110,6 +112,7 @@ private fun handlePrepareCardSettingsScreen(
} else {
null
},
isShowPasswordResetRadioButton = isShowPasswordResetRadioButton,
)
return state.copy(cardSettingsState = cardSettingsState)
}
@ -152,22 +155,28 @@ private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: Card
}
private fun handleEraseWallet(action: DetailsAction.ResetToFactory, state: DetailsState): DetailsState {
val cardSettingsState = state.cardSettingsState
return when (action) {
is DetailsAction.ResetToFactory.AcceptCondition1 -> {
val warning1Checked = action.accepted
val resetButtonEnabled = if (cardSettingsState?.isShowPasswordResetRadioButton == true) {
warning1Checked && cardSettingsState.condition2Checked
} else {
warning1Checked
}
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
cardSettingsState = cardSettingsState?.copy(
condition1Checked = action.accepted,
resetButtonEnabled = warning1Checked && state.cardSettingsState.condition2Checked,
resetButtonEnabled = resetButtonEnabled,
),
)
}
is DetailsAction.ResetToFactory.AcceptCondition2 -> {
val warning2Checked = action.accepted
state.copy(
cardSettingsState = state.cardSettingsState?.copy(
cardSettingsState = cardSettingsState?.copy(
condition2Checked = action.accepted,
resetButtonEnabled = warning2Checked && state.cardSettingsState.condition1Checked,
resetButtonEnabled = warning2Checked && cardSettingsState.condition1Checked,
),
)
}

View file

@ -45,6 +45,7 @@ data class CardSettingsState(
val condition1Checked: Boolean,
val condition2Checked: Boolean,
val accessCodeRecovery: AccessCodeRecoveryState? = null,
val isShowPasswordResetRadioButton: Boolean,
)
data class ManageSecurityState(

View file

@ -8,7 +8,6 @@ import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.common.BlockchainNetwork
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
@ -324,10 +323,9 @@ class WalletConnectMiddleware {
is WalletConnectAction.SessionRejected -> {
when (action.error) {
is WalletConnectError.ApprovalErrorAddNetwork -> {
val blockchains = action.error.networks.mapNotNull { Blockchain.fromNetworkId(it)?.fullName }
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.AddNetwork(blockchains.toString()),
WalletConnectDialog.UnsupportedNetwork(action.error.networks),
),
)
}

View file

@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.core.navigation.StateDialog
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents

View file

@ -6,16 +6,13 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.userwallets.Artwork
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -32,7 +29,7 @@ internal fun CardSettingsScreen(
modifier = modifier,
content = {
if (needReadCard) {
CardSettingsReadCard(state.onScanCardClick, state.cardImage)
CardSettingsReadCard(state.onScanCardClick)
} else {
CardSettings(state = state)
}
@ -44,39 +41,39 @@ internal fun CardSettingsScreen(
@Suppress("MagicNumber")
@Composable
private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwork?) {
private fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(top = TangemTheme.dimens.size70),
modifier = Modifier.fillMaxSize(),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(start = TangemTheme.dimens.size16, end = TangemTheme.dimens.size16),
.padding(bottom = TangemTheme.dimens.spacing40),
) {
val circleColor = TangemTheme.colors.stroke.primary
Canvas(
Image(
modifier = Modifier
.size(300.dp)
.align(Alignment.Center),
) {
drawCircle(
color = circleColor,
radius = size.minDimension / 2.0f,
)
}
SubcomposeAsyncImage(
model = ImageRequest.Builder(context = LocalContext.current)
.data(cardArtwork?.artworkId)
.crossfade(enable = true)
.build(),
modifier = Modifier
.align(Alignment.Center)
.fillMaxWidth(),
loading = { /* no-op */ },
error = { /* no-op */ },
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing80,
end = TangemTheme.dimens.spacing80,
top = TangemTheme.dimens.spacing70,
)
.rotate(-15f),
painter = painterResource(id = R.drawable.card_placeholder_secondary),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
Image(
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing60,
end = TangemTheme.dimens.spacing60,
)
.rotate(-1f),
painter = painterResource(id = R.drawable.card_placeholder_black),
contentDescription = null,
contentScale = ContentScale.FillWidth,
)
}
Spacer(modifier = Modifier.weight(1f))
@ -84,9 +81,9 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwo
modifier = Modifier
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.size16,
end = TangemTheme.dimens.size16,
bottom = TangemTheme.dimens.size32,
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing32,
),
) {
Text(
@ -103,7 +100,7 @@ private fun CardSettingsReadCard(onScanCardClick: () -> Unit, cardArtwork: Artwo
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false),
)
Spacer(modifier = Modifier.size(TangemTheme.dimens.size28))
Spacer(modifier = Modifier.size(TangemTheme.dimens.size32))
DetailsMainButton(
title = stringResource(id = R.string.scan_card_settings_button),
onClick = onScanCardClick,
@ -122,21 +119,21 @@ private fun CardSettings(state: CardSettingsScreenState) {
) {
items(state.cardDetails) {
val paddingBottom = when (it) {
is CardInfo.CardId, is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
is CardInfo.CardId, is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing14
is CardInfo.SecurityMode -> TangemTheme.dimens.spacing16
is CardInfo.ChangeAccessCode -> TangemTheme.dimens.spacing16
is CardInfo.AccessCodeRecovery -> TangemTheme.dimens.spacing16
is CardInfo.ResetToFactorySettings -> TangemTheme.dimens.spacing28
}
val paddingTop = when (it) {
is CardInfo.CardId -> 0.dp
is CardInfo.Issuer -> 12.dp
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
is CardInfo.CardId -> TangemTheme.dimens.spacing0
is CardInfo.Issuer -> TangemTheme.dimens.spacing12
is CardInfo.SignedHashes -> TangemTheme.dimens.spacing12
is CardInfo.SecurityMode -> TangemTheme.dimens.spacing14
is CardInfo.ChangeAccessCode -> TangemTheme.dimens.spacing16
is CardInfo.AccessCodeRecovery -> TangemTheme.dimens.spacing16
is CardInfo.ResetToFactorySettings -> TangemTheme.dimens.spacing16
}
Column(
modifier = Modifier
@ -145,7 +142,12 @@ private fun CardSettings(state: CardSettingsScreenState) {
enabled = it.clickable,
onClick = { state.onElementClick(it) },
)
.padding(start = 20.dp, end = 20.dp, bottom = paddingBottom, top = paddingTop),
.padding(
start = TangemTheme.dimens.spacing20,
end = TangemTheme.dimens.spacing20,
bottom = paddingBottom,
top = paddingTop,
),
) {
val titleColor = if (it.clickable) {
TangemTheme.colors.text.primary1
@ -162,7 +164,7 @@ private fun CardSettings(state: CardSettingsScreenState) {
color = titleColor,
style = TangemTheme.typography.subtitle1,
)
Spacer(modifier = Modifier.size(4.dp))
Spacer(modifier = Modifier.size(TangemTheme.dimens.size4))
Text(
text = it.subtitle.resolveReference(),
color = subtitleColor,

View file

@ -1,48 +1,35 @@
package com.tangem.tap.features.details.ui.cardsettings.coderecovery
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.compose.ui.Modifier
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.AndroidEntryPoint
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
class AccessCodeRecoveryFragment : Fragment(), StoreSubscriber<DetailsState> {
@AndroidEntryPoint
class AccessCodeRecoveryFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
private val viewModel = AccessCodeRecoveryViewModel(store)
private var screenState: MutableState<AccessCodeRecoveryScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setContent {
isTransitionGroup = true
TangemTheme {
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
}
}
@Composable
override fun ScreenContent(modifier: Modifier) {
AccessCodeRecoveryScreen(
state = screenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
override fun onStart() {

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.wallet.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -47,7 +46,7 @@ internal sealed class SettingsItem(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = stringReference(value = "Add new wallet"),
title = resourceReference(R.string.user_wallet_list_add_button),
)
data class ScanWallet(
@ -55,7 +54,7 @@ internal sealed class SettingsItem(
override val onClick: () -> Unit,
) : SettingsItem(
iconResId = R.drawable.ic_plus_24,
title = stringReference(value = "Scan new wallet"),
title = resourceReference(R.string.scan_card_settings_button),
)
data class LinkMoreCards(

View file

@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
@ -128,51 +129,51 @@ internal class DetailsViewModel(
}
private fun navigateToToS() {
store.dispatch(DisclaimerAction.Show(AppScreen.Details))
store.dispatchOnMain(DisclaimerAction.Show(AppScreen.Details))
}
private fun navigateToReferralProgram() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
}
private fun sendFeedback() {
Analytics.send(Settings.ButtonSendFeedback())
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
store.dispatchOnMain(GlobalAction.SendEmail(FeedbackEmail()))
}
private fun navigateToChat() {
Analytics.send(Settings.ButtonChat())
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
store.dispatchOnMain(GlobalAction.OpenChat(SupportInfo()))
}
private fun navigateToAppSettings() {
Analytics.send(Settings.ButtonAppSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AppSettings))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.AppSettings))
}
private fun navigateToCardSettings() {
Analytics.send(Settings.ButtonCardSettings())
store.dispatch(NavigationAction.NavigateTo(AppScreen.CardSettings))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.CardSettings))
}
private fun linkMoreCards() {
Analytics.send(Settings.ButtonCreateBackup())
store.dispatch(WalletAction.MultiWallet.BackupWallet)
store.dispatchOnMain(WalletAction.MultiWallet.BackupWallet)
}
private fun scanAndSaveUserWallet() {
Analytics.send(Settings.ScanNewCard)
store.dispatch(DetailsAction.ScanAndSaveUserWallet)
store.dispatchOnMain(DetailsAction.ScanAndSaveUserWallet)
}
private fun navigateToWalletConnect() {
Analytics.send(Settings.ButtonWalletConnect())
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
Analytics.send(Settings.ButtonSocialNetwork(link.network))
store.dispatch(NavigationAction.OpenUrl(link.url))
store.dispatchOnMain(NavigationAction.OpenUrl(link.url))
}
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {

View file

@ -22,7 +22,7 @@ internal class ResetCardFragment : ComposeFragment(), StoreSubscriber<DetailsSta
private val viewModel = ResetCardViewModel(store)
private var screenState: MutableState<ResetCardScreenState> =
mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState))
mutableStateOf(ResetCardScreenState.InitialState)
@Composable
override fun ScreenContent(modifier: Modifier) {

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -30,15 +29,23 @@ import com.tangem.wallet.R
internal fun ResetCardScreen(state: ResetCardScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
SettingsScreensScaffold(
modifier = modifier,
content = { ResetCardView(state = state) },
content = {
when (state) {
is ResetCardScreenState.ResetCardScreenContent -> {
ResetCardView(state = state)
}
ResetCardScreenState.InitialState -> {
// do nothing for now, just white screen
}
}
},
onBackClick = onBackClick,
)
}
@OptIn(ExperimentalAnimationApi::class)
@Suppress("LongMethod", "MagicNumber")
@Composable
private fun ResetCardView(state: ResetCardScreenState) {
private fun ResetCardView(state: ResetCardScreenState.ResetCardScreenContent) {
Column(
modifier = Modifier
.fillMaxSize()
@ -80,17 +87,24 @@ private fun ResetCardView(state: ResetCardScreenState) {
Spacer(modifier = Modifier.size(28.dp))
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
state.warningsToShow.forEach {
when (it) {
ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS -> {
ConditionCheckBox(
checkedState = state.acceptCondition1Checked,
onCheckedChange = state.onAcceptCondition1ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_1),
)
}
ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE -> {
ConditionCheckBox(
checkedState = state.acceptCondition2Checked,
onCheckedChange = state.onAcceptCondition2ToggleClick,
description = TextReference.Res(R.string.reset_card_to_factory_condition_2),
)
}
}
}
Spacer(modifier = Modifier.size(16.dp))
Box(
@ -111,7 +125,6 @@ private fun ResetCardView(state: ResetCardScreenState) {
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
private fun ConditionCheckBox(checkedState: Boolean, onCheckedChange: (Boolean) -> Unit, description: TextReference) {
Row(
@ -162,8 +175,9 @@ private fun ResetCardScreenSample(modifier: Modifier = Modifier) {
.background(TangemTheme.colors.background.secondary),
) {
ResetCardScreen(
state = ResetCardScreenState(
state = ResetCardScreenState.ResetCardScreenContent(
accepted = true,
warningsToShow = listOf(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS),
descriptionText = TextReference.Res(R.string.reset_card_with_backup_to_factory_message),
onAcceptCondition1ToggleClick = {},
onAcceptCondition2ToggleClick = {},

View file

@ -2,15 +2,25 @@ package com.tangem.tap.features.details.ui.resetcard
import com.tangem.tap.features.details.ui.cardsettings.TextReference
internal data class ResetCardScreenState(
val accepted: Boolean = false,
val descriptionText: TextReference,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) {
val resetButtonEnabled: Boolean
get() = accepted
internal sealed class ResetCardScreenState {
object InitialState : ResetCardScreenState()
data class ResetCardScreenContent(
val accepted: Boolean = false,
val descriptionText: TextReference,
val warningsToShow: List<WarningsToReset>,
val acceptCondition1Checked: Boolean = false,
val acceptCondition2Checked: Boolean = false,
val onAcceptCondition1ToggleClick: (Boolean) -> Unit,
val onAcceptCondition2ToggleClick: (Boolean) -> Unit,
val onResetButtonClick: () -> Unit,
) : ResetCardScreenState() {
val resetButtonEnabled: Boolean
get() = accepted
}
internal enum class WarningsToReset {
LOST_WALLET_ACCESS, LOST_PASSWORD_RESTORE
}
}

View file

@ -9,14 +9,23 @@ import org.rekotlin.Store
internal class ResetCardViewModel(private val store: Store<AppState>) {
fun updateState(state: CardSettingsState?): ResetCardScreenState {
fun updateState(state: CardSettingsState?): ResetCardScreenState.ResetCardScreenContent {
val descriptionText = state?.cardInfo
?.toResetCardDescriptionText()
?: TextReference.Str(value = "")
return ResetCardScreenState(
val warningsToShow = buildList {
add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS)
if (state?.isShowPasswordResetRadioButton == true) {
add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE)
}
}
return ResetCardScreenState.ResetCardScreenContent(
accepted = state?.resetButtonEnabled ?: false,
descriptionText = descriptionText,
warningsToShow = warningsToShow,
acceptCondition1Checked = state?.condition1Checked ?: false,
acceptCondition2Checked = state?.condition2Checked ?: false,
onAcceptCondition1ToggleClick = { store.dispatch(DetailsAction.ResetToFactory.AcceptCondition1(it)) },

View file

@ -27,6 +27,5 @@ private fun internalReduce(action: Action, state: AppState): DisclaimerState {
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
progressState = action.state,
)
else -> disclaimerState
}
}

View file

@ -1,36 +1,36 @@
package com.tangem.tap.features.home
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.domain.tokens.TokensAction
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.store
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
@AndroidEntryPoint
class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
class HomeFragment : ComposeFragment(), StoreSubscriber<HomeState> {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
@ -42,38 +42,28 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
store.dispatch(HomeAction.Init)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@Composable
override fun ScreenContent(modifier: Modifier) {
// sync adding story before screen creation
// if (learn2earnViewModel.uiState.storyScreenState.isVisible) {
// store.dispatch(HomeAction.InsertStory(position = 0, Stories.OneInchPromo))
// homeState.value = store.state.homeState
// }
return ComposeView(inflater.context).apply {
setContent {
TangemTheme {
BackHandler {
requireActivity().finish()
}
SystemBarsEffect {
setSystemBarsColor(
color = Color.Transparent,
darkIcons = false,
)
}
ScreenContent()
}
}
BackHandler(onBack = requireActivity()::finish)
SystemBarsEffect {
setSystemBarsColor(color = Color.Transparent, darkIcons = false)
}
ScreenContent()
}
override fun onStart() {
super.onStart()
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
}.select { it.homeState }
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
.select(AppState::homeState)
}
}
@ -82,11 +72,6 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
store.unsubscribe(this)
}
override fun onDestroyView() {
super.onDestroyView()
rollbackStatusBarIconsColor()
}
override fun newState(state: HomeState) {
if (activity == null || view == null) return
@ -101,11 +86,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
onLearn2earnClick = {}, // learn2earnViewModel.uiState.storyScreenState.onClick,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
lifecycleScope.launch {
store.dispatch(
HomeAction.ReadCard(scope = this),
)
}
store.dispatch(action = HomeAction.ReadCard(scope = requireActivity().lifecycleScope))
},
onShopButtonClick = {
Analytics.send(IntroductionProcess.ButtonBuyCards())
@ -118,17 +99,4 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
},
)
}
/*
* !!! Workaround !!!
* Used to roll back the color of icons in the system bars after the stories screen
*/
private fun rollbackStatusBarIconsColor() {
val windowInsetsController = WindowInsetsControllerCompat(
activity?.window ?: return,
view ?: return,
)
windowInsetsController.isAppearanceLightStatusBars = true
windowInsetsController.isAppearanceLightNavigationBars = true
}
}

View file

@ -96,6 +96,8 @@ fun StoriesTextAnimation(
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
@ -117,7 +119,7 @@ fun StoriesBottomImageAnimation(
)
},
label = "Appearing scale",
) { value -> if (value) SCALE_SWITCH_BARRIER else initialScale }
) { value -> if (value) secondStageScale else initialScale }
val secondTransition = updateTransition(
targetState = isSecondStepLaunched.value,
@ -131,14 +133,14 @@ fun StoriesBottomImageAnimation(
)
},
label = "Outgoing scale",
) { value -> if (value) 1f else SCALE_SWITCH_BARRIER }
) { value -> if (value) targetScale else secondStageScale }
val fadeIn = firstTransition.animateFloat(
transitionSpec = { tween(durationMillis = 400) },
label = "Fade in on start",
) { value -> if (value) 1f else 0f }
if (firstScaleStep.value == SCALE_SWITCH_BARRIER) {
if (firstScaleStep.value == secondStageScale) {
isSecondStepLaunched.value = true
}

View file

@ -105,6 +105,9 @@ fun StoriesWalletForEveryone(stepDuration: Int) {
SpacerH32()
BoxWithGradient {
StoriesBottomImageAnimation(
initialScale = 2.6f,
secondStageScale = 1.2f,
targetScale = 1.1f,
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
@ -184,8 +187,8 @@ private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = M
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = modifier.fillMaxWidth(),
contentScale = ContentScale.Inside,
modifier = modifier.fillMaxSize(),
)
}

View file

@ -4,8 +4,8 @@ import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -54,34 +54,30 @@ fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
textAlign = TextAlign.Center,
)
Box {
Column(
modifier = Modifier
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(TangemTheme.dimens.spacing94)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.weight(1f),
text = stringResource(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
SpacerH(TangemTheme.dimens.spacing46)
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerH(TangemTheme.dimens.spacing94)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier,
text = stringResource(R.string.story_meet_title),
style = style,
color = TangemColorPalette.White,
textAlign = TextAlign.Center,
)
}
SpacerH(TangemTheme.dimens.spacing46)
Image(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth(),
painter = painterResource(R.drawable.img_meet_tangem),
contentScale = ContentScale.FillWidth,
contentScale = ContentScale.Inside,
contentDescription = "Tangem Wallet card",
)
}

View file

@ -36,24 +36,26 @@ fun StoriesProgressBar(
val animatorSpeed = Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
0f,
1f,
)
LaunchedEffect(paused, currentStep) {
LaunchedEffect(paused, currentStep, animatorSpeed) {
if (paused) {
progress.stop()
} else {
if (animatorSpeed == 0f) {
progress.snapTo(1f)
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
}
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing,
),
)
progress.snapTo(0f)
onStepFinish()
}
}

View file

@ -17,9 +17,8 @@ sealed class HomeAction : Action {
/**
* Action for scanning card
*
* @property analyticsEvent analytics event
* @property lifecycleCoroutineScope lifecycle scope. It will be canceled when lifecycle-aware component is
* destroyed.
* @property analyticsEvent analytics event
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
*/
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Introduction),

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.net.Uri
import com.tangem.core.analytics.Analytics
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenScreenEvent
import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
@ -21,7 +21,7 @@ class BuyCurrencyIntentHandler : IntentHandler {
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
return if (data.host == successUri.host && data.authority == successUri.authority) {
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(TokenScreenEvent.Bought(currencyType.value))
Analytics.send(TokenScreenAnalyticsEvent.Bought(currencyType.value))
true
} else {
false

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.StateDialog
import com.tangem.core.navigation.StateDialog
/**
[REDACTED_AUTHOR]

View file

@ -38,7 +38,7 @@ object OnboardingHelper {
}
}
response.cardTypesResolver.isWallet2() -> {
response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
@ -53,7 +53,10 @@ object OnboardingHelper {
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
return when (scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Wallet, ProductType.Wallet2 -> if (scanResponse.card.settings.isBackupAllowed) {
ProductType.Wallet,
ProductType.Wallet2,
ProductType.Ring,
-> if (scanResponse.card.settings.isBackupAllowed) {
AppScreen.OnboardingWallet
} else {
AppScreen.OnboardingOther

View file

@ -16,6 +16,7 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import com.tangem.tap.features.demo.isDemoCard
import timber.log.Timber
import java.math.BigDecimal
@ -38,7 +39,8 @@ class OnboardingManager(
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
val balance = when (val result = walletManager.safeUpdate()) {
val isDemoCard = scanResponse.isDemoCard()
val balance = when (val result = walletManager.safeUpdate(isDemoCard)) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import androidx.lifecycle.LifecycleCoroutineScope
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
@ -18,7 +18,7 @@ sealed class OnboardingWalletAction : Action {
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
data class FinishOnboarding(val lifecycleCoroutineScope: LifecycleCoroutineScope) : OnboardingWalletAction()
data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction()
object ResumeBackup : OnboardingWalletAction()

View file

@ -5,6 +5,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
@ -18,6 +20,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.Artwork
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -33,6 +36,7 @@ import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.R
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
@ -92,7 +96,21 @@ private fun handleWalletAction(action: Action) {
is OnboardingWalletAction.LoadArtwork -> {
scope.launch {
val cardArtwork = when (onboardingManager) {
null -> action.cardArtworkUriForUnfinishedBackup
null -> {
// onboardingManager is null when backup started from previously interrupted
val primaryCardId = backupService.primaryCardId
val cardPublicKey = backupService.primaryPublicKey
if (primaryCardId != null && cardPublicKey != null) {
// uses when no scanResponse and backup state restored
loadArtworkForUnfinishedBackup(
cardId = primaryCardId,
cardPublicKey = cardPublicKey,
defaultArtwork = action.cardArtworkUriForUnfinishedBackup,
)
} else {
action.cardArtworkUriForUnfinishedBackup
}
}
else -> onboardingManager.loadArtworkUrl()
.takeIf { it != Artwork.DEFAULT_IMG_URL }
?.let { Uri.parse(it) }
@ -152,7 +170,7 @@ private fun handleWalletAction(action: Action) {
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard(scope = action.lifecycleCoroutineScope))
store.dispatch(HomeAction.ReadCard(scope = action.scope))
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
@ -185,6 +203,24 @@ private fun handleWalletAction(action: Action) {
}
}
private suspend fun loadArtworkForUnfinishedBackup(
cardId: String,
cardPublicKey: ByteArray,
defaultArtwork: Uri?,
): Uri {
return when (val cardInfo = OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)) {
is Result.Success -> {
val artworkId = cardInfo.data.artwork?.id
if (artworkId.isNullOrEmpty()) {
defaultArtwork ?: Uri.EMPTY
} else {
Uri.parse(OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId))
}
}
is Result.Failure -> defaultArtwork ?: Uri.EMPTY
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action) {
val globalState = store.state.globalState
@ -331,7 +367,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.ScanPrimaryCard -> {
backupService.readPrimaryCard(cardId = card?.cardId) { result ->
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = card?.cardId) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.StartAddingBackupCards)
@ -398,7 +439,12 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
is BackupAction.WritePrimaryCard -> {
backupService.proceedBackup { result ->
val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) {
R.drawable.img_hand_scan_ring
} else {
null
}
backupService.proceedBackup(iconScanRes = iconScanRes) { result ->
when (result) {
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1))

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -40,6 +41,7 @@ private object ReducerForGlobalAction {
maxBackupCards = MAX_BACKUP_CARDS,
canSkipBackup = action.canSkipBackup,
),
isRingOnboarding = action.scanResponse.cardTypesResolver.isRing(),
)
}
is GlobalAction.Onboarding.StartForUnfinishedBackup -> {

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.graphics.Bitmap
import android.net.Uri
import com.tangem.tap.common.redux.StateDialog
import com.tangem.core.navigation.StateDialog
import org.rekotlin.StateType
/**
@ -14,6 +14,7 @@ data class OnboardingWalletState(
val backupState: BackupState = BackupState(),
val cardArtworkUri: Uri? = null,
val showConfetti: Boolean = false,
val isRingOnboarding: Boolean = false,
) : StateType {
@Suppress("MagicNumber")

View file

@ -177,13 +177,21 @@ class OnboardingWalletFragment :
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
state.cardArtworkUri?.let {
seedPhraseViewModel.setCardArtworkUri(it.toString())
loadImageIntoImageView(it, binding.imvFrontCard)
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(it, binding.imvFirstBackupCard)
loadImageIntoImageView(it, binding.imvSecondBackupCard)
}
}
else -> {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
if (state.isRingOnboarding) {
binding.imvFrontCard.load(R.drawable.img_ring_placeholder)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
}
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
@ -390,9 +398,9 @@ class OnboardingWalletFragment :
}
private fun showWriteBackupCard(state: BackupState) = with(binding) {
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
prepareViewForFinalizeStep()
reInitCardsWidgetIfNeeded(state.backupCardsNumber)
val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tvHeader.text = getString(R.string.onboarding_title_backup_card_format, cardNumber)
@ -427,7 +435,7 @@ class OnboardingWalletFragment :
layoutButtonsCommon.btnWalletAlternativeAction.hide()
layoutButtonsCommon.btnWalletMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingWalletAction.FinishOnboarding(lifecycleCoroutineScope = lifecycleScope))
store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope))
}
animator.showSuccess {

View file

@ -6,8 +6,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.core.TangemSdkError
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token.Send.AddressEntered
import com.tangem.tap.common.redux.AppState
@ -9,6 +10,10 @@ import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetAddressError
import com.tangem.tap.features.send.redux.AddressVerifyAction.AddressVerification.SetWalletAddress
import com.tangem.tap.features.send.redux.AddressVerifyAction.Error
import com.tangem.tap.mainScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -17,6 +22,8 @@ import org.rekotlin.DispatchFunction
*/
internal class AddressMiddleware {
private val addressValidator = AddressValidator()
fun handle(action: AddressActionUi, appState: AppState?, dispatch: (Action) -> Unit) {
when (action) {
is AddressActionUi.HandleUserInput -> handleUserInput(action.data, appState, dispatch)
@ -54,13 +61,13 @@ internal class AddressMiddleware {
dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val wallet = sendState.walletManager?.wallet ?: return
val walletManager = sendState.walletManager ?: return
val address = sendState.addressState.normalFieldValue ?: return
val isUserInput = sendState.addressState.viewFieldValue.isFromUserInput
verifyAddress(
address = address,
wallet = wallet,
walletManager = walletManager,
isUserInput = isUserInput,
dispatch = dispatch,
sourceType = sourceType,
@ -68,6 +75,38 @@ internal class AddressMiddleware {
}
private fun verifyAddress(
address: String,
walletManager: WalletManager,
sourceType: AddressEntered.SourceType?,
isUserInput: Boolean,
dispatch: (Action) -> Unit,
) {
val wallet = walletManager.wallet
mainScope.launch {
val failReason = withContext(Dispatchers.IO) {
addressValidator.validateAddress(walletManager, address)
}
if (failReason == null) {
dispatchSuccessValidationActions(
address = address,
wallet = wallet,
sourceType = sourceType,
isUserInput = isUserInput,
dispatch = dispatch,
)
} else {
dispatchFailedValidationActions(
failReason = failReason,
sourceType = sourceType,
dispatch = dispatch,
)
}
}
}
private fun dispatchSuccessValidationActions(
address: String,
wallet: Wallet,
sourceType: AddressEntered.SourceType?,
@ -113,57 +152,49 @@ internal class AddressMiddleware {
val supposedAddress = noSchemeAddress.removeShareUriQuery() // TODO: parse query?
val failReason = isValidBlockchainAddressAndNotTheSameAsWallet(wallet, supposedAddress)
if (failReason == null) {
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Success,
),
)
}
} else {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
noSchemeAddress.getQueryParameter("amount")?.toBigDecimalOrNull()?.let {
dispatch(AmountAction.SetAmount(it, false))
dispatch(AmountActionUi.CheckAmountToSend)
}
dispatch(SetWalletAddress(supposedAddress, isUserInput))
dispatch(TransactionExtrasAction.Prepare(wallet.blockchain, address, null))
dispatch(FeeAction.RequestFee)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Success,
),
)
}
}
private fun isValidBlockchainAddressAndNotTheSameAsWallet(wallet: Wallet, address: String): Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
Error.ADDRESS_SAME_AS_WALLET
}
} else {
Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
private fun dispatchFailedValidationActions(
failReason: Error,
sourceType: AddressEntered.SourceType?,
dispatch: (Action) -> Unit,
) {
dispatch(SetAddressError(failReason))
dispatch(TransactionExtrasAction.Release)
sourceType?.let {
Analytics.send(
event = AddressEntered(
sourceType = sourceType,
validationResult = AddressEntered.ValidationResult.Fail,
),
)
}
}
private fun String.removeShareUriQuery(): String = this.substringBefore("?")
private fun String.getQueryParameter(name: String): String? {
return this.substringAfter("?").splitToMap("&", "=")[name]
}
private fun verifyClipboard(input: String?, appState: AppState?, dispatch: DispatchFunction) {
val address = input ?: return
val wallet = appState?.sendState?.walletManager?.wallet ?: return
val walletManager = appState?.sendState?.walletManager ?: return
val internalDispatcher: (Action) -> Unit = {
when (it) {
@ -178,7 +209,7 @@ internal class AddressMiddleware {
verifyAddress(
address = address,
wallet = wallet,
walletManager = walletManager,
sourceType = null,
isUserInput = false,
dispatch = internalDispatcher,

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.send.redux.middlewares
import com.tangem.blockchain.blockchains.near.NearWalletManager
import com.tangem.blockchain.blockchains.near.network.NearAccount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.features.send.redux.AddressVerifyAction
internal class AddressValidator {
suspend fun validateAddress(walletManager: WalletManager, address: String): AddressVerifyAction.Error? {
val blockchain = walletManager.wallet.blockchain
val wallet = walletManager.wallet
return if ((blockchain == Blockchain.Near || blockchain == Blockchain.NearTestnet) &&
address.length != NEAR_IMPLICIT_ADDRESS_LENGTH
) {
validateNearAddress(walletManager, address)
} else {
validateAddress(wallet, address)
}
}
private suspend fun validateNearAddress(
walletManager: WalletManager,
address: String,
): AddressVerifyAction.Error? {
val result = (walletManager as? NearWalletManager)?.getAccount(address)
return if (result is Result.Success && result.data is NearAccount.Full) {
null
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
private fun validateAddress(wallet: Wallet, address: String): AddressVerifyAction.Error? {
return if (wallet.blockchain.validateAddress(address)) {
if (wallet.addresses.all { it.value != address }) {
null
} else {
AddressVerifyAction.Error.ADDRESS_SAME_AS_WALLET
}
} else {
AddressVerifyAction.Error.ADDRESS_INVALID_OR_UNSUPPORTED_BY_BLOCKCHAIN
}
}
companion object {
private const val NEAR_IMPLICIT_ADDRESS_LENGTH = 64
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -72,6 +73,13 @@ class RequestFeeMiddleware {
dispatch(FeeAction.FeeCalculation.ClearResult)
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
store.state.globalState.feedbackManager?.infoHolder?.updateOnSendError(
walletManager = walletManager,
amountToSend = destinationAmount,
feeAmount = null,
destinationAddress = destinationAddress,
)
val blockchainSdkError = feeResult.error as? BlockchainSdkError ?: return@withContext
dispatch(
SendAction.Dialog.RequestFeeError(

View file

@ -4,10 +4,10 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -57,7 +57,7 @@ data class SendState(
return if (!this.coinIsConvertible()) value else coinConverter!!.toCrypto(value)
}
fun convertFiatToToken(value: BigDecimal): BigDecimal {
private fun convertFiatToToken(value: BigDecimal): BigDecimal {
return if (!this.tokenIsConvertible()) value else tokenConverter!!.toCrypto(value)
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.datasource.config.models.SprinklrConfig
import org.rekotlin.Action
sealed interface SprinklrAction : Action {
data class Init(val userId: String, val config: SprinklrConfig) : SprinklrAction
data class UpdateUrl(val url: String) : SprinklrAction
data class UpdateSprinklrDomains(val domains: List<String>) : SprinklrAction
}

View file

@ -1,42 +0,0 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.sprinklr.redux.model.SprinklrUrl
import com.tangem.tap.store
import org.rekotlin.Middleware
internal class SprinklrMiddleware {
val middleware: Middleware<AppState> = { _, appStateProvider ->
{ next ->
{ action ->
val appState = appStateProvider()
if (action is SprinklrAction && appState != null) {
handleAction(action)
}
next(action)
}
}
}
private fun handleAction(action: SprinklrAction) {
when (action) {
is SprinklrAction.Init -> updateUrl(action.userId, action.config)
is SprinklrAction.UpdateUrl,
is SprinklrAction.UpdateSprinklrDomains,
-> Unit
}
}
private fun updateUrl(userId: String, config: SprinklrConfig) {
updateSprinklrDomains(config.baseUrl)
val url = SprinklrUrl.Prod(userId, config.baseUrl, config.appId).url
store.dispatchOnMain(SprinklrAction.UpdateUrl(url))
}
private fun updateSprinklrDomains(baseUrl: String) {
val domains = listOf(baseUrl, SprinklrUrl.Static.url)
store.dispatchOnMain(SprinklrAction.UpdateSprinklrDomains(domains))
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.tap.features.sprinklr.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
internal object SprinklrReducer {
fun reduce(action: Action, state: AppState): SprinklrState {
return if (action is SprinklrAction) {
internalReduce(action, state.sprinklrState)
} else {
state.sprinklrState
}
}
private fun internalReduce(action: SprinklrAction, state: SprinklrState): SprinklrState {
return when (action) {
is SprinklrAction.Init -> state
is SprinklrAction.UpdateUrl -> state.copy(url = action.url)
is SprinklrAction.UpdateSprinklrDomains -> state.copy(sprinklrDomains = action.domains)
}
}
}

View file

@ -1,6 +0,0 @@
package com.tangem.tap.features.sprinklr.redux
data class SprinklrState(
val url: String = "",
val sprinklrDomains: List<String> = emptyList(),
)

View file

@ -1,27 +0,0 @@
package com.tangem.tap.features.sprinklr.redux.model
internal sealed class SprinklrUrl {
abstract val url: String
class Prod(userId: String, baseUrl: String, appId: String) : SprinklrUrl() {
private val locale = java.util.Locale.getDefault().language
override val url: String = "$baseUrl/page" +
"?appId=$appId" +
"&device=$Device" +
"&enableClose=$CloseEnabled" +
"&zoom=$ZoomEnabled" +
"&locale=$locale" +
"&user_id=$userId"
companion object {
private const val Device = "MOBILE"
private const val CloseEnabled = true
private const val ZoomEnabled = false
}
}
object Static : SprinklrUrl() {
override val url: String = "live-chat-static.sprinklr.com"
}
}

View file

@ -1,54 +0,0 @@
package com.tangem.tap.features.sprinklr.ui
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeActivity
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.common.analytics.events.Chat
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@AndroidEntryPoint
internal class SprinklrActivity : ComposeActivity() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
private val viewModel by viewModels<SprinklrViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Chat.ScreenOpened())
viewModel.setNavigateBackCallback {
this.finish()
}
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val state by viewModel.state.collectAsStateWithLifecycle()
val systemBarsColor = TangemTheme.colors.background.primary
SystemBarsEffect {
setSystemBarsColor(systemBarsColor)
}
BackHandler(onBack = state.onNavigateBack)
SprinklrScreenContent(
modifier = modifier
.systemBarsPadding()
.imePadding(),
state = state,
)
}
}

View file

@ -1,25 +0,0 @@
package com.tangem.tap.features.sprinklr.ui
import android.annotation.SuppressLint
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.tangem.tap.features.sprinklr.ui.webview.SprinklrWebViewClient
@SuppressLint("SetJavaScriptEnabled")
@Composable
internal fun SprinklrScreenContent(state: SprinklrScreenState, modifier: Modifier = Modifier) {
WebView(
modifier = modifier,
state = rememberWebViewState(url = state.initialUrl),
onCreated = { webView ->
with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
}
},
client = remember { SprinklrWebViewClient(state.onNewUrl) },
)
}

View file

@ -1,10 +0,0 @@
package com.tangem.tap.features.sprinklr.ui
import com.google.accompanist.web.WebContent
internal data class SprinklrScreenState(
val initialUrl: String = "",
val sprinklrDomains: List<String> = emptyList(),
val onNavigateBack: () -> Unit = {},
val onNewUrl: WebContent.(String) -> WebContent = { this },
)

View file

@ -1,66 +0,0 @@
package com.tangem.tap.features.sprinklr.ui
import androidx.lifecycle.ViewModel
import com.google.accompanist.web.WebContent
import com.tangem.core.navigation.NavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.sprinklr.redux.SprinklrState
import com.tangem.tap.store
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.rekotlin.StoreSubscriber
internal class SprinklrViewModel : ViewModel(), StoreSubscriber<SprinklrState> {
private val stateInternal = MutableStateFlow(SprinklrScreenState())
val state = stateInternal.asStateFlow()
init {
subscribeToStoreChanges()
}
override fun newState(state: SprinklrState) {
stateInternal.update { prevState ->
prevState.copy(
initialUrl = state.url,
sprinklrDomains = state.sprinklrDomains,
onNewUrl = { updateWebContentOrOpenExternalUrl(it) },
)
}
}
override fun onCleared() {
store.unsubscribe(this)
}
fun setNavigateBackCallback(callback: () -> Unit) {
stateInternal.update { prevState ->
prevState.copy(
onNavigateBack = callback,
)
}
}
private fun subscribeToStoreChanges() {
store.subscribe(this) { appState ->
appState.skip { old, new -> old.sprinklrState == new.sprinklrState }
.select { it.sprinklrState }
}
}
private fun WebContent.updateWebContentOrOpenExternalUrl(url: String): WebContent {
return if (isExternalUrl(url)) {
store.dispatchOnMain(NavigationAction.OpenUrl(url))
this
} else {
when (this) {
is WebContent.Url -> copy(url = url)
else -> WebContent.Url(url)
}
}
}
private fun isExternalUrl(url: String): Boolean {
return state.value.sprinklrDomains.none { url.contains(it) }
}
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.sprinklr.ui.webview
import android.webkit.WebResourceRequest
import android.webkit.WebView
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebContent
internal class SprinklrWebViewClient(
private val onNewUrl: WebContent.(String) -> WebContent,
) : AccompanistWebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
// If the url hasn't changed, this is probably an internal event like
// a javascript reload. We should let it happen.
if (view?.url == request?.url.toString()) {
return false
}
request?.let {
state.content = onNewUrl(state.content, it.url.toString())
}
return true
}
}

View file

@ -1,9 +1,7 @@
package com.tangem.tap.features.tokens.impl.presentation.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
@ -38,6 +36,8 @@ import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
import com.tangem.wallet.R
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
@ -82,14 +82,14 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi
bottomMarginDp = floatingButtonHeight,
)
stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh)
Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") {
if (it) {
LoadingContent()
}
}
AnimatedVisibility(
visible = stateHolder.isLoading,
enter = fadeIn(),
exit = fadeOut(),
) {
LoadingContent()
LaunchedEffect(key1 = tokens.loadState.refresh) {
stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh)
}
}
}
@ -116,19 +116,19 @@ private fun TokensListContent(
) {
val state = rememberLazyListState()
if (state.isScrollInProgress) {
LocalSoftwareKeyboardController.current?.hide()
}
LazyColumn(
modifier = Modifier
.imePadding()
.fillMaxSize()
.padding(scaffoldPadding),
state = state,
contentPadding = PaddingValues(bottom = bottomMarginDp),
) {
if (isDifferentAddressesBlockVisible) {
item { DifferentAddressesWarning() }
item(
key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
contentType = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
) {
if (isDifferentAddressesBlockVisible) DifferentAddressesWarning()
}
tokens.itemKey(TokenItemState::composedId)
@ -138,6 +138,13 @@ private fun TokensListContent(
TokenItem(model = it)
}
}
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(key1 = state) {
snapshotFlow(state::isScrollInProgress)
.distinctUntilChanged()
.collectLatest { if (it) keyboardController?.hide() }
}
}
@Composable

View file

@ -1,15 +1,11 @@
package com.tangem.tap.features.tokens.impl.presentation.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.LoadState
import androidx.paging.PagingData
import androidx.paging.map
import androidx.lifecycle.*
import androidx.paging.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.getActiveIconRes
@ -41,8 +37,7 @@ import com.tangem.utils.coroutines.Debouncer
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import timber.log.Timber
@ -102,6 +97,8 @@ internal class TokensListViewModel @Inject constructor(
changedBlockchainList = currentCoins.toMutableList()
changedTokensList = currentTokens.toMutableList()
uiState = uiState.copySealed(tokens = getTokensListBySearchText(text = ""))
}
}
@ -154,14 +151,13 @@ internal class TokensListViewModel @Inject constructor(
)
}
private fun getInitialTokensList(searchText: String = ""): Flow<PagingData<TokenItemState>> {
if (searchText.isNotEmpty()) analyticsSender.sendWhenTokenSearched()
return interactor.getTokensList(searchText = searchText).map {
it.map { token ->
if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
}
}
private fun getInitialTokensList(): Flow<PagingData<TokenItemState>> {
return flowOf(
value = PagingData.empty(
sourceLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading),
mediatorLoadStates = LoadStates(LoadState.Loading, LoadState.Loading, LoadState.Loading),
),
)
}
private fun createManageTokenContent(token: Token): TokenItemState.ManageContent {
@ -181,9 +177,7 @@ internal class TokensListViewModel @Inject constructor(
return NetworkItemState.ManageContent(
name = network.blockchain.fullNameWithoutTestnet.uppercase(),
protocolName = getNetworkProtocolName(network),
iconResId = mutableStateOf(
getNetworkIconResId(network.address, network.blockchain),
),
iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)),
isMainNetwork = isMainNetwork(network),
isAdded = mutableStateOf(
isAdded(address = network.address, blockchain = network.blockchain),
@ -212,7 +206,7 @@ internal class TokensListViewModel @Inject constructor(
return NetworkItemState.ReadContent(
name = network.blockchain.fullNameWithoutTestnet.uppercase(),
protocolName = getNetworkProtocolName(network),
iconResId = mutableStateOf(getNetworkIconResId(network.address, network.blockchain)),
iconResId = mutableIntStateOf(value = getNetworkIconResId(network.address, network.blockchain)),
isMainNetwork = isMainNetwork(network),
)
}
@ -247,6 +241,20 @@ internal class TokensListViewModel @Inject constructor(
}
}
private fun getTokensListBySearchText(text: String): Flow<PagingData<TokenItemState>> {
return interactor.getTokensList(searchText = text)
.mapToTokenItemState()
.cachedIn(viewModelScope)
}
private fun Flow<PagingData<Token>>.mapToTokenItemState(): Flow<PagingData<TokenItemState>> {
return map { pagingData ->
pagingData.map { token ->
if (isManageAccess) createManageTokenContent(token) else createReadTokenContent(token)
}
}
}
private inner class ActionsHandler(
private val router: TokensListRouter,
private val debouncer: Debouncer,
@ -306,10 +314,15 @@ internal class TokensListViewModel @Inject constructor(
uiState = uiState.copySealed(toolbarState = state.copy(value = newValue))
debouncer.debounce(waitMs = 800L, coroutineScope = viewModelScope + dispatchers.io) {
uiState = uiState.copySealed(tokens = getInitialTokensList(newValue))
uiState = uiState.copySealed(tokens = getSearchedTokensList(newValue))
}
}
private fun getSearchedTokensList(searchText: String): Flow<PagingData<TokenItemState>> {
analyticsSender.sendWhenTokenSearched()
return getTokensListBySearchText(text = searchText)
}
private fun onCleanButtonClick() {
val state = requireNotNull(uiState.toolbarState as? TokensListToolbarState.InputField)

View file

@ -5,6 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
@ -15,6 +16,7 @@ import com.tangem.domain.common.configs.CardConfig
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
import com.tangem.domain.tokens.TokenWithBlockchain
import com.tangem.domain.tokens.TokensAction
import com.tangem.domain.tokens.model.CryptoCurrency
@ -37,6 +39,14 @@ import timber.log.Timber
@Suppress("LargeClass")
object TokensMiddleware {
// TODO: Move to DI
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
}
val tokensMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
@ -84,11 +94,9 @@ object TokensMiddleware {
updatedScanResponse = it,
currencyList = currencyList,
)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
} else {
submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
}
}
@ -262,7 +270,6 @@ object TokensMiddleware {
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
@ -372,23 +379,15 @@ object TokensMiddleware {
updatedScanResponse: ScanResponse,
currencyList: List<CryptoCurrency>,
) {
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val networksRepository = store.state.daggerGraphState.get(DaggerGraphState::networksRepository)
scope.launch {
userWalletsListManager.update(
userWalletId = userWalletId,
update = { it.copy(scanResponse = updatedScanResponse) },
)
currenciesRepository.addCurrencies(userWalletId = userWalletId, currencies = currencyList)
networksRepository.getNetworkStatusesSync(
userWalletId = userWalletId,
networks = currencyList.map(CryptoCurrency::network).toSet(),
refresh = true,
)
).doOnSuccess {
addCryptoCurrenciesUseCase(userWalletId, currencyList)
}
}
store.dispatchOnMain(NavigationAction.PopBackTo())
}
private suspend fun removeLegacyCurrenciesIfNeeded(currencies: List<Currency>) {
@ -403,7 +402,19 @@ object TokensMiddleware {
private suspend fun removeNewCurrenciesIfNeeded(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
if (currencies.isEmpty()) return
val currenciesRepository = store.state.daggerGraphState.get(DaggerGraphState::currenciesRepository)
val walletManagersFacade = store.state.daggerGraphState.get(DaggerGraphState::walletManagersFacade)
currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = currencies)
walletManagersFacade.remove(
userWalletId = userWalletId,
networks = currencies
.filterIsInstance<CryptoCurrency.Coin>()
.mapTo(hashSetOf(), CryptoCurrency::network),
)
walletManagersFacade.removeTokens(
userWalletId = userWalletId,
tokens = currencies.filterIsInstance<CryptoCurrency.Token>().toSet(),
)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.wallet.R
import kotlinx.coroutines.CoroutineScope
import org.rekotlin.Action
sealed class WalletAction : Action {
@ -76,7 +77,7 @@ sealed class WalletAction : Action {
data class Scan(
val onScanSuccessEvent: AnalyticsEvent?,
val lifecycleScope: LifecycleCoroutineScope,
val scope: CoroutineScope,
) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction()
@ -113,7 +114,7 @@ sealed class WalletAction : Action {
data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction()
object CreateWallet : WalletAction()
data class ChangeWallet(val lifecycleScope: LifecycleCoroutineScope) : WalletAction()
data class ChangeWallet(val scope: LifecycleCoroutineScope) : WalletAction()
object ShowSaveWalletIfNeeded : WalletAction()
sealed class TradeCryptoAction : WalletAction() {
@ -121,14 +122,6 @@ sealed class WalletAction : Action {
data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction()
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
data class SendCrypto(
val currencyId: String,
val amount: String,
val destinationAddress: String,
val transactionId: String,
) : TradeCryptoAction()
object Swap : TradeCryptoAction()
}

View file

@ -74,6 +74,7 @@ class TradeCryptoMiddleware {
}
}
@Deprecated("Use proceedNewBuyAction instead")
private fun proceedBuyAction(state: () -> AppState?, action: TradeCryptoAction.Buy) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = chooseAppropriateCurrency(store.state.walletState) ?: return
@ -331,8 +332,7 @@ class TradeCryptoMiddleware {
}
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
val cryptoStatus = action.tokenStatus
val currency = cryptoStatus.currency
val currency = action.tokenCurrency
val blockchain = Blockchain.fromId(currency.network.id.value)
scope.launch {
@ -351,21 +351,21 @@ class TradeCryptoMiddleware {
return@launch
}
val sendableAmounts = walletManager.wallet.amounts.values.filter { it.type is AmountType.Token }
when (currency) {
is CryptoCurrency.Coin -> error("Action.tokenStatus.currency is Coin")
is CryptoCurrency.Token -> {
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmounts.first(),
tokenRate = cryptoStatus.value.fiatRate,
),
)
}
val sendableAmount = walletManager.wallet.amounts.values.firstOrNull {
val amountType = it.type
amountType is AmountType.Token && amountType.token.contractAddress == currency.contractAddress
}
store.dispatchOnMain(
action = PrepareSendScreen(
walletManager = walletManager,
coinAmount = walletManager.wallet.amounts[AmountType.Coin],
coinRate = action.coinFiatRate,
tokenAmount = sendableAmount,
tokenRate = action.tokenFiatRate,
),
)
val bundle = bundleOf(
SendRouter.CRYPTO_CURRENCY_KEY to currency,
SendRouter.USER_WALLET_ID_KEY to action.userWallet.walletId.stringValue,

View file

@ -116,9 +116,9 @@ class WalletMiddleware {
}
is WalletAction.Scan -> {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
action.lifecycleScope.launch {
action.scope.launch {
delay(timeMillis = 700)
store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent, action.lifecycleScope))
store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent, action.scope))
}
}
is WalletAction.LoadData,
@ -180,7 +180,7 @@ class WalletMiddleware {
}
if (walletState.isMultiwalletAllowed) {
val amountToSend = sendableAmounts.find { it.currencySymbol == currency.currencySymbol }
val amountToSend = findAmountToSend(currency = currency, amounts = sendableAmounts)
if (amountToSend == null) {
val error = TapError.UnsupportedState("WalletAction.Send: Amount to send is null")
FirebaseCrashlytics.getInstance().recordException(IllegalStateException(error.stateError))
@ -227,7 +227,7 @@ class WalletMiddleware {
showSaveWalletIfNeeded()
}
is WalletAction.ChangeWallet -> {
changeWallet(walletState, action.lifecycleScope)
changeWallet(walletState, action.scope)
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
@ -277,6 +277,18 @@ class WalletMiddleware {
}
}
private fun findAmountToSend(currency: Currency, amounts: List<Amount>): Amount? {
return amounts.find { amount ->
val amountType = amount.type
if (amountType is AmountType.Token && currency is Currency.Token) {
val token = amountType.token
token.symbol == currency.currencySymbol && token.contractAddress == currency.token.contractAddress
} else {
amount.currencySymbol == currency.currencySymbol
}
}
}
private fun makeInitSendStateActionByAmount(
amount: Amount,
walletStore: WalletStoreModel,
@ -391,7 +403,7 @@ class WalletMiddleware {
store.dispatch(
WalletAction.Scan(
onScanSuccessEvent = Basic.CardWasScanned(AnalyticsParam.ScannedFrom.Main),
lifecycleScope = lifecycleScope,
scope = lifecycleScope,
),
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.wallet.redux.models
import com.tangem.blockchain.common.Amount
import com.tangem.core.navigation.StateDialog
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.wallet.R
sealed interface WalletDialog : StateDialog {

View file

@ -133,9 +133,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber<W
binding.toolbar.setNavigationOnClickListener(
OneTouchClickListener {
lifecycleScope.launch {
store.dispatch(WalletAction.ChangeWallet(lifecycleScope))
}
store.dispatch(WalletAction.ChangeWallet(scope = requireActivity().lifecycleScope))
},
)
setupWarningsRecyclerView()

View file

@ -39,6 +39,7 @@ class RussianCardholdersWarningBottomSheetDialog(
binding?.btnYes?.setOnClickListener {
if (dialogData != null) {
store.dispatchOpenUrl(dialogData.topUpUrl)
Analytics.send(Token.Topup.ScreenOpened())
} else {
store.dispatch(TradeCryptoAction.Buy(checkUserLocation = false))
}

View file

@ -192,7 +192,9 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
isPermanent = error.isPermanent,
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
is UserWalletsListError.AllKeysInvalidated,
is UserWalletsListError.NoUserWalletSelected,
-> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(

View file

@ -92,7 +92,9 @@ internal class WelcomeViewModel @Inject constructor(
isPermanent = error.isPermanent,
onDismiss = this::dismissWarning,
)
is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
is UserWalletsListError.AllKeysInvalidated,
is UserWalletsListError.NoUserWalletSelected,
-> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarning,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
@ -89,7 +90,7 @@ class CurrencyExchangeManager(
}
suspend fun buyErc20TestnetTokens(card: CardDTO, walletManager: EthereumWalletManager, destinationAddress: String) {
walletManager.safeUpdate()
walletManager.safeUpdate(card.isDemoCard())
val amountToSend = Amount(walletManager.wallet.blockchain)

Some files were not shown because too many files have changed in this diff Show more