Updated on 2026-08-14

This commit is contained in:
Tangem 2023-04-04 15:31:21 +03:00
commit 8cf59cdcbf
239 changed files with 4687 additions and 2357 deletions

1
.gitignore vendored
View file

@ -32,4 +32,3 @@ local.properties
**/fastlane/Preview.html **/fastlane/Preview.html
**/fastlane/screenshots **/fastlane/screenshots
**/fastlane/test_output **/fastlane/test_output
/buildSrc/build/

View file

@ -1,117 +1,20 @@
plugins { plugins {
id("com.android.application") alias(deps.plugins.android.application)
kotlin("android") alias(deps.plugins.kotlin.android)
kotlin("kapt") alias(deps.plugins.kotlin.kapt)
kotlin("plugin.serialization") alias(deps.plugins.kotlin.serialization)
id("com.google.gms.google-services") alias(deps.plugins.google.services)
id("com.google.firebase.crashlytics") alias(deps.plugins.hilt.android)
id("com.google.dagger.hilt.android") alias(deps.plugins.firebase.crashlytics)
} id("configuration")
android {
compileSdk = AppConfig.compileSdkVersion
defaultConfig {
applicationId = AppConfig.packageName
minSdk = AppConfig.minSdkVersion
targetSdk = AppConfig.targetSdkVersion
versionCode = if (project.hasProperty("versionCode")) {
(project.property("versionCode") as String).toInt()
} else {
AppConfig.versionCode
}
versionName = if (project.hasProperty("versionName")) {
project.property("versionName") as String
} else {
AppConfig.versionName
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
compose = true
viewBinding = true
}
buildTypes {
release {
isDebuggable = false
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
BuildConfigFieldFactory(
fields = listOf(
Field.Environment("prod"),
Field.TestActionEnabled(false),
Field.LogEnabled(false),
),
builder = ::buildConfigField,
).create()
}
debug {
isDebuggable = true
isMinifyEnabled = false
applicationIdSuffix = ".dev"
configure<com.google.firebase.crashlytics.buildtools.gradle.CrashlyticsExtension> {
// disable mapping file uploads (default=true if minifying)
mappingFileUploadEnabled = false
}
BuildConfigFieldFactory(
fields = listOf(
Field.Environment("dev"),
Field.TestActionEnabled(true),
Field.LogEnabled(true),
),
builder = ::buildConfigField,
).create()
}
create("debug_beta") {
initWith(getByName("release"))
versionNameSuffix = "-beta"
applicationIdSuffix = ".debug"
signingConfig = signingConfigs.getByName("debug")
}
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = false
}
composeOptions {
kotlinCompilerExtensionVersion = Versions.compose
}
packagingOptions {
resources.excludes += "lib/x86_64/darwin/libscrypt.dylib"
resources.excludes += "lib/x86_64/freebsd/libscrypt.so"
resources.excludes += "lib/x86_64/linux/libscrypt.so"
resources.excludes += "META-INF/gradle/incremental.annotation.processors"
}
}
repositories {
mavenCentral()
maven(url = "https://jitpack.io")
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(project(":domain")) implementation(project(":domain"))
implementation(project(":common")) implementation(project(":common"))
implementation(project(":core:analytics")) implementation(project(":core:analytics"))
implementation(project(":core:featuretoggles"))
implementation(project(":core:res")) implementation(project(":core:res"))
implementation(project(":core:ui")) implementation(project(":core:ui"))
implementation(project(":core:datasource")) implementation(project(":core:datasource"))
@ -123,91 +26,93 @@ dependencies {
implementation(project(":features:referral:presentation")) implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain")) implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data")) implementation(project(":features:referral:data"))
implementation(project(":features:swap:api"))
implementation(project(":features:swap:presentation")) implementation(project(":features:swap:presentation"))
implementation(project(":features:swap:domain")) implementation(project(":features:swap:domain"))
implementation(project(":features:swap:data")) implementation(project(":features:swap:data"))
implementation(project(":features:tester:api"))
implementation(project(":features:tester:impl"))
/** AndroidX libraries */ /** AndroidX libraries */
implementation(AndroidX.coreKtx) implementation(deps.androidx.core.ktx)
implementation(AndroidX.appCompat) implementation(deps.androidx.appCompat)
implementation(AndroidX.fragmentKtx) implementation(deps.androidx.fragment.ktx)
implementation(AndroidX.constraintLayout) implementation(deps.androidx.constraintLayout)
implementation(AndroidX.browser) implementation(deps.androidx.activity.compose)
implementation(AndroidX.lifecycleRuntimeKtx) implementation(deps.androidx.browser)
implementation(AndroidX.lifecycleCommonJava8) implementation(deps.lifecycle.runtime.ktx)
implementation(AndroidX.lifecycleViewModelKtx) implementation(deps.lifecycle.common.java8)
implementation(AndroidX.lifecycleLiveDataKtx) implementation(deps.lifecycle.viewModel.ktx)
implementation(AndroidX.activityCompose)
/** Compose libraries */ /** Compose libraries */
implementation(Compose.material) implementation(deps.compose.material)
implementation(Compose.animation) implementation(deps.compose.animation)
implementation(Compose.foundation) implementation(deps.compose.foundation)
implementation(Compose.ui) implementation(deps.compose.ui)
implementation(Compose.uiTooling) implementation(deps.compose.ui.tooling)
implementation(Compose.coil) implementation(deps.compose.coil)
implementation(deps.compose.shimmer)
/** Firebase libraries */ /** Firebase libraries */
implementation(platform(Firebase.bom)) implementation(platform(deps.firebase.bom))
implementation(Firebase.firebaseAnalytics) implementation(deps.firebase.analytics)
implementation(Firebase.firebaseCrashlytics) implementation(deps.firebase.crashlytics)
/** Tangem libraries */ /** Tangem libraries */
implementation(Tangem.blockchain) { implementation(deps.tangem.blockchain) {
exclude(module = "joda-time") exclude(module = "joda-time")
} }
implementation(Tangem.cardCore) implementation(deps.tangem.card.core)
implementation(Tangem.cardAndroid) { implementation(deps.tangem.card.android) {
exclude(module = "joda-time") exclude(module = "joda-time")
} }
/** DI */ /** DI */
implementation(Library.hilt) implementation(deps.hilt.android)
kapt(Library.hiltKapt) kapt(deps.hilt.kapt)
/** Other libraries */ /** Other libraries */
implementation(Library.materialComponent) implementation(deps.material)
implementation(Library.googlePlayCore) implementation(deps.googlePlay.core)
implementation(Library.googlePlayCoreKtx) implementation(deps.googlePlay.core.ktx)
coreLibraryDesugaring(Library.desugarJdkLibs) implementation(deps.googlePlay.services.wallet)
implementation(Library.timber) coreLibraryDesugaring(deps.desugar)
implementation(Library.reKotlin) implementation(deps.timber)
implementation(Library.zxingQrCore) implementation(deps.reKotlin)
implementation(Library.zxingQrBarcodeScanner) implementation(deps.zxing.qrCore)
implementation(Library.otaliastudiosCameraView) implementation(deps.zxing.qrBarcodeScanner)
implementation(Library.coil) implementation(deps.otaliastudiosCameraView)
implementation(Library.appsflyer) implementation(deps.coil)
implementation(Library.amplitude) implementation(deps.appsflyer)
implementation(Library.kotsonGsonExt) implementation(deps.amplitude)
implementation(deps.kotsonGson)
//TODO: refactoring: remove it when all network services moved to the datasource module //TODO: refactoring: remove it when all network services moved to the datasource module
implementation(Library.retrofit) implementation(deps.retrofit)
implementation(Library.retrofitMoshiConverter) implementation(deps.retrofit.moshi)
implementation(Library.moshi) implementation(deps.moshi)
implementation(Library.moshiKotlin) implementation(deps.moshi.kotlin)
implementation(Library.okHttp) implementation(deps.okHttp)
implementation(Library.okHttpLogging) implementation(deps.okHttp.logging)
implementation(Library.zendeskChat) implementation(deps.zendesk.chat)
implementation(Library.zendeskMessaging) implementation(deps.zendesk.messaging)
implementation(Library.spongecastleCryptoCore) implementation(deps.spongecastle.core)
implementation(Library.lottie) implementation(deps.lottie)
implementation(Library.shopifyBuySdk) { implementation(deps.shopify.buy) {
exclude(group = "com.shopify.graphql.support") exclude(group = "com.shopify.graphql.support")
exclude(module = "joda-time") exclude(module = "joda-time")
} }
implementation(Library.accompanistAppCompatTheme) implementation(deps.compose.accompanist.appCompatTheme)
implementation(Library.accompanistSystemUiController) implementation(deps.compose.accompanist.systemUiController)
implementation(Library.accompanistWebView) implementation(deps.compose.accompanist.webView)
implementation(Library.xmlShimmer) implementation(deps.xmlShimmer)
implementation(Library.viewBindingDelegate) implementation(deps.viewBindingDelegate)
implementation(Library.armadillo) implementation(deps.armadillo)
implementation(Library.googlePlayServicesWallet) implementation(deps.mviCore.watcher)
implementation(Library.composeShimmer) implementation(deps.kotlin.serialization)
implementation(Library.mviCoreWatcher)
implementation(Library.kotlinSerialization)
/** Testing libraries */ /** Testing libraries */
testImplementation(Test.junit) testImplementation(deps.test.junit)
testImplementation(Test.truth) testImplementation(deps.test.truth)
androidTestImplementation(Test.junitAndroidExt) androidTestImplementation(deps.test.junit.android)
androidTestImplementation(Test.espresso) androidTestImplementation(deps.test.espresso)
} }

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
package="com.tangem.wallet">
<application
tools:ignore="GoogleAppIndexingWarning">
<!-- <provider-->
<!-- android:name="com.tangem.data.LogFileProvider"-->
<!-- android:authorities="@string/log_file_provider_authorities"-->
<!-- android:enabled="true"-->
<!-- android:exported="false"-->
<!-- android:grantUriPermissions="true" />-->
</application>
</manifest>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name">DebugTangem</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="tangem_app_name" translatable="false">DebugTangem</string> <string name="tangem_app_name" translatable="false">Debug Tangem</string>
</resources> </resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

View file

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:tools="http://schemas.android.com/tools"
package="com.tangem.wallet">
<application
tools:ignore="GoogleAppIndexingWarning">
<!-- <provider-->
<!-- android:name="com.tangem.data.LogFileProvider"-->
<!-- android:authorities="@string/log_file_provider_authorities"-->
<!-- android:enabled="true"-->
<!-- android:exported="false"-->
<!-- android:grantUriPermissions="true" />-->
</application>
</manifest>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name">BetaTangem</string>
</resources>

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name">BetaTangem</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">External Tangem</string>
</resources>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">Internal Tangem</string>
</resources>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>

@ -1 +1 @@
Subproject commit f5f6ac954894ef66331db8fe383b9773feb69ef4 Subproject commit c714678d9c46dee1b15ef9e8d27aa23afa1dfd8d

View file

@ -148,6 +148,17 @@
} }
] ]
}, },
{
"id": "the-open-network",
"symbol": "TON",
"name": "Toncoin",
"networks":
[
{
"networkId": "the-open-network/test"
}
]
},
{ {
"id" : "Tangem Coin A-TCA", "id" : "Tangem Coin A-TCA",
"symbol" : "TCA", "symbol" : "TCA",

View file

@ -10,9 +10,10 @@ import androidx.core.view.WindowInsetsControllerCompat
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk import com.tangem.TangemSdk
import com.tangem.features.tester.api.TesterRouter
import com.tangem.operations.backup.BackupService import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init import com.tangem.sdk.extensions.init
import com.tangem.tangem_sdk_new.extensions.initWithBiometrics import com.tangem.sdk.extensions.initWithBiometrics
import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.OnActivityResultCallback
@ -33,6 +34,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding import com.tangem.wallet.databinding.ActivityMainBinding
@ -71,6 +73,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject @Inject
lateinit var appStateHolder: AppStateHolder lateinit var appStateHolder: AppStateHolder
/** Router for opening tester menu */
@Inject
lateinit var testerRouter: TesterRouter
private var snackbar: Snackbar? = null private var snackbar: Snackbar? = null
private val dialogManager = DialogManager() private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -97,6 +103,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
GooglePayService(createPaymentsClient(this), this), GooglePayService(createPaymentsClient(this), this),
), ),
) )
store.dispatch(DaggerGraphAction.SetActivityDependencies(testerRouter))
} }
private fun initUserWalletsListManager() { private fun initUserWalletsListManager() {

View file

@ -11,12 +11,13 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config import com.tangem.datasource.config.models.Config
import com.tangem.datasource.utils.AndroidAssetReader import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.utils.AssetReader
import com.tangem.domain.DomainLayer import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig import com.tangem.domain.common.LogConfig
import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.IntentHandler
@ -48,11 +49,12 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.wallet.BuildConfig import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.launch
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store import org.rekotlin.Store
import timber.log.Timber import timber.log.Timber
@ -65,7 +67,6 @@ lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository lateinit var userTokensRepository: UserTokensRepository
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() } private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
@ -116,6 +117,15 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject @Inject
lateinit var configManager: ConfigManager lateinit var configManager: ConfigManager
@Inject
lateinit var assetReader: AssetReader
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -136,15 +146,15 @@ class TapApplication : Application(), ImageLoaderFactory {
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks) registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
DomainLayer.init() DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this) preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this) walletConnectRepository = WalletConnectRepository(this)
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT) val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
initConfigManager(configLoader, ::initWithConfigDependency) initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager() initWarningMessagesManager()
loadNativeLibraries()
if (LogConfig.network.blockchainSdkNetwork) { if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf( BlockchainSdkRetrofitBuilder.interceptors = listOf(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY }, HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY },
@ -154,10 +164,23 @@ class TapApplication : Application(), ImageLoaderFactory {
userTokensRepository = UserTokensRepository.init( userTokensRepository = UserTokensRepository.init(
context = this, context = this,
tangemTechService = store.state.domainNetworks.tangemTechService, tangemTechService = store.state.domainNetworks.tangemTechService,
networkConnectionManager = networkConnectionManager,
) )
appStateHolder.mainStore = store appStateHolder.mainStore = store
appStateHolder.userTokensRepository = userTokensRepository appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager appStateHolder.walletStoresManager = walletStoresManager
store.dispatch(
action = DaggerGraphAction.SetApplicationDependencies(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
),
)
scope.launch {
featureTogglesManager.init()
}
initTopUpController() initTopUpController()
} }
@ -180,6 +203,10 @@ class TapApplication : Application(), ImageLoaderFactory {
) )
} }
private fun loadNativeLibraries() {
System.loadLibrary("TrustWalletCore")
}
private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) { private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
configManager.load(loader) { config -> configManager.load(loader) { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager)) store.dispatch(GlobalAction.SetConfigManager(configManager))
@ -217,6 +244,7 @@ class TapApplication : Application(), ImageLoaderFactory {
) { ) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply { fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try { appVersion = try {
// TODO don't use deprecated method
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0) val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {

View file

@ -119,6 +119,8 @@ object TangemSdkErrorMapper {
is TangemSdkError.UserCanceledBiometricsAuthentication -> error is TangemSdkError.UserCanceledBiometricsAuthentication -> error
is TangemSdkError.EncryptionOperationFailed -> error is TangemSdkError.EncryptionOperationFailed -> error
is TangemSdkError.InvalidEncryptionKey -> error is TangemSdkError.InvalidEncryptionKey -> error
is TangemSdkError.KeyGenerationException -> error
is TangemSdkError.MnemonicException -> error
} }
} }
} }

