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/screenshots
**/fastlane/test_output
/buildSrc/build/

View file

@ -1,117 +1,20 @@
plugins {
id("com.android.application")
kotlin("android")
kotlin("kapt")
kotlin("plugin.serialization")
id("com.google.gms.google-services")
id("com.google.firebase.crashlytics")
id("com.google.dagger.hilt.android")
}
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")
alias(deps.plugins.android.application)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.kotlin.serialization)
alias(deps.plugins.google.services)
alias(deps.plugins.hilt.android)
alias(deps.plugins.firebase.crashlytics)
id("configuration")
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar"))))
implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(project(":domain"))
implementation(project(":common"))
implementation(project(":core:analytics"))
implementation(project(":core:featuretoggles"))
implementation(project(":core:res"))
implementation(project(":core:ui"))
implementation(project(":core:datasource"))
@ -123,91 +26,93 @@ dependencies {
implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data"))
implementation(project(":features:swap:api"))
implementation(project(":features:swap:presentation"))
implementation(project(":features:swap:domain"))
implementation(project(":features:swap:data"))
implementation(project(":features:tester:api"))
implementation(project(":features:tester:impl"))
/** AndroidX libraries */
implementation(AndroidX.coreKtx)
implementation(AndroidX.appCompat)
implementation(AndroidX.fragmentKtx)
implementation(AndroidX.constraintLayout)
implementation(AndroidX.browser)
implementation(AndroidX.lifecycleRuntimeKtx)
implementation(AndroidX.lifecycleCommonJava8)
implementation(AndroidX.lifecycleViewModelKtx)
implementation(AndroidX.lifecycleLiveDataKtx)
implementation(AndroidX.activityCompose)
implementation(deps.androidx.core.ktx)
implementation(deps.androidx.appCompat)
implementation(deps.androidx.fragment.ktx)
implementation(deps.androidx.constraintLayout)
implementation(deps.androidx.activity.compose)
implementation(deps.androidx.browser)
implementation(deps.lifecycle.runtime.ktx)
implementation(deps.lifecycle.common.java8)
implementation(deps.lifecycle.viewModel.ktx)
/** Compose libraries */
implementation(Compose.material)
implementation(Compose.animation)
implementation(Compose.foundation)
implementation(Compose.ui)
implementation(Compose.uiTooling)
implementation(Compose.coil)
implementation(deps.compose.material)
implementation(deps.compose.animation)
implementation(deps.compose.foundation)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.coil)
implementation(deps.compose.shimmer)
/** Firebase libraries */
implementation(platform(Firebase.bom))
implementation(Firebase.firebaseAnalytics)
implementation(Firebase.firebaseCrashlytics)
implementation(platform(deps.firebase.bom))
implementation(deps.firebase.analytics)
implementation(deps.firebase.crashlytics)
/** Tangem libraries */
implementation(Tangem.blockchain) {
implementation(deps.tangem.blockchain) {
exclude(module = "joda-time")
}
implementation(Tangem.cardCore)
implementation(Tangem.cardAndroid) {
implementation(deps.tangem.card.core)
implementation(deps.tangem.card.android) {
exclude(module = "joda-time")
}
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Other libraries */
implementation(Library.materialComponent)
implementation(Library.googlePlayCore)
implementation(Library.googlePlayCoreKtx)
coreLibraryDesugaring(Library.desugarJdkLibs)
implementation(Library.timber)
implementation(Library.reKotlin)
implementation(Library.zxingQrCore)
implementation(Library.zxingQrBarcodeScanner)
implementation(Library.otaliastudiosCameraView)
implementation(Library.coil)
implementation(Library.appsflyer)
implementation(Library.amplitude)
implementation(Library.kotsonGsonExt)
implementation(deps.material)
implementation(deps.googlePlay.core)
implementation(deps.googlePlay.core.ktx)
implementation(deps.googlePlay.services.wallet)
coreLibraryDesugaring(deps.desugar)
implementation(deps.timber)
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.zxing.qrBarcodeScanner)
implementation(deps.otaliastudiosCameraView)
implementation(deps.coil)
implementation(deps.appsflyer)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
//TODO: refactoring: remove it when all network services moved to the datasource module
implementation(Library.retrofit)
implementation(Library.retrofitMoshiConverter)
implementation(Library.moshi)
implementation(Library.moshiKotlin)
implementation(Library.okHttp)
implementation(Library.okHttpLogging)
implementation(Library.zendeskChat)
implementation(Library.zendeskMessaging)
implementation(Library.spongecastleCryptoCore)
implementation(Library.lottie)
implementation(Library.shopifyBuySdk) {
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)
implementation(deps.okHttp.logging)
implementation(deps.zendesk.chat)
implementation(deps.zendesk.messaging)
implementation(deps.spongecastle.core)
implementation(deps.lottie)
implementation(deps.shopify.buy) {
exclude(group = "com.shopify.graphql.support")
exclude(module = "joda-time")
}
implementation(Library.accompanistAppCompatTheme)
implementation(Library.accompanistSystemUiController)
implementation(Library.accompanistWebView)
implementation(Library.xmlShimmer)
implementation(Library.viewBindingDelegate)
implementation(Library.armadillo)
implementation(Library.googlePlayServicesWallet)
implementation(Library.composeShimmer)
implementation(Library.mviCoreWatcher)
implementation(Library.kotlinSerialization)
implementation(deps.compose.accompanist.appCompatTheme)
implementation(deps.compose.accompanist.systemUiController)
implementation(deps.compose.accompanist.webView)
implementation(deps.xmlShimmer)
implementation(deps.viewBindingDelegate)
implementation(deps.armadillo)
implementation(deps.mviCore.watcher)
implementation(deps.kotlin.serialization)
/** Testing libraries */
testImplementation(Test.junit)
testImplementation(Test.truth)
androidTestImplementation(Test.junitAndroidExt)
androidTestImplementation(Test.espresso)
testImplementation(deps.test.junit)
testImplementation(deps.test.truth)
androidTestImplementation(deps.test.junit.android)
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"?>
<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",
"symbol" : "TCA",

View file

@ -10,9 +10,10 @@ import androidx.core.view.WindowInsetsControllerCompat
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
import com.tangem.features.tester.api.TesterRouter
import com.tangem.operations.backup.BackupService
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tangem_sdk_new.extensions.initWithBiometrics
import com.tangem.sdk.extensions.init
import com.tangem.sdk.extensions.initWithBiometrics
import com.tangem.tap.common.ActivityResultCallbackHolder
import com.tangem.tap.common.DialogManager
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.welcome.redux.WelcomeAction
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
@ -71,6 +73,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var appStateHolder: AppStateHolder
/** Router for opening tester menu */
@Inject
lateinit var testerRouter: TesterRouter
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -97,6 +103,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
GooglePayService(createPaymentsClient(this), this),
),
)
store.dispatch(DaggerGraphAction.SetActivityDependencies(testerRouter))
}
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.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
import com.tangem.datasource.utils.AndroidAssetReader
import com.tangem.datasource.utils.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.DomainLayer
import com.tangem.domain.common.LogConfig
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.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.launch
import okhttp3.logging.HttpLoggingInterceptor
import org.rekotlin.Store
import timber.log.Timber
@ -65,7 +67,6 @@ lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var assetReader: AssetReader
lateinit var userTokensRepository: UserTokensRepository
private val walletStoresRepository by lazy { WalletStoresRepository.provideDefaultImplementation() }
@ -116,6 +117,15 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var configManager: ConfigManager
@Inject
lateinit var assetReader: AssetReader
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
override fun onCreate() {
super.onCreate()
@ -136,15 +146,15 @@ class TapApplication : Application(), ImageLoaderFactory {
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
DomainLayer.init()
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
walletConnectRepository = WalletConnectRepository(this)
assetReader = AndroidAssetReader(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
initConfigManager(configLoader, ::initWithConfigDependency)
initWarningMessagesManager()
loadNativeLibraries()
if (LogConfig.network.blockchainSdkNetwork) {
BlockchainSdkRetrofitBuilder.interceptors = listOf(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY },
@ -154,10 +164,23 @@ class TapApplication : Application(), ImageLoaderFactory {
userTokensRepository = UserTokensRepository.init(
context = this,
tangemTechService = store.state.domainNetworks.tangemTechService,
networkConnectionManager = networkConnectionManager,
)
appStateHolder.mainStore = store
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
store.dispatch(
action = DaggerGraphAction.SetApplicationDependencies(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
),
)
scope.launch {
featureTogglesManager.init()
}
initTopUpController()
}
@ -180,6 +203,10 @@ class TapApplication : Application(), ImageLoaderFactory {
)
}
private fun loadNativeLibraries() {
System.loadLibrary("TrustWalletCore")
}
private fun initConfigManager(loader: FeaturesLocalLoader, onComplete: (Config) -> Unit) {
configManager.load(loader) { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
@ -217,6 +244,7 @@ class TapApplication : Application(), ImageLoaderFactory {
) {
fun initAdditionalFeedbackInfo(context: Context): AdditionalFeedbackInfo = AdditionalFeedbackInfo().apply {
appVersion = try {
// TODO don't use deprecated method
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {

View file

@ -119,6 +119,8 @@ object TangemSdkErrorMapper {
is TangemSdkError.UserCanceledBiometricsAuthentication -> error
is TangemSdkError.EncryptionOperationFailed -> 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) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
object CustomToken : CardBalanceState("Custom token")
object BlockchainError : CardBalanceState("Blockchain error")
companion object
}
@ -79,6 +81,7 @@ sealed class AnalyticsParam {
companion object Key {
const val Source = "Source"
const val Balance = "Balance"
const val Batch = "Batch"
const val ProductType = "Product Type"
const val Firmware = "Firmware"

View file

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

View file

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

View file

@ -71,16 +71,16 @@ sealed class Onboarding(
class ButtonSetPinCode : Onboarding("Onboarding", "Button - Set PIN Code")
class CardConnectionScreenOpened : Onboarding("Onboarding", "Card Connection Screen Opened")
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 KYCStarted : Onboarding("Onboarding", "KYC started")
class KYCInProgress : Onboarding("Onboarding", "KYC in progress")
class KYCRejected : Onboarding("Onboarding", "KYC rejected")
class KYCStarted : Onboarding("Onboarding", "KYC Started")
class KYCInProgress : Onboarding("Onboarding", "KYC In Progress")
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 ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim Was Successfully")
class ButtonChat : Onboarding("Onboarding", "Button - Chat")

View file

@ -60,16 +60,16 @@ sealed class Settings(
params: Map<String, String> = mapOf(),
) : 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",
params = mapOf("State" to state.value),
)
class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : CardSettings(
class SaveAccessCodeSwitcherChanged(state: AnalyticsParam.OnOffState) : AppSettings(
event = "Save Access Code Switcher Changed",
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(
category = "Token",
event = "Token bought",
event = "Token Bought",
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.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp
import com.tangem.sdk.extensions.dpToPx
import com.tangem.sdk.extensions.pxToDp
/**
[REDACTED_AUTHOR]

View file

@ -36,6 +36,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Kusama -> R.drawable.ic_kusama_no_color
Blockchain.Optimism, Blockchain.OptimismTestnet -> R.drawable.ic_optimism_no_color
Blockchain.Dash -> R.drawable.ic_dash_no_color
Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color
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.shop.ui.ShopFragment
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.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment

View file

@ -42,8 +42,12 @@ fun Store<*>.dispatchNotification(resId: Int) {
dispatchOnMain(GlobalAction.ShowNotification(resId))
}
suspend fun Store<AppState>.onUserWalletSelected(userWallet: UserWallet, refresh: Boolean = false) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh)
suspend fun Store<AppState>.onUserWalletSelected(
userWallet: UserWallet,
refresh: Boolean = false,
sendAnalyticsEvent: Boolean = false,
) {
state.globalState.tapWalletManager.onWalletSelected(userWallet, refresh, sendAnalyticsEvent)
}
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.redux.AddressData
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.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.delay
import timber.log.Timber
@ -47,7 +47,8 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
} catch (exception: Exception) {
Timber.e(exception)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
Result.Failure(TapError.NoInternetConnection)
} else {
val blockchain = wallet.blockchain
@ -87,8 +88,6 @@ fun WalletManager?.getAddressData(): AddressData? {
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.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.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding

View file

@ -3,7 +3,7 @@ package com.tangem.tap.common.recyclerView
import android.graphics.Rect
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.sdk.extensions.dpToPx
class SpaceItemDecoration(
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.welcome.redux.WelcomeReducer
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphReducer
import org.rekotlin.Action
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),
sprinklrState = SprinklrReducer.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.welcome.redux.WelcomeMiddleware
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 org.rekotlin.Middleware
import org.rekotlin.StateType
@ -66,7 +68,8 @@ data class AppState(
val saveWalletState: SaveWalletState = SaveWalletState(),
val walletSelectorState: WalletSelectorState = WalletSelectorState(),
val sprinklrState: SprinklrState = SprinklrState(),
val signInState: SignInState = SignInState()
val signInState: SignInState = SignInState(),
val daggerGraphState: DaggerGraphState = DaggerGraphState(),
) : StateType {
val domainState: DomainState
@ -108,7 +111,8 @@ data class AppState(
LockUserWalletsTimerMiddleware().middleware,
AccessCodeRequestPolicyMiddleware().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.extensions.withMainContext
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.setContext
import com.tangem.tap.common.redux.global.GlobalAction
@ -36,8 +37,11 @@ class TapWalletManager {
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)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
}
val scanResponse = userWallet.scanResponse
val card = scanResponse.card
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.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.datasource.utils.AssetReader
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
@ -60,7 +60,7 @@ class LoadAvailableCoinsService(
return withContext(dispatchers.io) {
runCatching {
tangemTechApi.getCoins(
networkIds = supportedBlockchains.toSet().map(Blockchain::toNetworkId).joinToString(","),
networkIds = supportedBlockchains.toSet().joinToString(",", transform = Blockchain::toNetworkId),
active = true,
searchText = searchInput,
offset = offset,
@ -74,8 +74,8 @@ class LoadAvailableCoinsService(
}
}
fun getTestnetCoins(): List<Currency> {
val json = assetReader.readAssetAsString(FILE_NAME_TESTNET_COINS)
private fun getTestnetCoins(): List<Currency> {
val json = assetReader.readJson(FILE_NAME_TESTNET_COINS)
return currenciesAdapter.fromJson(json)!!.coins
.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.TangemTechService
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.AndroidFileReader
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.toBlockchainNetworks
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.CoroutineDispatcherProvider
import kotlinx.coroutines.withContext
@ -25,33 +25,31 @@ class UserTokensRepository(
private val storageService: UserTokensStorageService,
private val tangemTechApi: TangemTechApi,
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) {
val userId = getUserWalletId(card) ?: return@withContext emptyList()
val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext loadTokensOffline(card, userId).ifEmpty(::loadDemoCurrencies)
return@withContext loadTokensOffline(userWalletId = userWalletId).ifEmpty(::loadDemoCurrencies)
}
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
return@withContext loadTokensOffline(card, userId)
}
if (!networkConnectionManager.isOnline) return@withContext loadTokensOffline(userWalletId = userWalletId)
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) {
val userId = getUserWalletId(card) ?: return@withContext
val userWalletId = getUserWalletId(card) ?: return@withContext
val userTokens = tokens.toUserTokensResponse()
remoteSaveUserTokens(userId, userTokens)
storageService.saveUserTokens(userId, userTokens)
remoteSaveUserTokens(userWalletId = userWalletId, userTokens = userTokens)
storageService.saveUserTokens(userWalletId = userWalletId, tokens = userTokens)
}
suspend fun loadBlockchainsToDerive(card: CardDTO): List<BlockchainNetwork> = withContext(dispatchers.io) {
val userId = getUserWalletId(card) ?: return@withContext emptyList()
val blockchainNetworks = loadTokensOffline(card = card, userId = userId).toBlockchainNetworks()
val userWalletId = getUserWalletId(card) ?: return@withContext emptyList()
val blockchainNetworks = loadTokensOffline(userWalletId = userWalletId).toBlockchainNetworks()
if (DemoHelper.isDemoCardId(card.cardId)) {
return@withContext blockchainNetworks.ifEmpty(loadDemoCurrencies()::toBlockchainNetworks)
@ -60,8 +58,8 @@ class UserTokensRepository(
return@withContext blockchainNetworks
}
private suspend fun loadTokensOffline(card: CardDTO, userId: String): List<Currency> {
return storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
private fun loadTokensOffline(userWalletId: String): List<Currency> {
return storageService.getUserTokens(userWalletId = userWalletId) ?: emptyList()
}
private fun loadDemoCurrencies(): List<Currency> {
@ -76,54 +74,49 @@ class UserTokensRepository(
.flatMap(BlockchainNetwork::toCurrencies)
}
private fun List<Currency>.toUserTokensResponse() = UserTokensResponse(
tokens = CurrencyConverter.convertList(this),
group = GROUP_DEFAULT_VALUE,
sort = SORT_DEFAULT_VALUE,
)
private fun List<Currency>.toUserTokensResponse(): UserTokensResponse {
return UserTokensResponse(
tokens = CurrencyConverter.convertList(input = this),
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 {
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) ->
storageService.getUserTokens(card).also {
remoteSaveUserTokens(userId = userId, userTokens = it.toUserTokensResponse())
}
error is TangemSdkError.NetworkError && error.customMessage.contains(NOT_FOUND_HTTP_CODE) -> {
storageService
.getUserTokens(userWalletId)
?.also { remoteSaveUserTokens(userWalletId = userWalletId, userTokens = it.toUserTokensResponse()) }
?: emptyList()
}
else -> {
val tokens = storageService.getUserTokens(userId) ?: storageService.getUserTokens(card)
tokens.distinct()
storageService.getUserTokens(userWalletId)?.distinct() ?: emptyList()
}
}
}
private suspend fun remoteGetUserTokens(card: CardDTO, userId: String): List<Currency> {
runCatching {
tangemTechApi.getUserTokens(userId)
}.onSuccess { response ->
return response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse).also {
storageService.saveUserTokens(userId, it.toUserTokensResponse())
}
.distinct()
}.onFailure {
return handleGetUserTokensFailure(card = card, userId = userId, error = it)
}
error("Unreachable code because runCatching must return result")
private suspend fun remoteGetUserTokens(userWalletId: String): List<Currency> {
return runCatching { tangemTechApi.getUserTokens(userWalletId) }
.fold(
onSuccess = { response ->
response.tokens
.mapNotNull(Currency.Companion::fromTokenResponse)
.also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) }
.distinct()
},
onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },
)
}
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
// if the /user-tokens endpoint disabled
runCatching {
tangemTechApi.saveUserTokens(userId, userTokens)
}.onFailure {
Timber.e(it)
}
runCatching { tangemTechApi.saveUserTokens(userWalletId, userTokens) }
.onFailure { Timber.e(it) }
}
private fun getUserWalletId(card: CardDTO): String? {
return UserWalletIdBuilder.card(card).build()
?.stringValue
}
private fun getUserWalletId(card: CardDTO): String? = UserWalletIdBuilder.card(card).build()?.stringValue
companion object {
private const val GROUP_DEFAULT_VALUE = "none"
@ -131,24 +124,16 @@ class UserTokensRepository(
private const val NOT_FOUND_HTTP_CODE = "404"
// TODO("After adding DI") get dependencies by DI
fun init(context: Context, tangemTechService: TangemTechService): UserTokensRepository {
val fileReader = AndroidFileReader(context)
val dispatchers = AppCoroutineDispatcherProvider()
val oldUserTokensRepository = OldUserTokensRepository(
fileReader = fileReader,
tangemTechApi = tangemTechService.api,
dispatchers = dispatchers,
)
val storageService = UserTokensStorageService(
oldUserTokensRepository = oldUserTokensRepository,
fileReader = fileReader,
)
fun init(
context: Context,
tangemTechService: TangemTechService,
networkConnectionManager: NetworkConnectionManager,
): UserTokensRepository {
return UserTokensRepository(
storageService = storageService,
storageService = UserTokensStorageService(fileReader = AndroidFileReader(context)),
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.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.FileReader
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.toCurrencies
class UserTokensStorageService(
private val oldUserTokensRepository: OldUserTokensRepository,
private val fileReader: FileReader,
) {
class UserTokensStorageService(private val fileReader: FileReader) {
private val userTokensAdapter: JsonAdapter<UserTokensResponse> =
MoshiConverter.networkMoshi.adapter(UserTokensResponse::class.java)
fun getUserTokens(userId: String): List<Currency>? {
fun getUserTokens(userWalletId: String): List<Currency>? {
return try {
val json = fileReader.readFile(getFileNameForUserTokens(userId))
val json = fileReader.readFile(getFileNameForUserTokens(userWalletId))
userTokensAdapter.fromJson(json)?.tokens?.mapNotNull { Currency.fromTokenResponse(it) }
} catch (exception: Exception) {
Log.error { exception.stackTraceToString() }
@ -26,16 +21,9 @@ class UserTokensStorageService(
}
}
@Deprecated("")
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) {
fun saveUserTokens(userWalletId: String, tokens: UserTokensResponse) {
val json = userTokensAdapter.toJson(tokens)
fileReader.rewriteFile(json, getFileNameForUserTokens(userId))
fileReader.rewriteFile(json, getFileNameForUserTokens(userWalletId))
}
companion object {

View file

@ -9,10 +9,10 @@ import com.tangem.common.KeyPair
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.datasource.utils.AssetReader
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(
@ -89,7 +89,7 @@ class TwinCardsManager(
}
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)!!
}
}

View file

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

View file

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

View file

@ -61,6 +61,10 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet,
currenciesToAdd: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
if (currenciesToAdd.isEmpty()) {
return@withContext CompletionResult.Success(Unit)
}
val card = userWallet.scanResponse.card
val currenciesToAddWithMissingBlockchains = currenciesToAdd.addMissingBlockchainsIfNeeded(card)
listeners.forEach { it.willCurrenciesAdd(userWallet, currenciesToAddWithMissingBlockchains) }
@ -86,6 +90,10 @@ internal class DefaultWalletCurrenciesManager(
userWallet: UserWallet,
currenciesToRemove: List<Currency>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
if (currenciesToRemove.isEmpty()) {
return@withContext CompletionResult.Success(Unit)
}
listeners.forEach { it.willCurrenciesRemove(userWallet, currenciesToRemove) }
val card = userWallet.scanResponse.card
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.filterByCoin
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -62,8 +63,6 @@ internal class DefaultWalletAmountsRepository(
private val tangemTechApi: TangemTechApi,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletAmountsRepository {
private val walletStoresStorage = WalletStoresStorage
private val walletManagersStorage = WalletManagerStorage
override suspend fun updateAmountsForUserWallets(
userWallets: List<UserWallet>,
@ -76,8 +75,7 @@ internal class DefaultWalletAmountsRepository(
awaitAll(
async { fetchAmountsForUserWallets(userWallets) },
async { fetchFiatRates(userWallets, walletStores = null, fiatCurrency) },
)
.fold()
).fold()
}
}
}
@ -104,8 +102,7 @@ internal class DefaultWalletAmountsRepository(
awaitAll(
async { fetchAmountForWalletStores(userWalletId, scanResponse, walletStores) },
async { fetchFiatRates(listOf(userWallet), walletStores, fiatCurrency) },
)
.fold()
).fold()
}
}
}
@ -123,41 +120,41 @@ internal class DefaultWalletAmountsRepository(
walletStores: List<WalletStoreModel>?,
fiatCurrency: FiatCurrency,
): CompletionResult<Unit> {
// FIXME: Use NetworkConnectionManager when it is added to DI
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
return CompletionResult.Failure(WalletStoresError.NoInternetConnection)
}
val walletStoresInternal = walletStores ?: getWalletStores(userWallets)
val currencies = walletStoresInternal
.asSequence()
.flatMap { it.walletsData }
.map { it.currency }
val currencies = walletStoresInternal.asSequence().flatMap { it.walletsData }.map { it.currency }
val coinsIds = currencies.mapNotNull { it.coinId }.distinct().toList()
return withContext(dispatchers.io) {
runCatching { tangemTechApi.getRates(fiatCurrency.code.lowercase(), coinsIds.joinToString(",")) }
.onSuccess {
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
return@withContext CompletionResult.Success(Unit)
}
.onFailure {
val error = WalletStoresError.FetchFiatRatesError(
currencies = currencies.map(Currency::currencySymbol).toList(),
cause = it,
)
runCatching {
tangemTechApi.getRates(
fiatCurrency.code.lowercase(),
coinsIds.joinToString(","),
)
}.onSuccess {
updateWalletStoresWithFiatRates(walletStores = walletStoresInternal, fiatRates = it.rates)
return@withContext CompletionResult.Success(Unit)
}.onFailure {
val error = WalletStoresError.FetchFiatRatesError(
currencies = currencies.map(Currency::currencySymbol).toList(),
cause = it,
)
Timber.e(
error,
"""
Timber.e(
error,
"""
Unable to fetch fiat rates
|- Coins ids: $coinsIds
""".trimIndent(),
)
""".trimIndent(),
)
return@withContext CompletionResult.Failure(error)
}
return@withContext CompletionResult.Failure(error)
}
error("Unreachable code because runCatching must return result")
}
@ -166,9 +163,7 @@ internal class DefaultWalletAmountsRepository(
private suspend fun fetchAmountsForUserWallets(
userWallets: List<UserWallet>,
): CompletionResult<Unit> = withContext(Dispatchers.Default) {
userWallets.map { async { fetchAmountsForUserWallet(it) } }
.awaitAll()
.fold()
userWallets.map { async { fetchAmountsForUserWallet(it) } }.awaitAll().fold()
}
private suspend fun fetchAmountsForUserWallet(
@ -186,8 +181,8 @@ internal class DefaultWalletAmountsRepository(
scanResponse: ScanResponse,
walletStores: List<WalletStoreModel>,
): CompletionResult<Unit> = coroutineScope {
// FIXME: Use NetworkConnectionManager when it is added to DI
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
val networkConnectionManager = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
if (!networkConnectionManager.isOnline) {
walletStores.forEach {
updateWalletStoreWithUnreachable(it)
}
@ -200,9 +195,7 @@ internal class DefaultWalletAmountsRepository(
val walletManager = walletStore.walletManager
fetchAmountsForWalletStore(userWalletId, scanResponse, walletStore, walletManager)
}
}
.awaitAll()
.fold()
}.awaitAll().fold()
}
private suspend fun fetchAmountsForWalletStore(
@ -223,24 +216,25 @@ internal class DefaultWalletAmountsRepository(
updateWalletStoreWithUnreachable(walletStore)
}
else -> {
updateWalletManager(scanResponse, walletManager)
.map { updateWalletManagerInStorage(userWalletId, walletManager) }
.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}
.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }
.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
updateWalletManager(scanResponse, walletManager).map {
updateWalletManagerInStorage(
userWalletId,
walletManager,
)
}.flatMap {
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }.flatMapOnFailure { error ->
updateWalletStoreWithError(
walletStore = walletStore,
wallet = walletManager.wallet,
error = error,
)
}
}
}
}
@ -267,8 +261,7 @@ internal class DefaultWalletAmountsRepository(
walletStore: WalletStoreModel,
walletManager: WalletManager,
): CompletionResult<Unit> {
val rentProvider = walletManager as? RentProvider
?: return CompletionResult.Success(Unit)
val rentProvider = walletManager as? RentProvider ?: return CompletionResult.Success(Unit)
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is Success -> {
@ -318,7 +311,7 @@ internal class DefaultWalletAmountsRepository(
)
if (error is BlockchainSdkError) {
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
@ -350,7 +343,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(),
)
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
@ -378,7 +371,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(),
)
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
@ -402,7 +395,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(),
)
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
@ -425,7 +418,7 @@ internal class DefaultWalletAmountsRepository(
""".trimIndent(),
)
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStores(
walletStoresToUpdate = walletStores,
update = {
@ -450,7 +443,7 @@ internal class DefaultWalletAmountsRepository(
)
if (rent != walletStore.walletRent) {
walletStoresStorage.update { prevState ->
WalletStoresStorage.update { prevState ->
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
@ -465,14 +458,12 @@ internal class DefaultWalletAmountsRepository(
userWalletId: UserWalletId,
walletManager: WalletManager,
) = withContext(Dispatchers.Default) {
walletManagersStorage.update { prevManagers ->
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty()
.toMutableList()
.apply {
replaceByOrAdd(walletManager) {
it.wallet.blockchain == walletManager.wallet.blockchain
}
WalletManagerStorage.update { prevManagers ->
val newManagersForUserWallet = prevManagers[userWalletId].orEmpty().toMutableList().apply {
replaceByOrAdd(walletManager) {
it.wallet.blockchain == walletManager.wallet.blockchain
}
}
prevManagers.apply {
set(userWalletId, newManagersForUserWallet)
@ -481,13 +472,8 @@ internal class DefaultWalletAmountsRepository(
}
private suspend fun getWalletStores(userWallets: List<UserWallet>): List<WalletStoreModel> {
return userWallets
.map { it.walletId }
.flatMap { userWalletId ->
walletStoresStorage.getAll()
.firstOrNull()
?.get(userWalletId)
.orEmpty()
}
return userWallets.map { it.walletId }.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
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
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.common.CompletionResult
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.redux.AppState
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.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
/**
[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 {
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.domain.common.ScanResponse
import com.tangem.domain.common.demo.DemoConfig
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
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) {
when (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.ReCreateTwinsWallet -> {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
@ -157,15 +157,15 @@ class DetailsMiddleware {
class ManageSecurityMiddleware {
@Suppress("ComplexMethod")
fun handle(action: DetailsAction.ManageSecurity) {
fun handle(action: DetailsAction.ManageSecurity, detailsState: DetailsState) {
when (action) {
is DetailsAction.ManageSecurity.OpenSecurity -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsSecurity))
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardId = store.state.detailsState.scanResponse?.card?.cardId
val selectedOption =
store.state.detailsState.cardSettingsState?.manageSecurityState?.selectedOption
val cardSettingsState = detailsState.cardSettingsState
val cardId = cardSettingsState?.card?.cardId
val selectedOption = cardSettingsState?.manageSecurityState?.selectedOption
scope.launch {
val result = when (selectedOption) {
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.tap.common.entities.Button
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 java.util.*
import kotlin.properties.ReadOnlyProperty
data class DetailsState(
val scanResponse: ScanResponse? = null,
@ -19,14 +16,7 @@ data class DetailsState(
val createBackupAllowed: Boolean = false,
val appCurrency: FiatCurrency = FiatCurrency.Default,
val appSettingsState: AppSettingsState = AppSettingsState(),
) : 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
}
}
) : StateType
data class CardInfo(
val cardId: String,

View file

@ -4,8 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
@ -21,9 +19,6 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
private val detailsViewModel = DetailsViewModel(store)
private var detailsScreenState: MutableState<DetailsScreenState> =
mutableStateOf(detailsViewModel.updateState(store.state.detailsState))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Settings.ScreenOpened())
@ -42,7 +37,7 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
isTransitionGroup = true
TangemTheme {
DetailsScreen(
state = detailsScreenState.value,
state = detailsViewModel.detailsScreenState.value,
onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
)
}
@ -66,6 +61,6 @@ class DetailsFragment : Fragment(), StoreSubscriber<DetailsState> {
override fun newState(state: DetailsState) {
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.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -16,8 +18,13 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
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.Modifier
import androidx.compose.ui.res.colorResource
@ -27,10 +34,12 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect
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.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
import kotlinx.coroutines.launch
@Composable
fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
@ -46,32 +55,35 @@ fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit) {
@Composable
fun Content(state: DetailsScreenState) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
state.elements.map { element ->
if (element == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
} else {
DetailsItem(
item = element,
appCurrency = state.appCurrency,
onItemsClick = { state.onItemsClick(element) },
)
Box {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
ScreenTitle(titleRes = R.string.details_title, Modifier.padding(bottom = 52.dp))
state.elements.map { element ->
if (element == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(onItemsClick = state.onItemsClick)
} else {
DetailsItem(
item = 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))
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),
)
ShowSnackbarIfNeeded(state.showErrorSnackbar.value)
}
}
@ -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
@Preview
private fun Preview() {

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.wallet.R
@Immutable
@ -11,6 +13,7 @@ data class DetailsScreenState(
val appCurrency: String,
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
val showErrorSnackbar: MutableState<EventError> = mutableStateOf(EventError.Empty),
) {
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),
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
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
@ -38,6 +42,11 @@ data class SocialNetworkLink(
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) {
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram)
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter)

View file

@ -1,5 +1,7 @@
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.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail
@ -18,6 +20,9 @@ import org.rekotlin.Store
class DetailsViewModel(private val store: Store<AppState>) {
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
private set
@Suppress("ComplexMethod")
fun updateState(state: DetailsState): DetailsScreenState {
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.AppCurrency -> if (cardTypesResolver?.isMultiwalletAllowed() != 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
}
}
@ -96,6 +102,9 @@ class DetailsViewModel(private val store: Store<AppState>) {
SettingsElement.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.blockchain.common.WalletManager
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.tap.domain.TapError
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.core.analytics.Analytics
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.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.extensions.beginDelayedTransition
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.wallet.R
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
@Suppress("LargeClass")
@AndroidEntryPoint
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
@Inject
lateinit var assetReader: AssetReader
private val mainBinding by lazy { binding.vMain }
private var previousStep: TwinCardsStep = TwinCardsStep.None
@ -253,8 +259,8 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet())
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
AndroidAssetReader(requireContext()),
initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
reader = assetReader,
),
)
}

View file

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

View file

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

View file

@ -155,6 +155,12 @@ sealed class SendAction : SendScreenAction {
val reduceAmount: BigDecimal,
) : Dialog()
data class KaspaWarningDialog(
val maxOutputs: Int,
val maxAmount: BigDecimal,
val onOk: () -> Unit,
) : Dialog()
sealed class SendTransactionFails : Dialog() {
data class CardSdkError(val error: TangemSdkError) : 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) {
val potentialPayId = data.toLowerCase()
val potentialPayId = data.lowercase()
if (isPayIdEnabled() && PayIdManager.isPayId(potentialPayId)) {
dispatch(SetPayIdWalletAddress(potentialPayId, "", isUserInput))
} else {
@ -113,10 +113,11 @@ internal class AddressPayIdMiddleware {
}
private fun verifyAddress(address: String, wallet: Wallet, isUserInput: Boolean, dispatch: (Action) -> Unit) {
val addressSchemeSplit = if (wallet.blockchain == Blockchain.BitcoinCash) {
listOf(address)
} else {
address.split(":")
val addressSchemeSplit = when (wallet.blockchain) {
Blockchain.BitcoinCash,
Blockchain.Kaspa,
-> listOf(address)
else -> address.split(":")
}
val noSchemeAddress = when (addressSchemeSplit.size) {
1 -> address // no scheme

View file

@ -269,6 +269,18 @@ private fun sendTransaction(
// from XLM, XRP, Polkadot
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 -> {
when {
error.customMessage.contains(DemoTransactionSender.ID) -> {

View file

@ -55,6 +55,7 @@ private class SendReducer : SendInternalReducer {
sendState.copy(sendButtonState = IndeterminateProgressButton(action.state))
}
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.BlockchainSdkError -> 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.tangem.Message
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.analytics.events.Token
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.BlockchainSdkError
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.feedback.SendTransactionFailedEmail
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.ui.FeeUiHelper
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.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
@ -124,6 +125,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
dialog?.show()
}
}
is SendAction.Dialog.KaspaWarningDialog -> {
if (dialog == null) {
dialog = KaspaWarningDialog.create(fg.requireContext(), state.dialog)
dialog?.show()
}
}
is SendAction.Dialog.SendTransactionFails.CardSdkError -> {
if (dialog == null) {
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.view.Menu
@ -31,6 +31,7 @@ import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentAddTokensBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@ -40,7 +41,8 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
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 var tokensState: MutableState<TokensState> = mutableStateOf(store.state.tokensState)
@ -155,18 +157,18 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc
tokensState.value = state
binding.toolbar.title = getString(toolbarTitle)
}
}
fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
private fun SearchView.inputtedTextAsFlow(): Flow<String> = callbackFlow {
val watcher = setOnQueryTextListener(
object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean = false
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
awaitClose { watcher }
override fun onQueryTextChange(newText: String?): Boolean {
trySend(newText ?: "")
return false
}
},
)
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.operations.derivation.ExtendedPublicKeysMap
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.extensions.dispatchDebugErrorNotification
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.tokens.LoadAvailableCoinsService
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -85,7 +85,7 @@ class TokensMiddleware {
val loadCoinsService = LoadAvailableCoinsService(
tangemTechApi = store.state.domainNetworks.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
assetReader = assetReader,
assetReader = store.state.daggerGraphState.get(DaggerGraphState::assetReader),
)
scope.launch {
@ -264,14 +264,10 @@ class TokensMiddleware {
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return DerivationData(
derivations = mapKeyOfWalletPublicKey to toDerive,
)
return DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private class DerivationData(
val derivations: Pair<ByteArrayKey, List<DerivationPath>>,
)
private class DerivationData(val derivations: Pair<ByteArrayKey, List<DerivationPath>>)
private fun submitAdd(scanResponse: ScanResponse, currencyList: List<Currency>) {
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.common.extensions.isZero
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
@ -36,22 +37,26 @@ data class WalletData(
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()) &&
currency.blockchain.id != Blockchain.Optimism.id && // disable optimism blockchain for now
isExchangeFeatureOn &&
!currency.isCustomCurrency(null)
}
fun getAvailableActions(
swapInteractor: SwapInteractor,
exchangeManager: CurrencyExchangeManager,
isExchangeFeatureOn: Boolean,
swapFeatureToggleManager: SwapFeatureToggleManager,
): Set<CurrencyAction> {
return setOfNotNull(
if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy 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.extensions.guard
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.Basic
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.WalletStore
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.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.utils.coroutines.ifActive
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
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 ->
{ next ->
{ 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 -> {
Analytics.send(Token.Receive.ButtonCopyAddress())
action.context.copyToClipboard(action.address)
@ -155,7 +155,7 @@ class WalletMiddleware {
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
if (!networkConnectionManager.isOnline) {
store.dispatchErrorNotification(TapError.NoInternetConnection)
return
}
@ -184,11 +184,14 @@ class WalletMiddleware {
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
store.state.globalState.topUpController?.walletStoresChanged(action.walletStores)
updateWalletStores(action.walletStores, walletState)
fetchTotalFiatBalance(action.walletStores)
findMissedDerivations(action.walletStores)
tryToShowAppRatingWarning(action.walletStores)
// Cancel update job when new wallet stores received
updateWalletStoresJob = scope.launch(Dispatchers.Default) {
ifActive { updateWalletStores(action.walletStores, walletState) }
ifActive { fetchTotalFiatBalance(action.walletStores) }
ifActive { findMissedDerivations(action.walletStores) }
ifActive { tryToShowAppRatingWarning(action.walletStores) }
ifActive { store.state.globalState.topUpController?.walletStoresChanged(action.walletStores) }
}
}
is WalletAction.TotalFiatBalanceChanged -> Unit
is WalletAction.PopBackToInitialScreen -> {
@ -205,55 +208,47 @@ class WalletMiddleware {
}
private fun updateWalletStores(walletsStores: List<WalletStoreModel>, state: WalletState) {
scope.launch(Dispatchers.Default) {
val reduxWalletStores = walletsStores.mapToReduxModels()
if (!state.isMultiwalletAllowed) {
findSelectedCurrency(
walletsStores = reduxWalletStores,
currentSelectedCurrency = null,
isMultiWalletAllowed = false,
)?.let {
store.dispatchOnMain(WalletAction.MultiWallet.SetSingleWalletCurrency(it))
}
val reduxWalletStores = walletsStores.mapToReduxModels()
if (!state.isMultiwalletAllowed) {
findSelectedCurrency(
walletsStores = reduxWalletStores,
currentSelectedCurrency = null,
isMultiWalletAllowed = false,
)?.let {
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>) {
scope.launch(Dispatchers.Default) {
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(walletStores)?.mapToReduxModel()
private suspend fun fetchTotalFiatBalance(walletStores: List<WalletStoreModel>) {
val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull(walletStores)?.mapToReduxModel()
if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
if (totalFiatBalance != null) {
store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance))
}
}
private fun findMissedDerivations(wallStores: List<WalletStoreModel>) {
scope.launch(Dispatchers.Default) {
val missedDerivations = wallStores
.filter { store ->
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
}
.map(WalletStoreModel::blockchainNetwork)
val missedDerivations = wallStores
.filter { store ->
store.walletsData.any { it.status is WalletDataModel.MissedDerivation }
}
.map(WalletStoreModel::blockchainNetwork)
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
}
store.dispatchOnMain(WalletAction.MultiWallet.AddMissingDerivations(missedDerivations))
}
private fun tryToShowAppRatingWarning(walletStores: List<WalletStoreModel>) {
scope.launch(Dispatchers.Default) {
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
}
warningsMiddleware.tryToShowAppRatingWarning(
hasNonZeroWallets = walletStores
.flatMap { it.walletsData }
.any { it.status.amount.isGreaterThan(BigDecimal.ZERO) },
)
}
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.SignatureCountValidator
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
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.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class WarningsMiddleware {
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?) {
globalState?.scanResponse?.let { scanResponse ->
val card = scanResponse.card
@ -147,7 +137,9 @@ class WarningsMiddleware {
private fun checkHashesCountOnline() {
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 card = scanResponse?.card

View file

@ -24,8 +24,9 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.feature.swap.api.SwapFeatureToggleManager
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.TestActions
import com.tangem.tap.common.analytics.events.DetailsScreen
@ -75,6 +76,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
@Inject
lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter
private lateinit var warningMessagesAdapter: WalletDetailWarningMessagesAdapter
@ -290,7 +294,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy(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(
swapInteractor = swapInteractor,
exchangeManager = exchangeManager,
isExchangeFeatureOn = isExchangeServiceFeatureOn,
swapFeatureToggleManager = swapFeatureToggleManager,
)
binding.rowButtons.updateButtonsVisibility(
actions = actions,

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.wallet.ui
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
@ -10,7 +9,6 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
@ -21,6 +19,7 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.events.Portfolio
@ -56,6 +55,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
@Inject
lateinit var swapInteractor: SwapInteractor
@Inject
lateinit var swapFeatureToggleManager: SwapFeatureToggleManager
private lateinit var warningsAdapter: WarningMessagesAdapter
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 {
(WalletState::totalBalance) { totalBalance ->
totalBalance?.state?.let {
viewModel.onBalanceLoaded(totalBalance)
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?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.lifecycle?.addObserver(viewModel)
activity?.onBackPressedDispatcher?.addCallback(
this,
@ -179,6 +175,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
totalBalanceWatcher.invoke(state)
walletView.swapInteractor = swapInteractor
walletView.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state)
@ -255,10 +252,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
return when (item.itemId) {
R.id.details_menu -> {
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.state.globalState.scanResponse?.let { scanNoteResponse ->
store.state.globalState.scanResponse?.let { scanResponse ->
store.dispatch(
DetailsAction.PrepareScreen(
scanResponse = scanNoteResponse,
scanResponse = scanResponse,
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.ViewModel
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.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.extensions.dispatchOnMain
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.ui.analytics.WalletAnalyticsEventsMapper
import com.tangem.tap.store
import com.tangem.tap.walletStoresManager
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
import javax.inject.Inject
// TODO: Kill me, please
@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
set(value) {
field?.cancel()
field = value
}
private val walletAnalyticsEventsMapper = WalletAnalyticsEventsMapper()
init {
subscribeToUserWalletsListManagerUpdates()
}
@ -48,12 +59,13 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
}
override fun onCreate(owner: LifecycleOwner) {
launch()
val scanResponse = store.state.globalState.scanResponse
if (scanResponse != null) {
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
val signInType = store.state.signInState.type
if (currency != null && signInType != null) {
Analytics.send(
analyticsEventHandler.send(
Basic.SignedIn(
currency = currency,
batch = scanResponse.card.batchId,
@ -65,10 +77,10 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
}
override fun onStart(owner: LifecycleOwner) {
Analytics.send(MainScreen.ScreenOpened())
analyticsEventHandler.send(MainScreen.ScreenOpened())
}
fun launch() {
private fun launch() {
val manager = store.state.globalState.userWalletsListManager
if (manager != null) {
bootstrapSelectedWalletStoresChanges(manager)
@ -76,12 +88,28 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
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) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet
.map { it.walletId }
.flatMapLatest { selectedUserWalletId ->
walletStoresManager.get(selectedUserWalletId)
}
.debounce { walletStores ->
if (walletStores.isNotEmpty()) WALLET_STORES_DEBOUNCE_TIMEOUT else 0
}
.onEach { walletStores ->
store.dispatch(WalletAction.WalletStoresChanged(walletStores))
}
@ -104,4 +132,8 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
.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.core.view.isVisible
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.wallet.databinding.ViewCurrencyIconBinding
import kotlin.math.roundToInt

View file

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

View file

@ -1,5 +1,6 @@
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.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -8,6 +9,7 @@ import com.tangem.wallet.databinding.FragmentWalletBinding
abstract class WalletView {
var swapInteractor: SwapInteractor? = null
var swapFeatureToggleManager: SwapFeatureToggleManager? = null
protected var fragment: WalletFragment? = null
protected var binding: FragmentWalletBinding? = null

View file

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

View file

@ -70,7 +70,7 @@ internal class WalletSelectorMiddleware {
addWallet()
}
is WalletSelectorAction.SelectWallet -> {
selectWallet(action.userWalletId)
selectWallet(action.userWalletId, action.sendAnalyticsEvent)
}
is WalletSelectorAction.RemoveWallets -> {
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 {
userWalletsListManager.get(userWalletId)
.flatMap { userWallet ->
@ -212,7 +212,10 @@ internal class WalletSelectorMiddleware {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
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))
}
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.model.DialogModel
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<WalletSelectorScreenState>() {
private val viewModel by viewModels<WalletSelectorViewModel>()

View file

@ -54,7 +54,12 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
cancelWalletEditing(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.common.CompletionResult
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.toMapKey
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.models.Currency
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.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
@ -49,9 +52,10 @@ class DerivationManagerImpl(
if (scanResponse != null) {
deriveMissingBlockchains(
scanResponse = scanResponse,
listOf(appCurrency),
currencyList = listOf(appCurrency),
onSuccess = { continuation.resumeWith(Result.success(true)) },
) {
continuation.resumeWith(Result.success(true))
continuation.resumeWith(Result.failure(it))
}
}
} else {
@ -84,6 +88,7 @@ class DerivationManagerImpl(
scanResponse: ScanResponse,
currencyList: List<com.tangem.tap.features.wallet.models.Currency>,
onSuccess: (ScanResponse) -> Unit,
onFailure: (Exception) -> Unit,
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
@ -135,6 +140,7 @@ class DerivationManagerImpl(
"Error derivation",
),
)
onFailure.invoke(handleTangemError(result.error))
}
else -> {
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(
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.blockchain.blockchains.ethereum.EthereumTransactionExtras
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.Blockchain
import com.tangem.blockchain.common.BlockchainSdkError
@ -155,56 +156,31 @@ class TransactionManagerImpl(
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
val walletManager = getActualWalletManager(blockchain, derivationPath)
if (walletManager is EthereumWalletManager) {
val gasLimit = getGasLimit(
evmWalletManager = walletManager,
if (walletManager is OptimismWalletManager) {
return getFeeForOptimismBlockchain(
walletManager = walletManager,
amount = createAmount(amountToSend, currencyToSend, blockchain),
destinationAddress = destinationAddress,
data = data,
)
}
return getFeeForEthereumBlockchain(
walletManager = walletManager,
blockchain = blockchain,
amount = amountToSend,
amountToSend = amountToSend,
currency = currencyToSend,
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)
}
}
increaseBy = increaseBy,
)
} else {
val fee = (walletManager as? TransactionSender)?.getFee(
amount = createAmount(amountToSend, currencyToSend, blockchain),
destination = destinationAddress,
) ?: error("Cannot cast to TransactionSender")
when (fee) {
is Result.Success -> {
// 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)
}
}
return getFeeForBlockchain(
walletManager = walletManager,
amountToSend = amountToSend,
currency = currencyToSend,
blockchain = blockchain,
destinationAddress = destinationAddress,
)
}
}
@ -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")
private suspend fun getGasLimit(
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.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.doOnFailure
import com.tangem.common.extensions.guard
import com.tangem.domain.common.extensions.fromNetworkId
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.userWalletsListManager
import com.tangem.tap.walletCurrenciesManager
import com.tangem.tap.walletStoresManager
import kotlinx.coroutines.flow.firstOrNull
import timber.log.Timber
import java.math.BigDecimal
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 {
val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" }
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