View file

@ -24,6 +24,8 @@ sealed class AnalyticsParam {
sealed class CardBalanceState(val value: String) { sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty") object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full") object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
companion object companion object
} }
@ -79,6 +81,7 @@ sealed class AnalyticsParam {
companion object Key { companion object Key {
const val Source = "Source" const val Source = "Source"
const val Balance = "Balance"
const val Batch = "Batch" const val Batch = "Batch"
const val ProductType = "Product Type" const val ProductType = "Product Type"
const val Firmware = "Firmware" const val Firmware = "Firmware"

View file

@ -11,6 +11,13 @@ sealed class Basic(
error: Throwable? = null, error: Throwable? = null,
) : AnalyticsEvent("Basic", event, params, error) { ) : AnalyticsEvent("Basic", event, params, error) {
class BalanceLoaded(balance: AnalyticsParam.CardBalanceState) : Basic(
event = "Balance Loaded",
params = mapOf(
AnalyticsParam.Balance to balance.value,
),
)
class CardWasScanned( class CardWasScanned(
source: AnalyticsParam.ScannedFrom, source: AnalyticsParam.ScannedFrom,
) : Basic( ) : Basic(
@ -51,4 +58,6 @@ sealed class Basic(
event = "Scan", event = "Scan",
error = error, error = error,
) )
class WalletOpened : Basic(event = "Wallet Opened")
} }

View file

@ -11,7 +11,6 @@ sealed class MainScreen(
) : AnalyticsEvent("Main Screen", event, params) { ) : AnalyticsEvent("Main Screen", event, params) {
class ScreenOpened : MainScreen("Screen opened") class ScreenOpened : MainScreen("Screen opened")
class ButtonScanCard : MainScreen("Button - Scan Card") class ButtonScanCard : MainScreen("Button - Scan Card")
class ButtonMyWallets : MainScreen("Button - My Wallets") class ButtonMyWallets : MainScreen("Button - My Wallets")
class ButtonBuy : MainScreen("Button - Buy") class ButtonBuy : MainScreen("Button - Buy")

View file

@ -71,16 +71,16 @@ sealed class Onboarding(
class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code") class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code")
class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened") class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened")
class ButtonConnect : Onboarding("Onboarding", "Button - Connect") class ButtonConnect : Onboarding("Onboarding", "Button - Connect")
class PinCodeSet : Onboarding("Onboarding", "PIN code set") class PinCodeSet : Onboarding("Onboarding", "PIN Code Set")
class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened") class KYCScreenOpened : Onboarding("Onboarding", "KYC screen opened")
class KYCStarted : Onboarding("Onboarding", "KYC started") class KYCStarted : Onboarding("Onboarding", "KYC Started")
class KYCInProgress : Onboarding("Onboarding", "KYC in progress") class KYCInProgress : Onboarding("Onboarding", "KYC In Progress")
class KYCRejected : Onboarding("Onboarding", "KYC rejected") class KYCRejected : Onboarding("Onboarding", "KYC Rejected")
class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened") class ClaimScreenOpened : Onboarding("Onboarding", "Claim Screen Opened")
class ButtonClaim : Onboarding("Onboarding", "Button - Claim") class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully") class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully")
class ButtonChat : Onboarding("Onboarding", "Button - Chat") class ButtonChat : Onboarding("Onboarding", "Button - Chat")

View file

@ -60,16 +60,16 @@ sealed class Settings(
params: Map<String, String> = mapOf(), params: Map<String, String> = mapOf(),
) : Settings(category = "Settings / App Settings", event = event, params = params) { ) : Settings(category = "Settings / App Settings", event = event, params = params) {
class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( class SaveWalletSwitcherChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Save Wallet Switcher Changed", event = "Save Wallet Switcher Changed",
params = mapOf("State" to state.value), params = mapOf("State" to state.value),
) )
class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings( class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Save Access Code Switcher Changed", event = "Save Access Code Switcher Changed",
params = mapOf("State" to state.value), params = mapOf("State" to state.value),
) )
object ButtonEnableBiometricAuthentication : AppSettings("Button - Enable Biometric Authentication") object ButtonEnableBiometricAuthentication : AppSettings(event = "Button - Enable Biometric Authentication")
} }
} }

View file

@ -48,7 +48,7 @@ sealed class Token(
class Bought(type: CurrencyType) : Token( class Bought(type: CurrencyType) : Token(
category = "Token", category = "Token",
event = "Token bought", event = "Token Bought",
params = mapOf("Token" to type.value), params = mapOf("Token" to type.value),
) )

View file

@ -5,8 +5,8 @@ import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.sdk.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp import com.tangem.sdk.extensions.pxToDp
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]

View file

@ -36,6 +36,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Kusama -> R.drawable.ic_kusama_no_color Blockchain.Kusama -> R.drawable.ic_kusama_no_color
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color
Blockchain.Dash -> R.drawable.ic_dash_no_color Blockchain.Dash -> R.drawable.ic_dash_no_color
Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color
else -> R.drawable.ic_tangem_logo else -> R.drawable.ic_tangem_logo
} }
} }

View file

@ -26,7 +26,7 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment import com.tangem.tap.features.tokens.addCustomToken.AddCustomTokenFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.tokens.presentation.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment

View file

@ -42,8 +42,12 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId)) dispatchOnMain(GlobalAction.ShowNotification(resId))
} }
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) { suspend fun Store<AppState>.onUserWalletSelected(
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh) userWallet: UserWallet,
refresh: Boolean = false,
sendAnalyticsEvent: Boolean = false,
) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh, sendAnalyticsEvent)
} }
fun Store<*>.dispatchToastNotification(resId: Int) { fun Store<*>.dispatchToastNotification(resId: Int) {

View file

@ -16,8 +16,8 @@ import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.AddressData import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store import com.tangem.tap.store
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import timber.log.Timber import timber.log.Timber
@ -47,7 +47,8 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
} catch (exception: Exception) { } catch (exception: Exception) {
Timber.e(exception) Timber.e(exception)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
Result.Failure(TapError.NoInternetConnection) Result.Failure(TapError.NoInternetConnection)
} else { } else {
val blockchain = wallet.blockchain val blockchain = wallet.blockchain
@ -87,8 +88,6 @@ fun WalletManager?.getAddressData(): AddressData? {
return if (addressDataList.isEmpty()) null else addressDataList[0] return if (addressDataList.isEmpty()) null else addressDataList[0]
} }
fun WalletManager.getTxHistory(currency: Currency): List<TransactionData> = wallet.getTxHistory(currency)
fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory() fun WalletManager.getBlockchainTxHistory(): List<TransactionData> = wallet.getBlockchainTxHistory()
fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token) fun WalletManager.getTokenTxHistory(token: Token): List<TransactionData> = wallet.getTokenTxHistory(token)

View file

@ -6,8 +6,8 @@ import android.widget.FrameLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.tap.domain.twins.TwinsCardWidget import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding

View file

@ -3,7 +3,7 @@ package com.tangem.tap.common.recyclerView
import android.graphics.Rect import android.graphics.Rect
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.sdk.extensions.dpToPx
class SpaceItemDecoration( class SpaceItemDecoration(
private val horizontalSpaceDp: Float, private val horizontalSpaceDp: Float,

View file

@ -20,6 +20,7 @@ import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer import com.tangem.tap.features.walletSelector.redux.WalletSelectorReducer
import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState { fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder): AppState {
@ -46,6 +47,7 @@ fun appReducer(action: Action, state: AppState?, appStateHolder: AppStateHolder)
walletSelectorState = WalletSelectorReducer.reduce(action, state), walletSelectorState = WalletSelectorReducer.reduce(action, state),
sprinklrState = SprinklrReducer.reduce(action, state), sprinklrState = SprinklrReducer.reduce(action, state),
signInState = SignInReducer.reduce(action, state), signInState = SignInReducer.reduce(action, state),
daggerGraphState = DaggerGraphReducer.reduce(action, state),
) )
} }

View file

@ -43,6 +43,8 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorMiddleware
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
import com.tangem.tap.features.welcome.redux.WelcomeState import com.tangem.tap.features.welcome.redux.WelcomeState
import com.tangem.tap.proxy.redux.DaggerGraphMiddleware
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import org.rekotlin.Middleware import org.rekotlin.Middleware
import org.rekotlin.StateType import org.rekotlin.StateType
@ -66,7 +68,8 @@ data class AppState(
val saveWalletState: SaveWalletState = SaveWalletState(), val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(), val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(), val sprinklrState: SprinklrState = SprinklrState(),
val signInState: SignInState = SignInState() val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType { ) : StateType {
val domainState: DomainState val domainState: DomainState
@ -108,7 +111,8 @@ data class AppState(
LockUserWalletsTimerMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware,
SprinklrMiddleware().middleware, SprinklrMiddleware().middleware,
SignInMiddleware.middleware SignInMiddleware.middleware,
DaggerGraphMiddleware.daggerGraphMiddleware,
) )
} }
} }

View file

@ -12,6 +12,7 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction
@ -36,8 +37,11 @@ class TapWalletManager {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
} }
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean) { suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
Analytics.setContext(userWallet.scanResponse) Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
}
val scanResponse = userWallet.scanResponse val scanResponse = userWallet.scanResponse
val card = scanResponse.card val card = scanResponse.card
val attestationFailed = card.attestation.status == Attestation.Status.Failed val attestationFailed = card.attestation.status == Attestation.Status.Failed

View file

@ -6,8 +6,8 @@ import com.tangem.common.services.Result
import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.datasource.utils.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -60,7 +60,7 @@ class LoadAvailableCoinsService(
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
runCatching { runCatching {
tangemTechApi.getCoins( tangemTechApi.getCoins(
networkIds = supportedBlockchains.toSet().map(Blockchain::toNetworkId).joinToString(","), networkIds = supportedBlockchains.toSet().joinToString(",", transform = Blockchain::toNetworkId),
active = true, active = true,
searchText = searchInput, searchText = searchInput,
offset = offset, offset = offset,
@ -74,8 +74,8 @@ class LoadAvailableCoinsService(
} }
} }
fun getTestnetCoins(): List<Currency> { private fun getTestnetCoins(): List<Currency> {
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS) val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins return currenciesAdapter.fromJson(json)!!.coins
.map { Currency.fromJsonObject(it) } .map { Currency.fromJsonObject(it) }
} }

View file

@ -1,138 +0,0 @@
package com.tangem.tap.domain.tokens
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.common.FileReader
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.tokens.models.TokenDao
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
@Deprecated("Use this only for migration")
class OldUserTokensRepository(
private val fileReader: FileReader,
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) {
private val moshi = MoshiConverter.networkMoshi
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java),
)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java),
)
private val blockchainNetworkAdapter: JsonAdapter<List<BlockchainNetwork>> =
moshi.adapter(Types.newParameterizedType(List::class.java, BlockchainNetwork::class.java))
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedTokens(cardId: String): List<TokenDao> {
val json = try {
fileReader.readFile(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptyList()
}
return try {
tokensAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use BlockchainNetwork instead")
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
emptyList()
}
}
@Deprecated("Use TokensRepository instead")
suspend fun loadSavedCurrencies(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
val json = fileReader.readFile(getFileNameForBlockchains(cardId))
blockchainNetworkAdapter.fromJson(json)?.distinct() ?: emptyList()
} catch (exception: Exception) {
tryToLoadPreviousFormatAndMigrate(cardId, isHdWalletSupported)
}
}
private suspend fun tryToLoadPreviousFormatAndMigrate(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
return try {
loadSavedCurrenciesOldWay(
cardId,
isHdWalletSupported,
)
} catch (exception: Exception) {
emptyList()
}
}
private suspend fun loadSavedCurrenciesOldWay(
cardId: String,
isHdWalletSupported: Boolean = false,
): List<BlockchainNetwork> {
val blockchains = loadSavedBlockchains(cardId)
val tokens = loadSavedTokens(cardId)
val ids = getTokensIds(tokens)
val derivationStyle = if (isHdWalletSupported) DerivationStyle.LEGACY else null
val blockchainNetworks = blockchains.map { blockchain ->
BlockchainNetwork(
blockchain = blockchain,
derivationPath = blockchain.derivationPath(derivationStyle)?.rawPath,
tokens = tokens
.filter { it.blockchainDao.toBlockchain() == blockchain }
.map {
val token = it.toToken()
token.copy(id = ids[token.contractAddress])
},
)
}
return blockchainNetworks
}
private suspend fun getTokensIds(tokens: List<TokenDao>): Map<String, String> = withContext(dispatchers.io) {
tokens.map {
async {
tangemTechApi.getCoins(
contractAddress = it.contractAddress,
networkIds = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}
.map {
runCatching { it.await() }
.onSuccess { return@map it.coins.firstOrNull()?.id }
.onFailure { return@map null }
error("Unreachable code because runCatching must return result")
}
.mapIndexedNotNull { index, id ->
if (id == null) null else tokens[index].contractAddress to id
}
.toMap()
}
companion object {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
private fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.common.CardDTO import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
@ -15,7 +16,6 @@ import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toBlockchainNetworks import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.models.toCurrencies import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -25,33 +25,31 @@ class UserTokensRepository(
private val storageService: UserTokensStorageService, private val storageService: UserTokensStorageService,
private val tangemTechApi: TangemTechApi, private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
private val networkConnectionManager: NetworkConnectionManager,
) { ) {
// TODO("After adding DI") replace with CoroutineDispatcherProvider
suspend fun getUserTokens(card: CardDTO): List<Currency> = withContext(dispatchers.io) { suspend fun getUserTokens(card: CardDTO): List<Currency> = withContext(dispatchers.io) {
val userId = getUserWalletId(card) ?: return@withContext emptyList() val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) { if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies) return@withContext loadTokensOffline(userWalletId = userWalletId).ifEmpty(::loadDemoCurrencies)
} }
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { if (!networkConnectionManager.isOnline) return@withContext loadTokensOffline(userWalletId = userWalletId)
return@withContext loadTokensOffline(card, userId)
}
return@withContext remoteGetUserTokens(card, userId) return@withContext remoteGetUserTokens(userWalletId = userWalletId)
} }
// TODO("After adding DI") replace with CoroutineDispatcherProvider
suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) { suspend fun saveUserTokens(card: CardDTO, tokens: List<Currency>) = withContext(dispatchers.io) {
val userId = getUserWalletId(card) ?: return@withContext val userWalletId = getUserWalletId(card) ?: return@withContext
val userTokens = tokens.toUserTokensResponse() val userTokens = tokens.toUserTokensResponse()
remoteSaveUserTokens(userId, userTokens) remoteSaveUserTokens(userWalletId = userWalletId, userTokens = userTokens)
storageService.saveUserTokens(userId, userTokens) storageService.saveUserTokens(userWalletId = userWalletId, tokens = userTokens)
} }
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) { suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) {
val userId = getUserWalletId(card) ?: return@withContext emptyList() val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
val blockchainNetworks = loadTokensOffline(card = card, userId = userId).toBlockchainNetworks() val blockchainNetworks = loadTokensOffline(userWalletId = userWalletId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) { if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks) return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks)
@ -60,8 +58,8 @@ class UserTokensRepository(
return@withContext blockchainNetworks return@withContext blockchainNetworks
} }
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> { private fun loadTokensOffline(userWalletId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) return storageService.getUserTokens(userWalletId = userWalletId) ?: emptyList()
} }
private fun loadDemoCurrencies(): List<Currency> { private fun loadDemoCurrencies(): List<Currency> {
@ -76,54 +74,49 @@ class UserTokensRepository(
.flatMap(BlockchainNetwork::toCurrencies) .flatMap(BlockchainNetwork::toCurrencies)
} }
private fun List<Currency>.toUserTokensResponse() = UserTokensResponse( private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
tokens = CurrencyConverter.convertList(this), return UserTokensResponse(
group = GROUP_DEFAULT_VALUE, tokens = CurrencyConverter.convertList(input = this),
sort = SORT_DEFAULT_VALUE, group = GROUP_DEFAULT_VALUE,
) sort = SORT_DEFAULT_VALUE,
)
}
private suspend fun handleGetUserTokensFailure(card: CardDTO, userId: String, error: Throwable): List<Currency> { private suspend fun handleGetUserTokensFailure(userWalletId: String, error: Throwable): List<Currency> {
return when { return when {
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> {
storageService.getUserTokens(card).also { storageService
remoteSaveUserTokens(userId = userId, userTokens = it.toUserTokensResponse()) .getUserTokens(userWalletId)
} ?.also { remoteSaveUserTokens(userWalletId = userWalletId, userTokens = it.toUserTokensResponse()) }
?: emptyList()
}
else -> { else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card) storageService.getUserTokens(userWalletId)?.distinct() ?: emptyList()
tokens.distinct()
} }
} }
} }
private suspend fun remoteGetUserTokens(card: CardDTO, userId: String): List<Currency> { private suspend fun remoteGetUserTokens(userWalletId: String): List<Currency> {
runCatching { return runCatching { tangemTechApi.getUserTokens(userWalletId) }
tangemTechApi.getUserTokens(userId) .fold(
}.onSuccess { response -> onSuccess = { response ->
return response.tokens response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse).also { .mapNotNull(Currency.Companion::fromTokenResponse)
storageService.saveUserTokens(userId, it.toUserTokensResponse()) .also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) }
} .distinct()
.distinct() },
}.onFailure { onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },
return handleGetUserTokensFailure(card = card, userId = userId, error = it) )
}
error("Unreachable code because runCatching must return result")
} }
private suspend fun remoteSaveUserTokens(userId: String, userTokens: UserTokensResponse) { private suspend fun remoteSaveUserTokens(userWalletId: String, userTokens: UserTokensResponse) {
// it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR // it can throw okhttp3.internal.http2.StreamResetException: stream was reset: INTERNAL_ERROR
// if the /user-tokens endpoint disabled // if the /user-tokens endpoint disabled
runCatching { runCatching { tangemTechApi.saveUserTokens(userWalletId, userTokens) }
tangemTechApi.saveUserTokens(userId, userTokens) .onFailure { Timber.e(it) }
}.onFailure {
Timber.e(it)
}
} }
private fun getUserWalletId(card: CardDTO): String? { private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue
return UserWalletIdBuilder.card(card).build()
?.stringValue
}
companion object { companion object {
private const val GROUP_DEFAULT_VALUE = "none" private const val GROUP_DEFAULT_VALUE = "none"
@ -131,24 +124,16 @@ class UserTokensRepository(
private const val NOT_FOUND_HTTP_CODE = "404" private const val NOT_FOUND_HTTP_CODE = "404"
// TODO("After adding DI") get dependencies by DI // TODO("After adding DI") get dependencies by DI
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository { fun init(
val fileReader = AndroidFileReader(context) context: Context,
val dispatchers = AppCoroutineDispatcherProvider() tangemTechService: TangemTechService,
networkConnectionManager: NetworkConnectionManager,
val oldUserTokensRepository = OldUserTokensRepository( ): UserTokensRepository {
fileReader = fileReader,
tangemTechApi = tangemTechService.api,
dispatchers = dispatchers,
)
val storageService = UserTokensStorageService(
oldUserTokensRepository = oldUserTokensRepository,
fileReader = fileReader,
)
return UserTokensRepository( return UserTokensRepository(
storageService = storageService, storageService = UserTokensStorageService(fileReader = AndroidFileReader(context)),
tangemTechApi = tangemTechService.api, tangemTechApi = tangemTechService.api,
dispatchers = dispatchers, dispatchers = AppCoroutineDispatcherProvider(),
networkConnectionManager = networkConnectionManager,
) )
} }
} }

View file

@ -4,21 +4,16 @@ import com.squareup.moshi.JsonAdapter
import com.tangem.Log import com.tangem.Log
import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.FileReader import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
class UserTokensStorageService( class UserTokensStorageService(private val fileReader: FileReader) {
private val oldUserTokensRepository: OldUserTokensRepository,
private val fileReader: FileReader,
) {
private val userTokensAdapter: JsonAdapter<UserTokensResponse> = private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java) MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java)
fun getUserTokens(userId: String): List<Currency>? { fun getUserTokens(userWalletId: String): List<Currency>? {
return try { return try {
val json = fileReader.readFile(getFileNameForUserTokens(userId)) val json = fileReader.readFile(getFileNameForUserTokens(userWalletId))
userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) } userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) }
} catch (exception: Exception) { } catch (exception: Exception) {
Log.error { exception.stackTraceToString() } Log.error { exception.stackTraceToString() }
@ -26,16 +21,9 @@ class UserTokensStorageService(
} }
} }
@Deprecated("") fun saveUserTokens(userWalletId: String, tokens: UserTokensResponse) {
suspend fun getUserTokens(card: CardDTO): List<Currency> {
val blockchainNetworks =
oldUserTokensRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed)
return blockchainNetworks.flatMap { it.toCurrencies() }
}
fun saveUserTokens(userId: String, tokens: UserTokensResponse) {
val json = userTokensAdapter.toJson(tokens) val json = userTokensAdapter.toJson(tokens)
fileReader.rewriteFile(json, getFileNameForUserTokens(userId)) fileReader.rewriteFile(json, getFileNameForUserTokens(userWalletId))
} }
companion object { companion object {

View file

@ -9,10 +9,10 @@ import com.tangem.common.KeyPair
import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.CardDTO import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.datasource.utils.AssetReader
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
class TwinCardsManager( class TwinCardsManager(
@ -89,7 +89,7 @@ class TwinCardsManager(
} }
private fun getIssuers(reader: AssetReader): List<Issuer> { private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readAssetAsString("tangem-app-config/issuers") val file = reader.readJson(fileName = "tangem-app-config/issuers")
return getAdapter().fromJson(file)!! return getAdapter().fromJson(file)!!
} }
} }

View file

@ -7,9 +7,9 @@ import android.view.View
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import com.tangem.common.extensions.VoidCallback import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]

View file

@ -5,8 +5,8 @@ import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.json.TangemSdkAdapter import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage import com.tangem.common.services.secure.SecureStorage
import com.tangem.tangem_sdk_new.storage.AndroidSecureStorage import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.tangem_sdk_new.storage.createEncryptedSharedPreferences import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager

View file

@ -61,6 +61,10 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet, userWallet: UserWallet,
currenciesToAdd: List<Currency>, currenciesToAdd: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) { ): CompletionResult<Unit> = withContext(Dispatchers.Default) {
if (currenciesToAdd.isEmpty()) {
return@withContext CompletionResult.Success(Unit)
}
val card = userWallet.scanResponse.card val card = userWallet.scanResponse.card
val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card) val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card)
listeners.forEach { it.willCurrenciesAdd(userWallet, currenciesToAddWithMissingBlockchains) } listeners.forEach { it.willCurrenciesAdd(userWallet, currenciesToAddWithMissingBlockchains) }
@ -86,6 +90,10 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet, userWallet: UserWallet,
currenciesToRemove: List<Currency>, currenciesToRemove: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) { ): CompletionResult<Unit> = withContext(Dispatchers.Default) {
if (currenciesToRemove.isEmpty()) {
return@withContext CompletionResult.Success(Unit)
}
listeners.forEach { it.willCurrenciesRemove(userWallet, currenciesToRemove) } listeners.forEach { it.willCurrenciesRemove(userWallet, currenciesToRemove) }
val card = userWallet.scanResponse.card val card = userWallet.scanResponse.card
val remainingCurrencies = getSavedCurrencies(userWallet.walletId) val remainingCurrencies = getSavedCurrencies(userWallet.walletId)

View file

@ -44,7 +44,8 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransactionType import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.filterByCoin import com.tangem.tap.features.wallet.models.filterByCoin
import com.tangem.tap.features.wallet.models.getPendingTransactions import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -62,8 +63,6 @@ internal class DefaultWalletAmountsRepository(
private val tangemTechApi: TangemTechApi, private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : WalletAmountsRepository { ) : WalletAmountsRepository {
private val walletStoresStorage = WalletStoresStorage
private val walletManagersStorage = WalletManagerStorage
override suspend fun updateAmountsForUserWallets( override suspend fun updateAmountsForUserWallets(
userWallets: List<UserWallet>, userWallets: List<UserWallet>,
@ -76,8 +75,7 @@ internal class DefaultWalletAmountsRepository(
awaitAll( awaitAll(
async { fetchAmountsForUserWallets(userWallets) }, async { fetchAmountsForUserWallets(userWallets) },
async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) }, async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) },
) ).fold()
.fold()
} }
} }
} }
@ -104,8 +102,7 @@ internal class DefaultWalletAmountsRepository(
awaitAll( awaitAll(
async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) }, async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) },
async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) }, async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) },
) ).fold()
.fold()
} }
} }
} }
@ -123,41 +120,41 @@ internal class DefaultWalletAmountsRepository(
walletStores: List<WalletStoreModel>?, walletStores: List<WalletStoreModel>?,
fiatCurrency: FiatCurrency, fiatCurrency: FiatCurrency,
): CompletionResult<Unit> { ): CompletionResult<Unit> {
// FIXME: Use NetworkConnectionManager when it is added to DI val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { if (!networkConnectionManager.isOnline) {
return CompletionResult.Failure(WalletStoresError.NoInternetConnection) return CompletionResult.Failure(WalletStoresError.NoInternetConnection)
} }
val walletStoresInternal = walletStores ?: getWalletStores(userWallets) val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
val currencies = walletStoresInternal val currencies = walletStoresInternal.asSequence().flatMap { it.walletsData }.map { it.currency }
.asSequence()
.flatMap { it.walletsData }
.map { it.currency }
val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList() val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList()
return withContext(dispatchers.io) { return withContext(dispatchers.io) {
runCatching { tangemTechApi.getRates(fiatCurrency.code.lowercase(), coinsIds.joinToString(",")) } runCatching {
.onSuccess { tangemTechApi.getRates(
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates) fiatCurrency.code.lowercase(),
return@withContext CompletionResult.Success(Unit) coinsIds.joinToString(","),
} )
.onFailure { }.onSuccess {
val error = WalletStoresError.FetchFiatRatesError( updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
currencies = currencies.map(Currency::currencySymbol).toList(), return@withContext CompletionResult.Success(Unit)
cause = it, }.onFailure {
) val error = WalletStoresError.FetchFiatRatesError(
currencies = currencies.map(Currency::currencySymbol).toList(),
cause = it,
)
Timber.e( Timber.e(
error, error,
""" """
Unable to fetch fiat rates Unable to fetch fiat rates
|- Coins ids: $coinsIds |- Coins ids: $coinsIds
""".trimIndent(), """.trimIndent(),
) )
return@withContext CompletionResult.Failure(error) return@withContext CompletionResult.Failure(error)
} }
error("Unreachable code because runCatching must return result") error("Unreachable code because runCatching must return result")
} }
@ -166,9 +163,7 @@ internal class DefaultWalletAmountsRepository(
private suspend fun fetchAmountsForUserWallets( private suspend fun fetchAmountsForUserWallets(
userWallets: List<UserWallet>, userWallets: List<UserWallet>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) { ): CompletionResult<Unit> = withContext(Dispatchers.Default) {
userWallets.map { async { fetchAmountsForUserWallet(it) } } userWallets.map { async { fetchAmountsForUserWallet(it) } }.awaitAll().fold()
.awaitAll()
.fold()
} }
private suspend fun fetchAmountsForUserWallet( private suspend fun fetchAmountsForUserWallet(
@ -186,8 +181,8 @@ internal class DefaultWalletAmountsRepository(
scanResponse: ScanResponse, scanResponse: ScanResponse,
walletStores: List<WalletStoreModel>, walletStores: List<WalletStoreModel>,
): CompletionResult<Unit> = coroutineScope { ): CompletionResult<Unit> = coroutineScope {
// FIXME: Use NetworkConnectionManager when it is added to DI val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { if (!networkConnectionManager.isOnline) {
walletStores.forEach { walletStores.forEach {
updateWalletStoreWithUnreachable(it) updateWalletStoreWithUnreachable(it)
} }
@ -200,9 +195,7 @@ internal class DefaultWalletAmountsRepository(
val walletManager = walletStore.walletManager val walletManager = walletStore.walletManager
fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager) fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager)
} }
} }.awaitAll().fold()
.awaitAll()
.fold()
} }
private suspend fun fetchAmountsForWalletStore( private suspend fun fetchAmountsForWalletStore(
@ -223,24 +216,25 @@ internal class DefaultWalletAmountsRepository(
updateWalletStoreWithUnreachable(walletStore) updateWalletStoreWithUnreachable(walletStore)
} }
else -> { else -> {
updateWalletManager(scanResponse, walletManager) updateWalletManager(scanResponse, walletManager).map {
.map { updateWalletManagerInStorage(userWalletId, walletManager) } updateWalletManagerInStorage(
.flatMap { userWalletId,
updateWalletStoreWithAmounts( walletManager,
walletStore = walletStore, )
updatedWallet = walletManager.wallet, }.flatMap {
// FIXME: move DemoHelper to Demo core module maybe updateWalletStoreWithAmounts(
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId), walletStore = walletStore,
) updatedWallet = walletManager.wallet,
} // FIXME: move DemoHelper to Demo core module maybe
.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) } isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
.flatMapOnFailure { error -> )
updateWalletStoreWithError( }.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error ->
walletStore = walletStore, updateWalletStoreWithError(
wallet = walletManager.wallet, walletStore = walletStore,
error = error, wallet = walletManager.wallet,
) error = error,
} )
}
} }
} }
} }
@ -267,8 +261,7 @@ internal class DefaultWalletAmountsRepository(
walletStore: WalletStoreModel, walletStore: WalletStoreModel,
walletManager: WalletManager, walletManager: WalletManager,
): CompletionResult<Unit> { ): CompletionResult<Unit> {
val rentProvider = walletManager as? RentProvider val rentProvider = walletManager as? RentProvider ?: return CompletionResult.Success(Unit)
?: return CompletionResult.Success(Unit)
when (val result = rentProvider.minimalBalanceForRentExemption()) { when (val result = rentProvider.minimalBalanceForRentExemption()) {
is Success -> { is Success -> {
@ -318,7 +311,7 @@ internal class DefaultWalletAmountsRepository(
) )
if (error is BlockchainSdkError) { if (error is BlockchainSdkError) {
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore( prevState.replaceWalletStore(
walletStoreToUpdate = walletStore, walletStoreToUpdate = walletStore,
update = { update = {
@ -350,7 +343,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(), """.trimIndent(),
) )
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore( prevState.replaceWalletStore(
walletStoreToUpdate = walletStore, walletStoreToUpdate = walletStore,
update = { update = {
@ -378,7 +371,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(), """.trimIndent(),
) )
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore( prevState.replaceWalletStore(
walletStoreToUpdate = walletStore, walletStoreToUpdate = walletStore,
update = { update = {
@ -402,7 +395,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(), """.trimIndent(),
) )
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore( prevState.replaceWalletStore(
walletStoreToUpdate = walletStore, walletStoreToUpdate = walletStore,
update = { update = {
@ -425,7 +418,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(), """.trimIndent(),
) )
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStores( prevState.replaceWalletStores(
walletStoresToUpdate = walletStores, walletStoresToUpdate = walletStores,
update = { update = {
@ -450,7 +443,7 @@ internal class DefaultWalletAmountsRepository(
) )
if (rent != walletStore.walletRent) { if (rent != walletStore.walletRent) {
walletStoresStorage.update { prevState -> WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore( prevState.replaceWalletStore(
walletStoreToUpdate = walletStore, walletStoreToUpdate = walletStore,
update = { update = {
@ -465,14 +458,12 @@ internal class DefaultWalletAmountsRepository(
userWalletId: UserWalletId, userWalletId: UserWalletId,
walletManager: WalletManager, walletManager: WalletManager,
) = withContext(Dispatchers.Default) { ) = withContext(Dispatchers.Default) {
walletManagersStorage.update { prevManagers -> WalletManagerStorage.update { prevManagers ->
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty() val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply {
.toMutableList() replaceByOrAdd(walletManager) {
.apply { it.wallet.blockchain == walletManager.wallet.blockchain
replaceByOrAdd(walletManager) {
it.wallet.blockchain == walletManager.wallet.blockchain
}
} }
}
prevManagers.apply { prevManagers.apply {
set(userWalletId, newManagersForUserWallet) set(userWalletId, newManagersForUserWallet)
@ -481,13 +472,8 @@ internal class DefaultWalletAmountsRepository(
} }
private suspend fun getWalletStores(userWallets: List<UserWallet>): List<WalletStoreModel> { private suspend fun getWalletStores(userWallets: List<UserWallet>): List<WalletStoreModel> {
return userWallets return userWallets.map { it.walletId }.flatMap { userWalletId ->
.map { it.walletId } WalletStoresStorage.getAll().firstOrNull()?.get(userWalletId).orEmpty()
.flatMap { userWalletId -> }
walletStoresStorage.getAll()
.firstOrNull()
?.get(userWalletId)
.orEmpty()
}
} }
} }

View file

@ -0,0 +1,11 @@
package com.tangem.tap.features.demo
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.tap.proxy.AppStateHolder
import javax.inject.Inject
class DefaultDemoModeDatasource @Inject constructor(private val appStateHolder: AppStateHolder) : DemoModeDatasource {
override val isDemoModeActive: Boolean
get() = appStateHolder.scanResponse?.let { DemoHelper.isDemoCard(it) } == true
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.demo package com.tangem.tap.features.demo
import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner import com.tangem.blockchain.common.TransactionSigner
@ -11,6 +10,7 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.demo.DemoConfig
import com.tangem.tap.common.extensions.dispatchNotification import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsAction
@ -18,10 +18,8 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R import com.tangem.wallet.R
import org.rekotlin.Action import org.rekotlin.Action
import java.math.BigDecimal
/** /**
[REDACTED_AUTHOR] [REDACTED_AUTHOR]
@ -84,412 +82,6 @@ object DemoHelper {
} }
} }
@Suppress("LargeClass")
class DemoConfig {
val demoBlockchains = listOf(
Blockchain.Bitcoin,
Blockchain.Ethereum,
Blockchain.Dogecoin,
Blockchain.Solana,
)
val demoCardIds: List<String> by lazy {
val demoIds = getReleaseIds().toMutableList()
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
return@lazy demoIds.distinct()
}
private val walletBalances: Map<Blockchain, Amount> = mapOf(
Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin),
Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum),
Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin),
Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana),
)
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
fun isTestDemoCardId(cardId: String): Boolean = testDemoCardIds.contains(cardId)
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
private fun getReleaseIds(): List<String> {
return (releaseDemoCardIds + testDemoCardIds).distinct()
}
@Suppress("ClassOrdering")
private val releaseDemoCardIds = mutableListOf(
// === Not from the Google Sheet table ===
"AC01000000041225",
"AC01000000041472",
"AB01000000046498",
"AB01000000049608",
"AB01000000049574",
"AB01000000046704",
"AB02000000051000",
"AB02000000050911",
// === Mvideo ===
// Wallet
"AC01000000045754",
"AC01000000041662",
"AC01000000041647",
"AC01000000041209",
"AC01000000042462",
"AC01000000041100",
"AC01000000041621",
"AC01000000045960",
"AC01000000041092",
"AC01000000041217",
"AC01000000013489",
"AC01000000028610",
"AC01000000028701",
"AC01000000028578",
"AC01000000027281",
"AC01000000027216",
"AC01000000028594",
"AC01000000028602",
"AC01000000028636",
"AC01000000013968",
"AC01000000027208",
"AC01000000013471",
"AC01000000028586",
"AC01000000013703",
"AC01000000028628",
"AC01000000028693",
"AC01000000028685",
"AC01000000013950",
"AC01000000013828",
"AC01000000013497",
"AC01000000013836",
"AC01000000013505",
"AC03000000046693",
"AC03000000046685",
"AC03000000046677",
"AC03000000046669",
"AC03000000046651",
"AC03000000046644",
"AC03000000046636",
"AC03000000046628",
"AC03000000046610",
"AC03000000046602",
"AC03000000046594",
"AC03000000046586",
"AC03000000046578",
"AC03000000046560",
"AC03000000046552",
"AC03000000046545",
"AC03000000046537",
"AC03000000046529",
"AC03000000046511",
"AC03000000046800",
"AC03000000046792",
"AC03000000046784",
"AC03000000046776",
"AC03000000046768",
"AC03000000046750",
"AC03000000046743",
"AC03000000046735",
"AC03000000046727",
"AC03000000046446",
"AC03000000046438",
"AC03000000046412",
"AC03000000046388",
"AC03000000046370",
"AC03000000046354",
"AC03000000046347",
"AC03000000046339",
"AC03000000046321",
"AC03000000046172",
"AC03000000046396",
"AC03000000046404",
"AC03000000046701",
"AC03000000046420",
"AC03000000046719",
"AC03000000046503",
"AC03000000046495",
"AC03000000046487",
"AC03000000046362",
"AC03000000046479",
"AC03000000046461",
"AC03000000046453",
// Note BTC
"AB01000000059608",
"AB01000000046647",
"AB01000000046571",
"AB01000000046746",
"AB01000000059574",
"AB01000000046753",
"AB01000000046605",
"AB01000000046761",
"AB01000000046720",
"AB01000000046530",
"AB01000000016475",
"AB01000000016483",
"AB01000000016491",
"AB01000000020709",
"AB01000000020717",
"AB01000000015550",
"AB01000000015394",
"AB01000000016079",
"AB01000000016087",
"AB01000000016095",
"AB01000000020915",
"AB01000000017184",
"AB01000000020907",
"AB01000000017192",
"AB01000000016210",
"AB01000000016111",
"AB01000000016103",
"AB01000000015766",
"AB01000000015774",
"AB01000000015782",
"AB01000000022598",
"AB01000000022580",
"AB01000000005688",
"AB07000000005696",
"AB07000000005902",
"AB07000000005910",
"AB07000000005928",
"AB07000000005936",
"AB07000000005944",
"AB07000000005993",
"AB07000000005985",
"AB07000000005977",
"AB07000000005969",
"AB07000000005951",
"AB07000000005605",
"AB07000000005803",
"AB07000000005811",
"AB07000000005829",
"AB07000000005837",
"AB07000000005845",
"AB07000000005852",
"AB07000000005860",
"AB07000000005878",
"AB07000000005886",
"AB07000000005894",
"AB07000000005704",
"AB07000000005712",
"AB07000000005720",
"AB07000000005738",
"AB07000000005746",
"AB07000000005514",
"AB07000000005522",
"AB07000000005563",
"AB07000000005571",
"AB07000000005589",
"AB07000000005597",
"AB07000000005613",
"AB07000000005621",
"AB07000000005639",
"AB07000000005647",
"AB07000000005654",
"AB07000000005662",
"AB07000000005670",
"AB07000000005530",
"AB07000000005548",
"AB07000000005555",
"AB07000000005753",
"AB07000000005761",
"AB07000000005779",
"AB07000000005787",
"AB07000000005795",
"AB07000000005506",
// Note ETH
"AB02000000051083",
"AB02000000051059",
"AB02000000051158",
"AB02000000050986",
"AB02000000051026",
"AB02000000050960",
"AB02000000051042",
"AB02000000051091",
"AB02000000051034",
"AB02000000051133",
"AB02000000019924",
"AB02000000019932",
"AB02000000022092",
"AB02000000022282",
"AB02000000023983",
"AB02000000023439",
"AB02000000020328",
"AB02000000020310",
"AB02000000021565",
"AB02000000022357",
"AB02000000023355",
"AB02000000022324",
"AB02000000022100",
"AB02000000019999",
"AB02000000020013",
"AB02000000020005",
"AB02000000020021",
"AB02000000020039",
"AB02000000020278",
"AB02000000020252",
"AB02000000018652",
"AB02000000018561",
"AB08000000009481",
"AB08000000009473",
"AB08000000009705",
"AB08000000009897",
"AB08000000009689",
"AB08000000009671",
"AB08000000009465",
"AB08000000009457",
"AB08000000009440",
"AB08000000009432",
"AB08000000009424",
"AB08000000009416",
"AB08000000009408",
"AB08000000009390",
"AB08000000009374",
"AB08000000009382",
"AB08000000009267",
"AB08000000009275",
"AB08000000009283",
"AB08000000009291",
"AB08000000009309",
"AB08000000009317",
"AB08000000009325",
"AB08000000009333",
"AB08000000009341",
"AB08000000009358",
"AB08000000009366",
"AB08000000009077",
"AB08000000009143",
"AB08000000009168",
"AB08000000009184",
"AB08000000009192",
"AB08000000009200",
"AB08000000009226",
"AB08000000009218",
"AB08000000009234",
"AB08000000009242",
"AB08000000008574",
"AB08000000009069",
"AB08000000008525",
"AB08000000009051",
"AB08000000009135",
"AB08000000009150",
"AB08000000009176",
"AB08000000009085",
"AB08000000009093",
"AB08000000009101",
"AB08000000009119",
"AB08000000009127",
"AB08000000009259",
// === Technopark ===
// Wallet
"AC01000000044120",
"AC01000000044997",
"AC01000000044989",
"AC01000000043494",
"AC01000000043486",
"AC01000000044187",
"AC01000000043148",
"AC01000000044013",
"AC01000000043973",
"AC01000000044815",
"AC01000000044807",
"AC01000000043809",
"AC01000000043833",
"AC01000000043460",
"AC01000000043064",
"AC01000000044138",
"AC01000000044500",
"AC01000000044492",
"AC01000000044260",
"AC01000000044278",
// Note BTC
"AB01000000049864",
"AB01000000053239",
"AB01000000053056",
"AB01000000054237",
"AB01000000054245",
"AB01000000054211",
"AB01000000054229",
"AB01000000053189",
"AB01000000054195",
"AB01000000050797",
"AB01000000053833",
"AB01000000052124",
"AB01000000051605",
"AB01000000052223",
"AB01000000052207",
"AB01000000052199",
"AB01000000047785",
"AB01000000047850",
"AB01000000047868",
"AB01000000048288",
// Note ETH
"AB02000000049715",
"AB02000000049848",
"AB02000000049814",
"AB02000000049863",
"AB02000000049871",
"AB02000000049855",
"AB02000000049285",
"AB02000000049277",
"AB02000000049558",
"AB02000000049889",
"AB02000000049988",
"AB02000000049707",
"AB02000000049699",
"AB02000000049897",
"AB02000000049905",
"AB02000000049913",
"AB02000000049251",
"AB02000000049533",
"AB02000000049541",
"AB02000000049830",
// === more cids ===
"AC03000000091418",
"AC03000000091400",
"AC03000000099007",
"AC03000000098991",
"AC03000000098942",
"AC03000000091715",
"AC03000000091301",
"AC03000000091343",
"AB01000000055705",
"AB01000000052918",
"AB01000000047710",
"AB01000000052306",
"AB01000000047645",
"AB01000000048957",
"AB01000000052900",
"AB01000000050391",
"AB01000000047363",
"AB02000000053998",
"AB02000000019809",
"AB02000000020872",
"AB02000000022027",
"AB02000000058955",
"AB02000000053253",
"AB02000000048063",
"AB02000000023736",
"AB02000000058187",
)
@Suppress("ClassOrdering")
private val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
)
@Suppress("ClassOrdering")
private val debugTestDemoCardIds = listOf<String>()
}
class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender { class DemoTransactionSender(private val walletManager: WalletManager) : TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> { override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.demo.DemoConfig
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.demo.di
import com.tangem.datasource.demo.DemoModeDatasource
import com.tangem.tap.features.demo.DefaultDemoModeDatasource
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface DemoModule {
@Binds
@Singleton
fun bindDemoModeHelper(demoModeHelper: DefaultDemoModeDatasource): DemoModeDatasource
}

View file

@ -68,7 +68,7 @@ class DetailsMiddleware {
private fun handleAction(state: DetailsState, action: Action) { private fun handleAction(state: DetailsState, action: Action) {
when (action) { when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action, state)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action) is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(state, action)
is DetailsAction.ReCreateTwinsWallet -> { is DetailsAction.ReCreateTwinsWallet -> {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet)) store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
@ -157,15 +157,15 @@ class DetailsMiddleware {
class ManageSecurityMiddleware { class ManageSecurityMiddleware {
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
fun handle(action: DetailsAction.ManageSecurity) { fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
when (action) { when (action) {
is DetailsAction.ManageSecurity.OpenSecurity -> { is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity)) store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
} }
is DetailsAction.ManageSecurity.SaveChanges -> { is DetailsAction.ManageSecurity.SaveChanges -> {
val cardId = store.state.detailsState.scanResponse?.card?.cardId val cardSettingsState = detailsState.cardSettingsState
val selectedOption = val cardId = cardSettingsState?.card?.cardId
store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
scope.launch { scope.launch {
val result = when (selectedOption) { val result = when (selectedOption) {
SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId) SecurityOption.LongTap -> tangemSdkManager.setLongTap(cardId)

View file

@ -5,11 +5,8 @@ import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.entities.Button import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.store
import org.rekotlin.StateType import org.rekotlin.StateType
import java.util.* import java.util.*
import kotlin.properties.ReadOnlyProperty
data class DetailsState( data class DetailsState(
val scanResponse: ScanResponse? = null, val scanResponse: ScanResponse? = null,
@ -19,14 +16,7 @@ data class DetailsState(
val createBackupAllowed: Boolean = false, val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default, val appCurrency: FiatCurrency = FiatCurrency.Default,
val appSettingsState: AppSettingsState = AppSettingsState(), val appSettingsState: AppSettingsState = AppSettingsState(),
) : StateType { ) : StateType
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { _, _ ->
store.state.twinCardsState
}
}
data class CardInfo( data class CardInfo(
val cardId: String, val cardId: String,

View file

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
@ -21,9 +19,6 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val detailsViewModel = DetailsViewModel(store) private val detailsViewModel = DetailsViewModel(store)
private var detailsScreenState: MutableState<DetailsScreenState> =
mutableStateOf(detailsViewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
Analytics.send(Settings.ScreenOpened()) Analytics.send(Settings.ScreenOpened())
@ -42,7 +37,7 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
isTransitionGroup = true isTransitionGroup = true
TangemTheme { TangemTheme {
DetailsScreen( DetailsScreen(
state = detailsScreenState.value, state = detailsViewModel.detailsScreenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) }, onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
) )
} }
@ -66,6 +61,6 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun newState(state: DetailsState) { override fun newState(state: DetailsState) {
if (activity == null || view == null) return if (activity == null || view == null) return
detailsScreenState.value = detailsViewModel.updateState(state) detailsViewModel.detailsScreenState.value = detailsViewModel.updateState(state)
} }
} }

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.details.ui.details
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -16,8 +18,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
@ -27,10 +34,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.ScreenTitle import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.launch
@Composable @Composable
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) { fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
@ -46,32 +55,35 @@ fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
@Composable @Composable
fun Content(state: DetailsScreenState) { fun Content(state: DetailsScreenState) {
Column( Box {
modifier = Modifier Column(
.fillMaxSize() modifier = Modifier
.verticalScroll(rememberScrollState()), .fillMaxSize()
) { .verticalScroll(rememberScrollState()),
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp)) ) {
state.elements.map { element -> ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
if (element == SettingsElement.WalletConnect) { state.elements.map { element ->
WalletConnectDetailsItem(onItemsClick = state.onItemsClick) if (element == SettingsElement.WalletConnect) {
} else { WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
DetailsItem( } else {
item = element, DetailsItem(
appCurrency = state.appCurrency, item = element,
onItemsClick = { state.onItemsClick(element) }, appCurrency = state.appCurrency,
) onItemsClick = { state.onItemsClick(element) },
)
}
} }
Spacer(modifier = Modifier.weight(1f))
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
)
} }
Spacer(modifier = Modifier.weight(1f)) ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
)
} }
} }
@ -165,6 +177,36 @@ fun TangemSocialAccounts(
} }
} }
@Composable
fun BoxScope.ShowSnackbarIfNeeded(snackbarErrorState: EventError) {
val snackbarHostState = remember { SnackbarHostState() }
val coroutineScope = rememberCoroutineScope()
SnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(vertical = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
hostState = snackbarHostState,
)
val errorTitle = when (snackbarErrorState) {
is EventError.DemoReferralNotAvailable -> stringResource(id = R.string.alert_demo_feature_disabled)
EventError.Empty -> ""
}
if (snackbarErrorState != EventError.Empty) {
SideEffect {
coroutineScope.launch {
snackbarHostState.showSnackbar(errorTitle)
}
when (snackbarErrorState) {
is EventError.DemoReferralNotAvailable -> snackbarErrorState.onErrorShow.invoke()
else -> {
/*no-op*/
}
}
}
}
}
@Composable @Composable
@Preview @Preview
private fun Preview() { private fun Preview() {

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.details.ui.details package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.wallet.R import com.tangem.wallet.R
@Immutable @Immutable
@ -11,6 +13,7 @@ data class DetailsScreenState(
val appCurrency: String, val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit, val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit, val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
) { ) {
val appNameRes: Int = R.string.tangem_app_name val appNameRes: Int = R.string.tangem_app_name
} }
@ -29,7 +32,8 @@ enum class SettingsElement(
AppSettings(R.drawable.ic_settings, R.string.app_settings_title), AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy); PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy),
TesterMenu(R.drawable.ic_alert_24, R.string.tester_menu)
} }
@Immutable @Immutable
@ -38,6 +42,11 @@ data class SocialNetworkLink(
val url: String, val url: String,
) )
sealed class EventError {
object Empty : EventError()
data class DemoReferralNotAvailable(val onErrorShow: () -> Unit) : EventError()
}
sealed class SocialNetwork(val id: String, val iconRes: Int) { sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram) object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram)
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter) object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter)

View file

@ -1,5 +1,7 @@
package com.tangem.tap.features.details.ui.details package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.FeedbackEmail
@ -18,6 +20,9 @@ import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) { class DetailsViewModel(private val store: Store<AppState>) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
private set
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
fun updateState(state: DetailsState): DetailsScreenState { fun updateState(state: DetailsState): DetailsScreenState {
val cardTypesResolver = state.scanResponse?.cardTypesResolver val cardTypesResolver = state.scanResponse?.cardTypesResolver
@ -39,6 +44,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null SettingsElement.AppSettings -> if (state.appSettingsState.isBiometricsAvailable) it else null
SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null SettingsElement.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != true) it else null
SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null SettingsElement.ReferralProgram -> if (cardTypesResolver?.isTangemWallet() == true) it else null
SettingsElement.TesterMenu -> if (BuildConfig.TESTER_MENU_ENABLED) it else null
else -> it else -> it
} }
} }
@ -96,6 +102,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.ReferralProgram -> { SettingsElement.ReferralProgram -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram)) store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
} }
SettingsElement.TesterMenu -> {
store.state.daggerGraphState.testerRouter?.startTesterScreen()
}
} }
} }

View file

@ -3,7 +3,7 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message import com.tangem.Message
import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback import com.tangem.common.extensions.VoidCallback
import com.tangem.datasource.utils.AssetReader import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.TwinCardsManager

View file

@ -15,10 +15,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.datasource.utils.AndroidAssetReader import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TwinCardNumber import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.beginDelayedTransition import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat import com.tangem.tap.common.extensions.getDrawableCompat
@ -39,10 +39,16 @@ import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@Suppress("LargeClass") @Suppress("LargeClass")
@AndroidEntryPoint
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() { class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
@Inject
lateinit var assetReader: AssetReader
private val mainBinding by lazy { binding.vMain } private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None private var previousStep: TwinCardsStep = TwinCardsStep.None
@ -253,8 +259,8 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch( store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep( TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
AndroidAssetReader(requireContext()), reader = assetReader,
), ),
) )
} }

View file

@ -19,8 +19,8 @@ import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.inflate import com.tangem.tap.common.extensions.inflate

View file

@ -6,9 +6,9 @@ import android.animation.PropertyValuesHolder
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState import com.tangem.sdk.ui.widget.leapfrogWidget.LeapViewState
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
class WalletCardsWidget( class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget, val leapfrogWidget: LeapfrogWidget,

View file

@ -155,6 +155,12 @@ sealed class SendAction : SendScreenAction {
val reduceAmount: BigDecimal, val reduceAmount: BigDecimal,
) : Dialog() ) : Dialog()
data class KaspaWarningDialog(
val maxOutputs: Int,
val maxAmount: BigDecimal,
val onOk: () -> Unit,
) : Dialog()
sealed class SendTransactionFails : Dialog() { sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError) : Dialog() data class CardSdkError(val error: TangemSdkError) : Dialog()
data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog() data class BlockchainSdkError(val error: com.tangem.blockchain.common.BlockchainSdkError) : Dialog()

View file

@ -52,7 +52,7 @@ internal class AddressPayIdMiddleware {
} }
private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) { private fun setAddressAndCheck(data: String, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val potentialPayId = data.toLowerCase() val potentialPayId = data.lowercase()
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) { if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput)) dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else { } else {
@ -113,10 +113,11 @@ internal class AddressPayIdMiddleware {
} }
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) { private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) { val addressSchemeSplit = when (wallet.blockchain) {
listOf(address) Blockchain.BitcoinCash,
} else { Blockchain.Kaspa,
address.split(":") -> listOf(address)
else -> address.split(":")
} }
val noSchemeAddress = when (addressSchemeSplit.size) { val noSchemeAddress = when (addressSchemeSplit.size) {
1 -> address // no scheme 1 -> address // no scheme

View file

@ -269,6 +269,18 @@ private fun sendTransaction(
// from XLM, XRP, Polkadot // from XLM, XRP, Polkadot
dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error)) dispatch(SendAction.Dialog.SendTransactionFails.BlockchainSdkError(error))
} }
is BlockchainSdkError.Kaspa.UtxoAmountError -> {
dispatch(
SendAction.Dialog.KaspaWarningDialog(
maxOutputs = error.maxOutputs,
maxAmount = error.maxAmount,
onOk = {
dispatch(AmountAction.SetAmount(error.maxAmount, isUserInput = false))
dispatch(AmountActionUi.CheckAmountToSend)
},
),
)
}
else -> { else -> {
when { when {
error.customMessage.contains(DemoTransactionSender.ID) -> { error.customMessage.contains(DemoTransactionSender.ID) -> {

View file

@ -55,6 +55,7 @@ private class SendReducer : SendInternalReducer {
sendState.copy(sendButtonState = IndeterminateProgressButton(action.state)) sendState.copy(sendButtonState = IndeterminateProgressButton(action.state))
} }
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action) is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.KaspaWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action) is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action) is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action) is SendAction.Dialog.RequestFeeError -> sendState.copy(dialog = action)

View file

@ -17,7 +17,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message import com.tangem.Message
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.tangem_sdk_new.extensions.hideSoftKeyboard import com.tangem.sdk.extensions.hideSoftKeyboard
import com.tangem.tap.common.KeyboardObserver import com.tangem.tap.common.KeyboardObserver
import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.entities.FiatCurrency

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
object KaspaWarningDialog {
fun create(context: Context, dialog: SendAction.Dialog.KaspaWarningDialog): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(
context.getString(
R.string.kaspa_withdrawal_message_warning,
dialog.maxOutputs,
dialog.maxAmount.toPlainString(),
),
)
setPositiveButton(R.string.common_ok) { _, _ ->
dialog.onOk()
}
setOnDismissListener {
store.dispatchDialogHide()
}
}
.create()
}
}

View file

@ -5,7 +5,7 @@ import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.common.module.ModuleMessageConverter import com.tangem.common.module.ModuleMessageConverter
import com.tangem.tangem_sdk_new.extensions.localizedDescription import com.tangem.sdk.extensions.localizedDescription
import com.tangem.tap.common.extensions.stripZeroPlainString import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.common.redux.global.GlobalAction

View file

@ -33,6 +33,7 @@ import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.KaspaWarningDialog
import com.tangem.tap.features.send.ui.dialogs.RequestFeeErrorDialog import com.tangem.tap.features.send.ui.dialogs.RequestFeeErrorDialog
import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
@ -124,6 +125,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
dialog?.show() dialog?.show()
} }
} }
is SendAction.Dialog.KaspaWarningDialog -> {
if (dialog == null) {
dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails.CardSdkError -> { is SendAction.Dialog.SendTransactionFails.CardSdkError -> {
if (dialog == null) { if (dialog == null) {
dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog) dialog = SendTransactionFailsDialog.create(fg.requireContext(), state.dialog)

View file

@ -0,0 +1,21 @@
package com.tangem.tap.features.tokens.di
import com.tangem.tap.features.tokens.presentation.router.AddTokensRouter
import com.tangem.tap.features.tokens.presentation.router.DefaultAddTokensRouter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ViewModelComponent
import dagger.hilt.android.scopes.ViewModelScoped
/**
[REDACTED_AUTHOR]
*/
@Module
@InstallIn(ViewModelComponent::class)
internal object AddTokensRouterModule {
@Provides
@ViewModelScoped
fun provideAddTokensRouter(): AddTokensRouter = DefaultAddTokensRouter()
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.tokens.ui package com.tangem.tap.features.tokens.presentation
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
@ -31,6 +31,7 @@ import com.tangem.tap.mainScope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentAddTokensBinding import com.tangem.wallet.databinding.FragmentAddTokensBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
@ -40,7 +41,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.rekotlin.StoreSubscriber import org.rekotlin.StoreSubscriber
class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> { @AndroidEntryPoint
internal class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubscriber<TokensState> {
private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind) private val binding: FragmentAddTokensBinding by viewBinding(FragmentAddTokensBinding::bind)
private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState) private var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
@ -155,18 +157,18 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc
tokensState.value = state tokensState.value = state
binding.toolbar.title = getString(toolbarTitle) binding.toolbar.title = getString(toolbarTitle)
} }
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow { private fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener( val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener { object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean { override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "") trySend(newText ?: "")
return false return false
} }
}, },
) )
awaitClose { watcher } awaitClose { watcher }
}
} }

View file

@ -0,0 +1,11 @@
package com.tangem.tap.features.tokens.presentation.router
/** Add tokens feature router */
internal interface AddTokensRouter {
/** Return to last screen */
fun popBackStack()
/** Open screen for adding custom token */
fun openAddCustomTokenScreen()
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.tokens.presentation.router
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.store
/**
* Router implementation for add tokens feature
*
[REDACTED_AUTHOR]
*/
internal class DefaultAddTokensRouter : AddTokensRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
override fun openAddCustomTokenScreen() {
Analytics.send(ManageTokens.ButtonCustomToken())
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.tap.features.tokens.presentation.states
/**
* Network item state
*
[REDACTED_AUTHOR]
*/
sealed interface AddTokensNetworkItemState {
/** Network name */
val name: String
/** Network protocol name */
val protocolName: String
/** Network icon id from resources */
val iconResId: Int
/** Flag that determines if the network is the main network for the token */
val isMainNetwork: Boolean
/**
* Network item state that is available for read
*
* @property name network name
* @property iconResId network icon id from resources
* @property isMainNetwork flag that determines if the network is the main network for the token
*/
data class ReadAccess(
override val name: String,
override val protocolName: String,
override val iconResId: Int,
override val isMainNetwork: Boolean,
) : AddTokensNetworkItemState
/**
* Network item state that is available for read and edit
*
* @property name network name
* @property iconResId network icon id from resources
* @property isMainNetwork flag that determines if the network is the main network for the token
* @property isAdded flag that determines if the user has saved the token
* @property networkId network id
* @property onToggleClick lambda be invoked when switch is been toggled
*/
data class EditAccess(
override val name: String,
override val protocolName: String,
override val iconResId: Int,
override val isMainNetwork: Boolean,
val isAdded: Boolean,
val networkId: String,
val onToggleClick: (String) -> Unit,
) : AddTokensNetworkItemState
}

View file

@ -0,0 +1,56 @@
package com.tangem.tap.features.tokens.presentation.states
/**
* State holder for adding token screen
*
[REDACTED_AUTHOR]
*/
internal sealed interface AddTokensStateHolder {
/** Toolbar state */
val toolbarState: AddTokensToolbarState
/**
* Util function that allow to make a copy
*
* @param toolbarState toolbar state
*/
fun copySealed(toolbarState: AddTokensToolbarState): AddTokensStateHolder {
return when (this) {
is ManageAccess -> copy(toolbarState = toolbarState)
is Loading -> copy(toolbarState = toolbarState)
is ReadAccess -> copy(toolbarState = toolbarState)
}
}
/**
* Loading state
*
* @property toolbarState toolbar state
*/
data class Loading(override val toolbarState: AddTokensToolbarState) : AddTokensStateHolder
/**
* State screen that is available only for read
*
* @property toolbarState toolbar state
* @property tokens tokens list
*/
data class ReadAccess(
override val toolbarState: AddTokensToolbarState,
override val tokens: List<TokenItemModel>,
) : AddTokensStateHolder, TokensListVisibility
/**
* State screen that is available for read and edit
*
* @property toolbarState toolbar state
* @property tokens tokens list
* @property onSaveButtonClick callback to be invoked when SaveButton is being clicked
*/
data class ManageAccess(
override val toolbarState: AddTokensToolbarState,
override val tokens: List<TokenItemModel>,
val onSaveButtonClick: () -> Unit,
) : AddTokensStateHolder, TokensListVisibility
}

View file

@ -0,0 +1,63 @@
package com.tangem.tap.features.tokens.presentation.states
/** Toolbar state */
sealed interface AddTokensToolbarState {
/** Callback to be invoked when BackButton is being clicked */
val onBackButtonClick: () -> Unit
/** Callback to be invoked when SearchButton is being clicked */
val onSearchButtonClick: () -> Unit
/** Toolbar state as title */
sealed interface Title : AddTokensToolbarState {
/** Toolbar title id from resources */
val titleResId: Int
/**
* Title state that is available only for read
*
* @property titleResId toolbar title id from resources
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
* @property onSearchButtonClick callback to be invoked when SearchButton is being clicked
*/
data class ReadAccess(
override val titleResId: Int,
override val onBackButtonClick: () -> Unit,
override val onSearchButtonClick: () -> Unit,
) : Title
/**
* Title state that is available for read and edit
*
* @property titleResId toolbar title id from resources
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
* @property onSearchButtonClick callback to be invoked when SearchButton is being clicked
* @property onAddCustomTokenClick callback to be invoked when AddCustomTokenButton is being clicked
*/
data class EditAccess(
override val titleResId: Int,
override val onBackButtonClick: () -> Unit,
override val onSearchButtonClick: () -> Unit,
val onAddCustomTokenClick: () -> Unit,
) : Title
}
/**
* Toolbar state as input field
*
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
* @property onSearchButtonClick callback to be invoked when SearchButton is being clicked
* @property value input value
* @property onValueChange lambda to be invoked when search value is being changed
* @property onCleanButtonClick callback to be invoked when CleanButton is being clicked
*/
data class SearchInputField(
override val onBackButtonClick: () -> Unit,
override val onSearchButtonClick: () -> Unit,
val value: String,
val onValueChange: (String) -> Unit,
val onCleanButtonClick: () -> Unit,
) : AddTokensToolbarState
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.tokens.presentation.states
/**
* Token model
*
* @property name token name
* @property iconUrl token icon url
* @property networks list of networks
*/
data class TokenItemModel(
val name: String,
val iconUrl: String,
val networks: List<AddTokensNetworkItemState>,
)

View file

@ -0,0 +1,8 @@
package com.tangem.tap.features.tokens.presentation.states
/** Marker interface for states that have a list of tokens */
sealed interface TokensListVisibility {
/** Tokens list */
val tokens: List<TokenItemModel>
}

View file

@ -0,0 +1,113 @@
package com.tangem.tap.features.tokens.presentation.viewmodels
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.tokens.presentation.router.AddTokensRouter
import com.tangem.tap.features.tokens.presentation.states.AddTokensStateHolder
import com.tangem.tap.features.tokens.presentation.states.AddTokensToolbarState
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.store
import com.tangem.wallet.R
import dagger.hilt.android.lifecycle.HiltViewModel
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
/**
* ViewModel for add tokens screen
*
* @property router feature router
*
[REDACTED_AUTHOR]
*/
@HiltViewModel
internal class AddTokensViewModel @Inject constructor(
private val router: AddTokensRouter,
) : ViewModel(), DefaultLifecycleObserver, StoreSubscriber<TokensState> {
var uiState by mutableStateOf(getInitialUiState())
private set
init {
loadContent()
}
override fun onStop(owner: LifecycleOwner) {
store.unsubscribe(subscriber = this)
}
override fun onDestroy(owner: LifecycleOwner) {
store.dispatch(TokensAction.ResetState)
}
override fun newState(state: TokensState) {
uiState = uiState.copySealed(
toolbarState = getToolbarState(hasEditAccess = state.allowToAdd),
)
}
private fun loadContent() {
store.subscribe(subscriber = this) { state ->
state
.skipRepeats { old, new -> old.tokensState == new.tokensState }
.select(AppState::tokensState)
}
}
private fun getInitialUiState(): AddTokensStateHolder {
return AddTokensStateHolder.Loading(toolbarState = getToolbarState(store.state.tokensState.allowToAdd))
}
private fun getToolbarState(hasEditAccess: Boolean): AddTokensToolbarState {
return if (hasEditAccess) {
AddTokensToolbarState.Title.EditAccess(
titleResId = R.string.main_manage_tokens,
onBackButtonClick = router::popBackStack,
onSearchButtonClick = ::onSearchButtonClick,
onAddCustomTokenClick = router::openAddCustomTokenScreen,
)
} else {
AddTokensToolbarState.Title.ReadAccess(
titleResId = R.string.search_tokens_title,
onBackButtonClick = router::popBackStack,
onSearchButtonClick = ::onSearchButtonClick,
)
}
}
private fun onSearchButtonClick() {
uiState = when (val state = uiState.toolbarState) {
is AddTokensToolbarState.Title -> {
uiState.copySealed(
toolbarState = AddTokensToolbarState.SearchInputField(
onBackButtonClick = uiState.toolbarState.onBackButtonClick,
onSearchButtonClick = ::onSearchButtonClick,
value = "",
onValueChange = ::onValueChange,
onCleanButtonClick = { onValueChange(newValue = "") },
),
)
}
is AddTokensToolbarState.SearchInputField -> {
store.dispatch(TokensAction.SetSearchInput(state.value))
uiState.copySealed(
getToolbarState(store.state.tokensState.allowToAdd),
)
}
}
}
private fun onValueChange(newValue: String) {
val state = requireNotNull(
value = uiState.toolbarState as? AddTokensToolbarState.SearchInputField,
lazyMessage = { "Impossible to change value in another state" },
)
uiState = uiState.copySealed(toolbarState = state.copy(value = newValue))
}
}

View file

@ -21,7 +21,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.assetReader
import com.tangem.tap.common.analytics.events.ManageTokens import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
@ -32,6 +31,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.tokens.LoadAvailableCoinsService import com.tangem.tap.domain.tokens.LoadAvailableCoinsService
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
@ -85,7 +85,7 @@ class TokensMiddleware {
val loadCoinsService = LoadAvailableCoinsService( val loadCoinsService = LoadAvailableCoinsService(
tangemTechApi = store.state.domainNetworks.tangemTechService.api, tangemTechApi = store.state.domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(), dispatchers = AppCoroutineDispatcherProvider(),
assetReader = assetReader, assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader),
) )
scope.launch { scope.launch {
@ -264,14 +264,10 @@ class TokensMiddleware {
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) } val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null if (toDerive.isEmpty()) return null
return DerivationData( return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
derivations = mapKeyOfWalletPublicKey to toDerive,
)
} }
private class DerivationData( private class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) { private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionData
import com.tangem.common.extensions.isZero import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction import com.tangem.tap.features.wallet.models.PendingTransaction
@ -36,22 +37,26 @@ data class WalletData(
return exchangeManager.availableForSell(currency) return exchangeManager.availableForSell(currency)
} }
fun isAvailableToSwap(swapInteractor: SwapInteractor, isExchangeFeatureOn: Boolean): Boolean { fun isAvailableToSwap(
swapFeatureToggleManager: SwapFeatureToggleManager,
swapInteractor: SwapInteractor,
): Boolean {
if (currency.blockchain.id == Blockchain.Optimism.id && !swapFeatureToggleManager.isOptimismSwapEnabled) {
return false
}
return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) && return swapInteractor.isAvailableToSwap(currency.blockchain.toNetworkId()) &&
currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now
isExchangeFeatureOn &&
!currency.isCustomCurrency(null) !currency.isCustomCurrency(null)
} }
fun getAvailableActions( fun getAvailableActions(
swapInteractor: SwapInteractor, swapInteractor: SwapInteractor,
exchangeManager: CurrencyExchangeManager, exchangeManager: CurrencyExchangeManager,
isExchangeFeatureOn: Boolean, swapFeatureToggleManager: SwapFeatureToggleManager,
): Set<CurrencyAction> { ): Set<CurrencyAction> {
return setOfNotNull( return setOfNotNull(
if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null, if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null,
if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null, if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null,
if (isAvailableToSwap(swapInteractor, isExchangeFeatureOn)) CurrencyAction.Swap else null, if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor)) CurrencyAction.Swap else null,
) )
} }

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.MainScreen
@ -34,17 +35,17 @@ import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.redux.reducers.findSelectedCurrency import com.tangem.tap.features.wallet.redux.reducers.findSelectedCurrency
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.preferencesStorage import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager import com.tangem.tap.userWalletsListManager
import com.tangem.tap.userWalletsListManagerSafe import com.tangem.utils.coroutines.ifActive
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.Action import org.rekotlin.Action
@ -68,6 +69,15 @@ class WalletMiddleware {
) )
} }
private val networkConnectionManager: NetworkConnectionManager
get() = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
private var updateWalletStoresJob: Job? = null
set(value) {
field?.cancel()
field = value
}
val walletMiddleware: Middleware<AppState> = { _, state -> val walletMiddleware: Middleware<AppState> = { _, state ->
{ next -> { next ->
{ action -> { action ->
@ -131,16 +141,6 @@ class WalletMiddleware {
) )
} }
} }
is NetworkStateChanged -> {
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
if (!action.isOnline) return
val selectedUserWallet = userWalletsListManagerSafe?.selectedUserWalletSync.guard {
Timber.e("Unable to proceed with changed network state, no user wallet selected")
return
}
scope.launch { globalState.tapWalletManager.loadData(selectedUserWallet, refresh = true) }
}
is WalletAction.CopyAddress -> { is WalletAction.CopyAddress -> {
Analytics.send(Token.Receive.ButtonCopyAddress()) Analytics.send(Token.Receive.ButtonCopyAddress())
action.context.copyToClipboard(action.address) action.context.copyToClipboard(action.address)
@ -155,7 +155,7 @@ class WalletMiddleware {
store.dispatchOpenUrl(action.exploreUrl) store.dispatchOpenUrl(action.exploreUrl)
} }
is WalletAction.Send -> { is WalletAction.Send -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { if (!networkConnectionManager.isOnline) {
store.dispatchErrorNotification(TapError.NoInternetConnection) store.dispatchErrorNotification(TapError.NoInternetConnection)
return return
} }
@ -184,11 +184,14 @@ class WalletMiddleware {
} }
is WalletAction.UserWalletChanged -> Unit is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> { is WalletAction.WalletStoresChanged -> {
store.state.globalState.topUpController?.walletStoresChanged(action.walletStores) // Cancel update job when new wallet stores received
updateWalletStores(action.walletStores, walletState) updateWalletStoresJob = scope.launch(Dispatchers.Default) {
fetchTotalFiatBalance(action.walletStores) ifActive { updateWalletStores(action.walletStores, walletState) }
findMissedDerivations(action.walletStores) ifActive { fetchTotalFiatBalance(action.walletStores) }
tryToShowAppRatingWarning(action.walletStores) ifActive { findMissedDerivations(action.walletStores) }
ifActive { tryToShowAppRatingWarning(action.walletStores) }
ifActive { store.state.globalState.topUpController?.walletStoresChanged(action.walletStores) }
}
} }
is WalletAction.TotalFiatBalanceChanged -> Unit is WalletAction.TotalFiatBalanceChanged -> Unit
is WalletAction.PopBackToInitialScreen -> { is WalletAction.PopBackToInitialScreen -> {
@ -205,55 +208,47 @@ class WalletMiddleware {
} }
private fun updateWalletStores(walletsStores: List<WalletStoreModel>, state: WalletState) { private fun updateWalletStores(walletsStores: List<WalletStoreModel>, state: WalletState) {
scope.launch(Dispatchers.Default) { val reduxWalletStores = walletsStores.mapToReduxModels()
val reduxWalletStores = walletsStores.mapToReduxModels() if (!state.isMultiwalletAllowed) {
if (!state.isMultiwalletAllowed) { findSelectedCurrency(
findSelectedCurrency( walletsStores = reduxWalletStores,
walletsStores = reduxWalletStores, currentSelectedCurrency = null,
currentSelectedCurrency = null, isMultiWalletAllowed = false,
isMultiWalletAllowed = false, )?.let {
)?.let { store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it))
store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it))
}
} }
store.dispatchOnMain(
WalletAction.WalletStoresChanged.UpdateWalletStores(
reduxWalletStores = reduxWalletStores,
),
)
} }
store.dispatchOnMain(
WalletAction.WalletStoresChanged.UpdateWalletStores(
reduxWalletStores = reduxWalletStores,
),
)
} }
private fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>) { private suspend fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>) {
scope.launch(Dispatchers.Default) { val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(walletStores)?.mapToReduxModel()
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(walletStores)?.mapToReduxModel()
if (totalFiatBalance != null) { if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance)) store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
} }
} }
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) { private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
scope.launch(Dispatchers.Default) { val missedDerivations = wallStores
val missedDerivations = wallStores .filter { store ->
.filter { store -> store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
store.walletsData.any { it.status is WalletDataModel.MissedDerivation } }
} .map(WalletStoreModel::blockchainNetwork)
.map(WalletStoreModel::blockchainNetwork)
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations)) store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
}
} }
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) { private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
scope.launch(Dispatchers.Default) { warningsMiddleware.tryToShowAppRatingWarning(
warningsMiddleware.tryToShowAppRatingWarning( hasNonZeroWallets = walletStores
hasNonZeroWallets = walletStores .flatMap { it.walletsData }
.flatMap { it.walletsData } .any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) }, )
)
}
} }
private fun showSaveWalletIfNeeded() { private fun showSaveWalletIfNeeded() {

View file

@ -2,29 +2,26 @@ package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.SignatureCountValidator import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.FirmwareVersion import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.CardDTO import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.hasSignedHashes import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.features.demo.isDemoCard import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope import com.tangem.tap.scope
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.wallet.R import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.math.BigDecimal
class WarningsMiddleware { class WarningsMiddleware {
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) { fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
@ -75,13 +72,6 @@ class WarningsMiddleware {
} }
} }
fun tryToShowAppRatingWarning(wallet: Wallet) {
val nonZeroWalletsCount = wallet.amounts.filter {
it.value.value?.isGreaterThan(BigDecimal.ZERO) ?: false
}.size
tryToShowAppRatingWarning(hasNonZeroWallets = nonZeroWalletsCount > 0)
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) { private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
globalState?.scanResponse?.let { scanResponse -> globalState?.scanResponse?.let { scanResponse ->
val card = scanResponse.card val card = scanResponse.card
@ -147,7 +137,9 @@ class WarningsMiddleware {
private fun checkHashesCountOnline() { private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) return
val scanResponse = store.state.globalState.scanResponse val scanResponse = store.state.globalState.scanResponse
val card = scanResponse?.card val card = scanResponse?.card

View file

@ -24,8 +24,9 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.extensions.withMainContext
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.sdk.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions import com.tangem.tap.common.TestActions
import com.tangem.tap.common.analytics.events.DetailsScreen import com.tangem.tap.common.analytics.events.DetailsScreen
@ -75,6 +76,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
@Inject @Inject
lateinit var swapInteractor: SwapInteractor lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
@ -290,7 +294,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
WalletAction.DialogAction.ChooseTradeActionDialog( WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager), buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager),
sellAllowed = selectedWallet.isAvailableToSell(exchangeManager), sellAllowed = selectedWallet.isAvailableToSell(exchangeManager),
swapAllowed = selectedWallet.isAvailableToSwap(swapInteractor, isExchangeServiceFeatureOn), swapAllowed = selectedWallet.isAvailableToSwap(swapFeatureToggleManager, swapInteractor),
), ),
) )
} }
@ -298,7 +302,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
val actions = selectedWallet.getAvailableActions( val actions = selectedWallet.getAvailableActions(
swapInteractor = swapInteractor, swapInteractor = swapInteractor,
exchangeManager = exchangeManager, exchangeManager = exchangeManager,
isExchangeFeatureOn = isExchangeServiceFeatureOn, swapFeatureToggleManager = swapFeatureToggleManager,
) )
binding.rowButtons.updateButtonsVisibility( binding.rowButtons.updateButtonsVisibility(
actions = actions, actions = actions,

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.wallet.ui package com.tangem.tap.features.wallet.ui
import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -10,7 +9,6 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater import androidx.transition.TransitionInflater
@ -21,6 +19,7 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.Portfolio import com.tangem.tap.common.analytics.events.Portfolio
@ -56,6 +55,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
@Inject @Inject
lateinit var swapInteractor: SwapInteractor lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var warningsAdapter: WarningMessagesAdapter private lateinit var warningsAdapter: WarningMessagesAdapter
private val binding: FragmentWalletBinding by viewBinding(FragmentWalletBinding::bind) private val binding: FragmentWalletBinding by viewBinding(FragmentWalletBinding::bind)
@ -67,22 +69,16 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private val totalBalanceWatcher = modelWatcher { private val totalBalanceWatcher = modelWatcher {
(WalletState::totalBalance) { totalBalance -> (WalletState::totalBalance) { totalBalance ->
totalBalance?.state?.let { totalBalance?.state?.let {
viewModel.onBalanceLoaded(totalBalance)
store.state.globalState.topUpController?.totalBalanceStateChanged(it) store.state.globalState.topUpController?.totalBalanceStateChanged(it)
} }
} }
} }
override fun onAttach(context: Context) {
super.onAttach(context)
lifecycle.addObserver(viewModel)
activity?.lifecycleScope?.launchWhenCreated {
viewModel.launch()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
activity?.lifecycle?.addObserver(viewModel)
activity?.onBackPressedDispatcher?.addCallback( activity?.onBackPressedDispatcher?.addCallback(
this, this,
@ -179,6 +175,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
totalBalanceWatcher.invoke(state) totalBalanceWatcher.invoke(state)
walletView.swapInteractor = swapInteractor walletView.swapInteractor = swapInteractor
walletView.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state) walletView.onNewState(state)
@ -255,10 +252,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
return when (item.itemId) { return when (item.itemId) {
R.id.details_menu -> { R.id.details_menu -> {
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers)) store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.state.globalState.scanResponse?.let { scanNoteResponse -> store.state.globalState.scanResponse?.let { scanResponse ->
store.dispatch( store.dispatch(
DetailsAction.PrepareScreen( DetailsAction.PrepareScreen(
scanResponse = scanNoteResponse, scanResponse = scanResponse,
wallets = store.state.walletState.walletManagers.map { it.wallet }, wallets = store.state.walletState.walletManagers.map { it.wallet },
), ),
) )

View file

@ -4,34 +4,45 @@ import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.Basic import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.domain.userWalletList.UserWalletsListManager import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store import com.tangem.tap.store
import com.tangem.tap.walletStoresManager import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// TODO: Kill me, please // TODO: Kill me, please
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListManager?>, DefaultLifecycleObserver { @HiltViewModel
internal class WalletViewModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
) : ViewModel(), StoreSubscriber<UserWalletsListManager?>, DefaultLifecycleObserver {
private var observeWalletStoresUpdatesJob: Job? = null private var observeWalletStoresUpdatesJob: Job? = null
set(value) { set(value) {
field?.cancel() field?.cancel()
field = value field = value
} }
private val walletAnalyticsEventsMapper = WalletAnalyticsEventsMapper()
init { init {
subscribeToUserWalletsListManagerUpdates() subscribeToUserWalletsListManagerUpdates()
} }
@ -48,12 +59,13 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
} }
override fun onCreate(owner: LifecycleOwner) { override fun onCreate(owner: LifecycleOwner) {
launch()
val scanResponse = store.state.globalState.scanResponse val scanResponse = store.state.globalState.scanResponse
if (scanResponse != null) { if (scanResponse != null) {
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver) val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
val signInType = store.state.signInState.type val signInType = store.state.signInState.type
if (currency != null && signInType != null) { if (currency != null && signInType != null) {
Analytics.send( analyticsEventHandler.send(
Basic.SignedIn( Basic.SignedIn(
currency = currency, currency = currency,
batch = scanResponse.card.batchId, batch = scanResponse.card.batchId,
@ -65,10 +77,10 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
} }
override fun onStart(owner: LifecycleOwner) { override fun onStart(owner: LifecycleOwner) {
Analytics.send(MainScreen.ScreenOpened()) analyticsEventHandler.send(MainScreen.ScreenOpened())
} }
fun launch() { private fun launch() {
val manager = store.state.globalState.userWalletsListManager val manager = store.state.globalState.userWalletsListManager
if (manager != null) { if (manager != null) {
bootstrapSelectedWalletStoresChanges(manager) bootstrapSelectedWalletStoresChanges(manager)
@ -76,12 +88,28 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
bootstrapShowSaveWalletIfNeeded() bootstrapShowSaveWalletIfNeeded()
} }
fun onBalanceLoaded(totalBalance: TotalBalance?) {
if (totalBalance != null) {
walletAnalyticsEventsMapper.convert(totalBalance)?.let { balanceParam ->
analyticsEventHandler.send(
Basic.BalanceLoaded(
balance = balanceParam,
),
)
}
}
}
@OptIn(FlowPreview::class)
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) { private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet observeWalletStoresUpdatesJob = manager.selectedUserWallet
.map { it.walletId } .map { it.walletId }
.flatMapLatest { selectedUserWalletId -> .flatMapLatest { selectedUserWalletId ->
walletStoresManager.get(selectedUserWalletId) walletStoresManager.get(selectedUserWalletId)
} }
.debounce { walletStores ->
if (walletStores.isNotEmpty()) WALLET_STORES_DEBOUNCE_TIMEOUT else 0
}
.onEach { walletStores -> .onEach { walletStores ->
store.dispatch(WalletAction.WalletStoresChanged(walletStores)) store.dispatch(WalletAction.WalletStoresChanged(walletStores))
} }
@ -104,4 +132,8 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
.select { it.globalState.userWalletsListManager } .select { it.globalState.userWalletsListManager }
} }
} }
companion object {
private const val WALLET_STORES_DEBOUNCE_TIMEOUT = 100L
}
} }

View file

@ -0,0 +1,31 @@
package com.tangem.tap.features.wallet.ui.analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.isGreaterThan
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
class WalletAnalyticsEventsMapper : Converter<TotalBalance, AnalyticsParam.CardBalanceState?> {
override fun convert(value: TotalBalance): AnalyticsParam.CardBalanceState? {
return when (value.state) {
ProgressState.Done -> if (value.fiatAmount?.isGreaterThan(BigDecimal.ZERO) == true) {
AnalyticsParam.CardBalanceState.Full
} else {
AnalyticsParam.CardBalanceState.Empty
}
ProgressState.Error -> {
if (value.fiatAmount == null) {
// if fiatAmount is null while ProgressState.Error it means error occurs when loading blockchain
AnalyticsParam.CardBalanceState.BlockchainError
} else {
// if fiatAmount is not null while ProgressState.Error it means couldn't load custom token amount
AnalyticsParam.CardBalanceState.CustomToken
}
}
else -> null
}
}
}

View file

@ -8,7 +8,7 @@ import androidx.constraintlayout.utils.widget.ImageFilterView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.tangem.blockchain.common.DerivationStyle import com.tangem.blockchain.common.DerivationStyle
import com.tangem.tangem_sdk_new.extensions.dpToPx import com.tangem.sdk.extensions.dpToPx
import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.Currency
import com.tangem.wallet.databinding.ViewCurrencyIconBinding import com.tangem.wallet.databinding.ViewCurrencyIconBinding
import kotlin.math.roundToInt import kotlin.math.roundToInt

View file

@ -139,6 +139,7 @@ class SingleWalletView : WalletView() {
isExchangeServiceFeatureEnabled: Boolean, isExchangeServiceFeatureEnabled: Boolean,
) { ) {
val swapInteractor = this.swapInteractor ?: return val swapInteractor = this.swapInteractor ?: return
val swapFeatureToggleManager = this.swapFeatureToggleManager ?: return
val exchangeManager = store.state.globalState.exchangeManager val exchangeManager = store.state.globalState.exchangeManager
binding?.rowButtons?.apply { binding?.rowButtons?.apply {
@ -158,7 +159,7 @@ class SingleWalletView : WalletView() {
val actions = walletData.getAvailableActions( val actions = walletData.getAvailableActions(
swapInteractor = swapInteractor, swapInteractor = swapInteractor,
exchangeManager = exchangeManager, exchangeManager = exchangeManager,
isExchangeFeatureOn = isExchangeServiceFeatureEnabled, swapFeatureToggleManager = swapFeatureToggleManager,
) )
binding?.rowButtons?.updateButtonsVisibility( binding?.rowButtons?.updateButtonsVisibility(
actions = actions, actions = actions,

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.wallet.ui.wallet package com.tangem.tap.features.wallet.ui.wallet
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.WalletFragment
@ -8,6 +9,7 @@ import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView { abstract class WalletView {
var swapInteractor: SwapInteractor? = null var swapInteractor: SwapInteractor? = null
var swapFeatureToggleManager: SwapFeatureToggleManager? = null
protected var fragment: WalletFragment? = null protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null protected var binding: FragmentWalletBinding? = null

View file

@ -35,6 +35,7 @@ internal sealed interface WalletSelectorAction : Action {
data class SelectWallet( data class SelectWallet(
val userWalletId: UserWalletId, val userWalletId: UserWalletId,
val sendAnalyticsEvent: Boolean = false,
) : WalletSelectorAction ) : WalletSelectorAction
data class RenameWallet( data class RenameWallet(

View file

@ -70,7 +70,7 @@ internal class WalletSelectorMiddleware {
addWallet() addWallet()
} }
is WalletSelectorAction.SelectWallet -> { is WalletSelectorAction.SelectWallet -> {
selectWallet(action.userWalletId) selectWallet(action.userWalletId, action.sendAnalyticsEvent)
} }
is WalletSelectorAction.RemoveWallets -> { is WalletSelectorAction.RemoveWallets -> {
deleteWallets(action.userWalletsIds, state) deleteWallets(action.userWalletsIds, state)
@ -189,7 +189,7 @@ internal class WalletSelectorMiddleware {
} }
} }
private fun selectWallet(userWalletId: UserWalletId) { private fun selectWallet(userWalletId: UserWalletId, sendAnalyticsEvent: Boolean) {
scope.launch { scope.launch {
userWalletsListManager.get(userWalletId) userWalletsListManager.get(userWalletId)
.flatMap { userWallet -> .flatMap { userWallet ->
@ -212,7 +212,10 @@ internal class WalletSelectorMiddleware {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) { if (selectedUserWallet != null) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet) store.onUserWalletSelected(
userWallet = selectedUserWallet,
sendAnalyticsEvent = true,
)
} }
} }
} }
@ -336,7 +339,10 @@ internal class WalletSelectorMiddleware {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome)) store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
} }
currentSelectedWalletId != selectedUserWallet.walletId -> { currentSelectedWalletId != selectedUserWallet.walletId -> {
store.onUserWalletSelected(selectedUserWallet) store.onUserWalletSelected(
userWallet = selectedUserWallet,
sendAnalyticsEvent = true,
)
} }
} }
} }

View file

@ -32,7 +32,9 @@ import com.tangem.tap.features.walletSelector.ui.components.RenameWalletDialogCo
import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent import com.tangem.tap.features.walletSelector.ui.components.WalletSelectorScreenContent
import com.tangem.tap.features.walletSelector.ui.model.DialogModel import com.tangem.tap.features.walletSelector.ui.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() { internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
private val viewModel by viewModels<WalletSelectorViewModel>() private val viewModel by viewModels<WalletSelectorViewModel>()

View file

@ -54,7 +54,12 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
cancelWalletEditing(userWalletId) cancelWalletEditing(userWalletId)
} }
selectedUserWalletId != userWalletId -> { selectedUserWalletId != userWalletId -> {
store.dispatch(WalletSelectorAction.SelectWallet(userWalletId)) store.dispatch(
WalletSelectorAction.SelectWallet(
userWalletId = userWalletId,
sendAnalyticsEvent = true,
),
)
} }
} }
} }

View file

@ -1,71 +0,0 @@
package com.tangem.tap.network
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import org.rekotlin.Action
import org.rekotlin.Store
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
@Deprecated("Use com.tangem.datasource.connection.NetworkConnectionManager")
class NetworkConnectivity(
private val store: Store<*>,
context: Context,
) {
private val wContext: WeakReference<Context> = WeakReference(context)
private val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
}
}
init {
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION)
wContext.get()?.registerReceiver(receiver, intentFilter)
store.dispatch(NetworkStateChanged(isOnlineOrConnecting()))
}
fun isOnlineOrConnecting(): Boolean {
val connectivityManager = getConnectivityManager() ?: return false
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
capabilities != null &&
(capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET))
} else {
val networkInfo = connectivityManager.activeNetworkInfo
networkInfo != null && networkInfo.isConnectedOrConnecting
}
}
private fun getConnectivityManager(): ConnectivityManager? {
return wContext.get()?.applicationContext
?.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
}
companion object {
private lateinit var instance: NetworkConnectivity
fun createInstance(store: Store<*>, context: Context): NetworkConnectivity {
instance = NetworkConnectivity(store, context)
return instance
}
fun getInstance(): NetworkConnectivity = instance
}
}
data class NetworkStateChanged(val isOnline: Boolean) : Action

View file

@ -4,6 +4,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath import com.tangem.common.hdWallet.DerivationPath
@ -13,6 +15,7 @@ import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.lib.crypto.DerivationManager import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NonNativeToken import com.tangem.lib.crypto.models.Currency.NonNativeToken
import com.tangem.lib.crypto.models.errors.UserCancelledException
import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -49,9 +52,10 @@ class DerivationManagerImpl(
if (scanResponse != null) { if (scanResponse != null) {
deriveMissingBlockchains( deriveMissingBlockchains(
scanResponse = scanResponse, scanResponse = scanResponse,
listOf(appCurrency), currencyList = listOf(appCurrency),
onSuccess = { continuation.resumeWith(Result.success(true)) },
) { ) {
continuation.resumeWith(Result.success(true)) continuation.resumeWith(Result.failure(it))
} }
} }
} else { } else {
@ -84,6 +88,7 @@ class DerivationManagerImpl(
scanResponse: ScanResponse, scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>, currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
onSuccess: (ScanResponse) -> Unit, onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) { ) {
val derivationDataList = listOfNotNull( val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList), getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
@ -135,6 +140,7 @@ class DerivationManagerImpl(
"Error derivation", "Error derivation",
), ),
) )
onFailure.invoke(handleTangemError(result.error))
} }
else -> { else -> {
error("result result is null") error("result result is null")
@ -176,6 +182,19 @@ class DerivationManagerImpl(
) )
} }
/**
* Simple error handler
* for now specifically handle only UserCancelled
*
* @param error [TangemError]
*/
private fun handleTangemError(error: TangemError): Exception {
if (error is TangemSdkError.UserCancelled) {
return UserCancelledException()
}
return IllegalStateException(error.customMessage)
}
private class DerivationData( private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>, val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
) )

View file

@ -4,6 +4,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.Message import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager
import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.common.BlockchainSdkError
@ -155,56 +156,31 @@ class TransactionManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath) val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) { if (walletManager is EthereumWalletManager) {
val gasLimit = getGasLimit( if (walletManager is OptimismWalletManager) {
evmWalletManager = walletManager, return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = createAmount(amountToSend, currencyToSend, blockchain),
destinationAddress = destinationAddress,
data = data,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain, blockchain = blockchain,
amount = amountToSend, amountToSend = amountToSend,
currency = currencyToSend, currency = currencyToSend,
destinationAddress = destinationAddress, destinationAddress = destinationAddress,
data = data, data = data,
).let { increaseBy = increaseBy,
if (increaseBy != null && increaseBy != 0) { )
it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100"))
} else {
it
}
}
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
} else { } else {
val fee = (walletManager as? TransactionSender)?.getFee( return getFeeForBlockchain(
amount = createAmount(amountToSend, currencyToSend, blockchain), walletManager = walletManager,
destination = destinationAddress, amountToSend = amountToSend,
) ?: error("Cannot cast to TransactionSender") currency = currencyToSend,
when (fee) { blockchain = blockchain,
is Result.Success -> { destinationAddress = destinationAddress,
// for not EVM blockchains set gasLimit ZERO for now )
return ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
} }
} }
@ -217,6 +193,100 @@ class TransactionManagerImpl(
) )
} }
private suspend fun getFeeForBlockchain(
walletManager: WalletManager,
amountToSend: BigDecimal,
currency: Currency,
blockchain: Blockchain,
destinationAddress: String,
): ProxyFee {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currency, blockchain),
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
return when (fee) {
is Result.Success -> {
// for not EVM blockchains set gasLimit ZERO for now
ProxyFee(
gasLimit = BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.firstOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
@Suppress("LongParameterList")
private suspend fun getFeeForEthereumBlockchain(
walletManager: EthereumWalletManager,
blockchain: Blockchain,
amountToSend: BigDecimal,
currency: Currency,
destinationAddress: String,
data: String?,
increaseBy: Int?,
): ProxyFee {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
blockchain = blockchain,
amount = amountToSend,
currency = currency,
destinationAddress = destinationAddress,
data = data,
).let {
if (increaseBy != null && increaseBy != 0) {
it.multiply(increaseBy.toBigInteger()).divide(BigInteger("100"))
} else {
it
}
}
return when (val gasPrice = walletManager.getGasPrice()) {
is Result.Success -> {
val fee = gasLimit.multiply(gasPrice.data).toBigDecimal(
scale = blockchain.decimals(),
mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN),
)
ProxyFee(
gasLimit = gasLimit,
fee = ProxyAmount(
currencySymbol = blockchain.currency,
value = fee,
decimals = blockchain.decimals(),
),
)
}
is Result.Failure -> {
error(gasPrice.error.message ?: gasPrice.error.customMessage)
}
}
}
private suspend fun getFeeForOptimismBlockchain(
walletManager: OptimismWalletManager,
amount: Amount,
destinationAddress: String,
data: String?,
): ProxyFee {
val fee = if (data.isNullOrEmpty()) {
walletManager.getFee(amount, destinationAddress)
} else {
walletManager.getFee(amount, destinationAddress, data)
}
when (fee) {
is Result.Success -> {
return ProxyFee(
gasLimit = walletManager.gasLimit ?: BigInteger.ZERO,
fee = convertToProxyAmount(fee.data.lastOrNull() ?: error("no fee found")),
)
}
is Result.Failure -> {
error(fee.error.message ?: fee.error.customMessage)
}
}
}
@Suppress("LongParameterList") @Suppress("LongParameterList")
private suspend fun getGasLimit( private suspend fun getGasLimit(
evmWalletManager: EthereumWalletManager, evmWalletManager: EthereumWalletManager,

View file

@ -4,6 +4,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure
import com.tangem.common.extensions.guard import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toCoinId
@ -20,6 +21,8 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.userWalletsListManager import com.tangem.tap.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager import com.tangem.tap.walletCurrenciesManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber import timber.log.Timber
import java.math.BigDecimal import java.math.BigDecimal
import com.tangem.tap.features.wallet.models.Currency as WalletCurrency import com.tangem.tap.features.wallet.models.Currency as WalletCurrency
@ -117,6 +120,28 @@ class UserWalletManagerImpl(
) )
} }
override suspend fun hideAllTokens() {
val userWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("No user wallets selected")
return
}
val currencies = walletStoresManager.get(userWallet.walletId)
.firstOrNull()
?.flatMap { walletStore ->
walletStore.walletsData.map { it.currency }
}
.guard {
Timber.d("No currencies found")
return
}
walletCurrenciesManager.removeCurrencies(userWallet, currenciesToRemove = currencies)
.doOnFailure { e ->
Timber.e(e, "Unable to delete all currencies")
}
}
override fun getWalletAddress(networkId: String, derivationPath: String?): String { override fun getWalletAddress(networkId: String, derivationPath: String?): String {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath) val walletManager = getActualWalletManager(blockchain, derivationPath)

View file

@ -0,0 +1,16 @@
package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.features.tester.api.TesterRouter
import org.rekotlin.Action
sealed interface DaggerGraphAction : Action {
data class SetApplicationDependencies(
val assetReader: AssetReader,
val networkConnectionManager: NetworkConnectionManager,
) : DaggerGraphAction
data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction
}

View file

@ -0,0 +1,12 @@
package com.tangem.tap.proxy.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
object DaggerGraphMiddleware {
val daggerGraphMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action -> next(action) }
}
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.proxy.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
object DaggerGraphReducer {
fun reduce(action: Action, state: AppState): DaggerGraphState {
if (action !is DaggerGraphAction) return state.daggerGraphState
return internalReduce(action, state)
}
private fun internalReduce(action: DaggerGraphAction, state: AppState): DaggerGraphState {
return when (action) {
is DaggerGraphAction.SetApplicationDependencies -> state.daggerGraphState.copy(
assetReader = action.assetReader,
networkConnectionManager = action.networkConnectionManager,
)
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
testerRouter = action.testerRouter,
)
}
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.features.tester.api.TesterRouter
import org.rekotlin.StateType
data class DaggerGraphState(
val assetReader: AssetReader? = null,
val testerRouter: TesterRouter? = null,
val networkConnectionManager: NetworkConnectionManager? = null,
) : StateType {
inline fun <reified T> get(getDependency: DaggerGraphState.() -> T?): T {
return requireNotNull(getDependency()) {
"${T::class.simpleName} isn't initialized "
}
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:fillColor="#EBEBEB"
android:pathData="M11,0L11,0A11,11 0,0 1,22 11L22,11A11,11 0,0 1,11 22L11,22A11,11 0,0 1,0 11L0,11A11,11 0,0 1,11 0z" />
<path
android:fillColor="#C9C9C9"
android:pathData="M13.659,5.35L11.564,5.663L12.174,9.721L7.787,6.34L6.5,8.022L10.343,10.975L6.5,13.944L7.787,15.626L12.174,12.245L11.564,16.303L13.659,16.616L14.5,10.975L13.659,5.35Z" />
</vector>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">Tangem</string>
</resources>

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