Updated on 2026-08-14

This commit is contained in:
Tangem 2023-03-30 10:15:48 +03:00
commit 0625a92045
199 changed files with 3829 additions and 2255 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 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name">DebugTangem</string>
<string name="tangem_app_name" translatable="false">DebugTangem</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">ExternalTangem</string>
</resources>

View file

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

View file

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

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="tangem_app_name" translatable="false">InternalTangem</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

@ -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,11 +146,9 @@ 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()
@ -154,10 +162,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()
}
@ -217,6 +238,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

@ -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

@ -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

@ -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

@ -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

@ -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
@ -8,6 +10,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
@ -18,6 +21,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 +45,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
}
}
@ -94,7 +101,18 @@ class DetailsViewModel(private val store: Store<AppState>) {
// TODO: To be available later
}
SettingsElement.ReferralProgram -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ReferralProgram))
if (store.state.detailsState.scanResponse?.let { DemoHelper.isDemoCard(it) } == true) {
detailsScreenState.value.showErrorSnackbar.value = EventError.DemoReferralNotAvailable(
onErrorShow = {
detailsScreenState.value.showErrorSnackbar.value = EventError.Empty
},
)
} else {
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,27 @@
package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
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) { _, _ ->
store.dispatch(SendAction.Dialog.Hide)
dialog.onOk()
}
}
.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

@ -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,15 +35,14 @@ 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.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@ -68,6 +68,9 @@ class WalletMiddleware {
)
}
private val networkConnectionManager: NetworkConnectionManager
get() = store.state.daggerGraphState.get(DaggerGraphState::networkConnectionManager)
val walletMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
@ -131,16 +134,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 +148,7 @@ class WalletMiddleware {
store.dispatchOpenUrl(action.exploreUrl)
}
is WalletAction.Send -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
if (!networkConnectionManager.isOnline) {
store.dispatchErrorNotification(TapError.NoInternetConnection)
return
}

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

@ -21,6 +21,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 +57,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,6 +71,7 @@ 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)
}
}
@ -75,7 +80,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onAttach(context: Context) {
super.onAttach(context)
lifecycle.addObserver(viewModel)
activity?.lifecycleScope?.launchWhenCreated {
lifecycleScope.launchWhenCreated {
viewModel.launch()
}
}
@ -179,6 +184,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
totalBalanceWatcher.invoke(state)
walletView.swapInteractor = swapInteractor
walletView.swapFeatureToggleManager = swapFeatureToggleManager
walletView.onNewState(state)
@ -255,10 +261,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,15 +4,18 @@ 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.Job
import kotlinx.coroutines.delay
@ -22,16 +25,22 @@ 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()
}
@ -53,7 +62,7 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
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,7 +74,7 @@ internal class WalletViewModel : ViewModel(), StoreSubscriber<UserWalletsListMan
}
override fun onStart(owner: LifecycleOwner) {
Analytics.send(MainScreen.ScreenOpened())
analyticsEventHandler.send(MainScreen.ScreenOpened())
}
fun launch() {
@ -76,6 +85,18 @@ 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,
),
)
}
}
}
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet
.map { it.walletId }

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

@ -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

@ -1,13 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<certificates src="system" />
</trust-anchors>
</debug-overrides>
<domain-config>
<domain includeSubdomains="true">api.tangem.com</domain>
<trust-anchors>
@ -17,5 +9,4 @@
</domain-config>
<base-config cleartextTrafficPermitted="false" />
</network-security-config>

View file

@ -1,37 +1,13 @@
buildscript {
repositories {
google()
mavenCentral()
maven(url = "https://maven.fabric.io/public")
}
dependencies {
classpath(ClasspathDependency.AndroidGradlePlugin)
classpath(ClasspathDependency.KotlinGradlePlugin)
classpath(ClasspathDependency.AndroidMavenGradlePlugin)
classpath(ClasspathDependency.GoogleServices)
classpath(ClasspathDependency.GoogleFirebaseCrashlytics)
}
}
plugins {
id("com.google.dagger.hilt.android") version "2.44" apply false
kotlin("plugin.serialization") version Versions.kotlin apply false
}
allprojects {
repositories {
google()
jcenter() // unable to replace with mavenCentral() due to rekotlin and com.otaliastudios:cameraview
mavenLocal()
maven("https://nexus.tangem-tech.com/repository/maven-releases/")
maven("https://jitpack.io")
maven("https://zendesk.jfrog.io/zendesk/repo")
}
}
subprojects {
apply(plugin = "detekt-convention")
alias(deps.plugins.kotlin.android) apply false
alias(deps.plugins.kotlin.jvm) apply false
alias(deps.plugins.kotlin.serialization) apply false
alias(deps.plugins.kotlin.kapt) apply false
alias(deps.plugins.android.application) apply false
alias(deps.plugins.android.library) apply false
alias(deps.plugins.hilt.android) apply false
alias(deps.plugins.google.services) apply false
alias(deps.plugins.firebase.crashlytics) apply false
}
tasks.register("clean", Delete::class) {

View file

@ -1,16 +0,0 @@
plugins {
`kotlin-dsl`
}
repositories {
google()
mavenCentral()
}
dependencies {
implementation("com.android.tools.build:gradle:7.1.3")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10")
//TODO("Upgrade version to 1.22.0 after upgrading kotlin version to 1.7.21")
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0")
implementation("com.squareup:javapoet:1.13.0")
}

View file

@ -1,9 +0,0 @@
object ClasspathDependency {
const val AndroidGradlePlugin = "com.android.tools.build:gradle:" + Versions.androidGradlePlugin
const val AndroidMavenGradlePlugin = "com.github.dcendents:android-maven-gradle-plugin:" +
Versions.androidMavenGradlePlugin
const val GoogleFirebaseCrashlytics = "com.google.firebase:firebase-crashlytics-gradle:" +
Versions.googleFirebaseCrashlytics
const val GoogleServices = "com.google.gms:google-services:" + Versions.googleServices
const val KotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:" + Versions.kotlin
}

View file

@ -1,26 +0,0 @@
typealias ConfigFieldBuilder = (String, String, String) -> Unit
class BuildConfigFieldFactory(
private val fields: List<Field>,
private val builder: ConfigFieldBuilder,
) {
fun create() {
fields.forEach { field -> builder(field.type, field.name, field.value) }
}
}
/**
* Build config field
*
* @property type field type
* @property name field name
* @property value field value
*/
sealed class Field(val type: String, val name: String, val value: String) {
class Environment(value: String) : Field("String", "ENVIRONMENT", "\"$value\"")
class TestActionEnabled(isEnabled: Boolean) : Field("Boolean","TEST_ACTION_ENABLED", isEnabled.toString())
class LogEnabled(isEnabled: Boolean) : Field("Boolean","LOG_ENABLED", isEnabled.toString())
}

View file

@ -1,91 +0,0 @@
object AndroidX {
const val activityCompose = "androidx.activity:activity-compose:" + Versions.androidxActivityCompose
const val appCompat = "androidx.appcompat:appcompat:" + Versions.androidxAppCompat
const val browser = "androidx.browser:browser:" + Versions.androidxBrowser
const val constraintLayout = "androidx.constraintlayout:constraintlayout:" + Versions.androidxConstraintLayout
const val coreKtx = "androidx.core:core-ktx:" + Versions.androidxCore
const val fragmentKtx = "androidx.fragment:fragment-ktx:" + Versions.androidxFragment
const val lifecycleCommonJava8 = "androidx.lifecycle:lifecycle-common-java8:" + Versions.androidxLifecycle
const val lifecycleLiveDataKtx = "androidx.lifecycle:lifecycle-livedata-ktx:" + Versions.androidxLifecycle
const val lifecycleRuntimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:" + Versions.androidxLifecycle
const val lifecycleViewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:" + Versions.androidxLifecycle
}
object Compose {
const val animation = "androidx.compose.animation:animation:" + Versions.compose
const val coil = "io.coil-kt:coil-compose:" + Versions.coil
const val foundation = "androidx.compose.foundation:foundation:" + Versions.composeFoundation
const val material = "androidx.compose.material:material:" + Versions.compose
const val ui = "androidx.compose.ui:ui:" + Versions.compose
const val uiTooling = "androidx.compose.ui:ui-tooling:" + Versions.compose
const val constraintLayout = "androidx.constraintlayout:constraintlayout-compose:" + Versions.composeConstraint
}
object Firebase {
const val bom = "com.google.firebase:firebase-bom:" + Versions.firebase
const val firebaseAnalytics = "com.google.firebase:firebase-analytics-ktx"
const val firebaseCrashlytics = "com.google.firebase:firebase-crashlytics-ktx"
}
object Library {
const val accompanistAppCompatTheme = "com.google.accompanist:accompanist-appcompat-theme:" + Versions.accompanist
const val accompanistSystemUiController =
"com.google.accompanist:accompanist-systemuicontroller:" + Versions.accompanist
const val accompanistWebView = "com.google.accompanist:accompanist-webview:" + Versions.accompanist
const val amplitude = "com.amplitude:android-sdk:" + Versions.amplitude
const val appsflyer = "com.appsflyer:af-android-sdk:" + Versions.appsflyer
const val armadillo = "at.favre.lib:armadillo:" + Versions.armadillo
const val coil = "io.coil-kt:coil:" + Versions.coil
const val composeShimmer = "com.valentinilk.shimmer:compose-shimmer:" + Versions.composeShimmer
const val coroutine = "org.jetbrains.kotlinx:kotlinx-coroutines-core:" + Versions.coroutine
const val desugarJdkLibs = "com.android.tools:desugar_jdk_libs:" + Versions.desugarJdkLibs
const val googlePlayCore = "com.google.android.play:core:" + Versions.googlePlayCore
const val googlePlayCoreKtx = "com.google.android.play:core-ktx:" + Versions.googlePlayCoreKtx
const val googlePlayServicesWallet =
"com.google.android.gms:play-services-wallet:" + Versions.googlePlayServicesWallet
const val hilt = "com.google.dagger:hilt-android:" + Versions.hilt
const val hiltCore = "com.google.dagger:hilt-core:" + Versions.hilt
const val hiltKapt = "com.google.dagger:hilt-compiler:" + Versions.hilt
const val jodatime = "joda-time:joda-time:" + Versions.jodatime
const val kotsonGsonExt = "com.github.salomonbrys.kotson:kotson:" + Versions.kotsonGsonExt
const val lottie = "com.airbnb.android:lottie:" + Versions.lottie
const val materialComponent = "com.google.android.material:material:" + Versions.googleMaterialComponent
const val moshi = "com.squareup.moshi:moshi:" + Versions.moshi
const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:" + Versions.moshi
const val okHttp = "com.squareup.okhttp3:okhttp:" + Versions.okhttp
const val okHttpLogging = "com.squareup.okhttp3:logging-interceptor:" + Versions.okhttp
const val otaliastudiosCameraView = "com.otaliastudios:cameraview:" + Versions.otaliastudiosCameraView
const val shopifyBuySdk = "com.shopify.mobilebuysdk:buy3:" + Versions.shopifyBuySdk
const val spongecastleCryptoCore = "com.madgag.spongycastle:core:" + Versions.spongycastleCryptoCore
const val reKotlin = "org.rekotlin:rekotlin:" + Versions.rekotlin
const val retrofit = "com.squareup.retrofit2:retrofit:" + Versions.retrofit
const val retrofitMoshiConverter = "com.squareup.retrofit2:converter-moshi:" + Versions.retrofitMoshiConverter
const val timber = "com.jakewharton.timber:timber:" + Versions.timber
const val viewBindingDelegate =
"com.github.kirich1409:viewbindingpropertydelegate-noreflection:" + Versions.viewBindingDelegate
const val xmlShimmer = "com.github.skydoves:androidveil:" + Versions.xmlShimmer
const val zendeskChat = "com.zendesk:chat:" + Versions.zendeskChat
const val zendeskMessaging = "com.zendesk:messaging:" + Versions.zendeskMessaging
const val zxingQrBarcodeScanner = "me.dm7.barcodescanner:zxing:" + Versions.zxingQrBarcodeScanner
const val zxingQrCore = "com.google.zxing:core:" + Versions.zxingQrCode
const val mviCoreWatcher = "com.github.badoo.mvicore:mvicore-diff:" + Versions.mviCore
const val kotlinSerialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:" + Versions.kotlinSerialization
}
object Tangem {
const val blockchain = "com.tangem:blockchain:" + Versions.tangemBlockchainSdk
const val cardAndroid = "com.tangem.tangem-sdk-kotlin:android:" + Versions.tangemCardSdk
const val cardCore = "com.tangem.tangem-sdk-kotlin:core:" + Versions.tangemCardSdk
}
object Tools {
const val composeDetektRules = "ru.kode:detekt-rules-compose:" + Versions.composeDetektRules
const val formattingDetektRules = "io.gitlab.arturbosch.detekt:detekt-formatting:" + Versions.formattingDetektRules
}
object Test {
const val espresso = "androidx.test.espresso:espresso-core:" + Versions.espresso
const val junit = "junit:junit:" + Versions.junit
const val junitAndroidExt = "androidx.test.ext:junit:" + Versions.junitAndroidExt
const val truth = "com.google.truth:truth:" + Versions.truth
}

View file

@ -1,82 +0,0 @@
object Versions {
// region Classpath
const val androidGradlePlugin = "7.1.3"
const val androidMavenGradlePlugin = "2.1"
const val googleFirebaseCrashlytics = "2.7.1"
const val googleServices = "4.3.10"
const val kotlin = "1.7.10"
// endregion Classpath
// region AndroidX
const val androidxActivityCompose = "1.5.0"
const val androidxAppCompat = "1.5.1"
const val androidxBrowser = "1.4.0"
const val androidxConstraintLayout = "2.1.4"
const val androidxCore = "1.9.0"
const val androidxFragment = "1.5.3"
const val androidxLifecycle = "2.5.1"
// endregion AndroidX
// region Compose
const val compose = "1.3.1"
const val composeFoundation = "1.3.1"
const val composeConstraint = "1.0.1"
// endregion Compose
// region Other libraries
const val accompanist = "0.28.0"
const val amplitude = "2.36.1"
const val appsflyer = "6.5.1"
const val armadillo = "0.9.0"
const val coil = "2.1.0"
const val composeShimmer = "1.0.3"
const val coroutine = "1.5.2"
const val desugarJdkLibs = "1.1.5"
const val firebase = "26.0.0"
const val googleMaterialComponent = "1.6.1"
const val googlePlayCore = "1.10.3"
const val googlePlayCoreKtx = "1.8.1"
const val googlePlayServicesWallet = "19.1.0"
const val hilt = "2.44"
const val jodatime = "2.12.1"
const val kotsonGsonExt = "2.5.0"
const val lottie = "3.4.0"
const val moshi = "1.12.0"
const val okhttp = "4.9.3"
const val otaliastudiosCameraView = "2.6.3"
const val rekotlin = "1.0.4"
const val retrofit = "2.8.1"
const val retrofitMoshiConverter = "2.6.0"
const val shopifyBuySdk = "12.0.0"
const val spongycastleCryptoCore = "1.58.0.0"
const val timber = "4.7.1"
const val viewBindingDelegate = "1.5.6"
const val xmlShimmer = "1.1.3"
const val zendeskChat = "3.3.5"
const val zendeskMessaging = "5.2.4"
const val zxingQrBarcodeScanner = "1.9.8"
const val zxingQrCode = "3.3.3"
const val mviCore = "1.3.1"
const val kotlinSerialization = "1.4.1"
// endregion Other libraries
// region Tangem
const val tangemBlockchainSdk = "develop-174"
// const val tangemBlockchainSdk = "0.0.1" // Keep it! - used for local builds
const val tangemCardSdk = "develop-200"
// const val tangemCardSdk = "0.0.1" // Keep it! - used for local builds
// endregion Tangem
// region Tools
const val composeDetektRules = "1.2.2"
const val formattingDetektRules = "1.21.0"
// endregion Tools
// region Testing
const val espresso = "3.4.0"
const val junit = "4.13.2"
const val truth = "1.1.3"
const val junitAndroidExt = "1.1.3"
// endregion Testing
}

View file

@ -1,33 +0,0 @@
import io.gitlab.arturbosch.detekt.Detekt
plugins {
id("io.gitlab.arturbosch.detekt")
}
detekt {
parallel = true
ignoreFailures = false
autoCorrect = true
buildUponDefaultConfig = true
config.setFrom(rootProject.files("config/detekt/detekt.yml"))
}
tasks.withType<Detekt> {
include("**/*.kt")
exclude("**/resources/**", "**/build/**")
reports {
sarif {
required.set(false)
}
txt {
required.set(true)
}
}
jvmTarget = "1.8"
}
dependencies {
detektPlugins(Tools.composeDetektRules)
detektPlugins(Tools.formattingDetektRules)
}

View file

@ -1,9 +1,4 @@
plugins {
id("java-library")
id("org.jetbrains.kotlin.jvm")
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
alias(deps.plugins.kotlin.jvm)
id("configuration")
}

View file

@ -1,47 +1,16 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
android {
defaultConfig {
compileSdk = AppConfig.compileSdkVersion
minSdk = AppConfig.minSdkVersion
targetSdk = AppConfig.targetSdkVersion
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = false
}
buildTypes {
create("debug_beta") {
initWith(getByName("release"))
BuildConfigFieldFactory(
fields = listOf(
Field.Environment("release"),
Field.TestActionEnabled(true),
Field.LogEnabled(true),
),
builder = ::buildConfigField,
).create()
}
}
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
dependencies {
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
implementation(project(":core:utils"))

View file

@ -1,40 +1,9 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
}
android {
defaultConfig {
compileSdk = AppConfig.compileSdkVersion
minSdk = AppConfig.minSdkVersion
targetSdk = AppConfig.targetSdkVersion
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
isCoreLibraryDesugaringEnabled = false
}
buildTypes {
create("debug_beta") {
initWith(getByName("release"))
BuildConfigFieldFactory(
fields = listOf(
Field.Environment("release"),
Field.TestActionEnabled(true),
Field.LogEnabled(true),
),
builder = ::buildConfigField,
).create()
}
}
alias(deps.plugins.android.library)
alias(deps.plugins.kotlin.android)
alias(deps.plugins.kotlin.kapt)
alias(deps.plugins.hilt.android)
id("configuration")
}
dependencies {
@ -44,30 +13,31 @@ dependencies {
implementation(project(":libs:auth"))
/** Tangem libraries */
implementation(Tangem.blockchain)
implementation(Tangem.cardCore)
implementation(deps.tangem.blockchain)
implementation(deps.tangem.card.core)
/** DI */
implementation(Library.hilt)
kapt(Library.hiltKapt)
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
/** Coroutines */
implementation(Library.coroutine)
implementation(deps.kotlin.coroutines)
/** Logging */
implementation(Library.timber)
implementation(deps.timber)
/** Network */
implementation(Library.retrofit)
implementation(Library.retrofitMoshiConverter)
implementation(Library.moshi)
implementation(Library.moshiKotlin)
implementation(Library.okHttp)
implementation(Library.okHttpLogging)
implementation(deps.krateSharedPref)
implementation(deps.moshi)
implementation(deps.moshi.kotlin)
implementation(deps.okHttp)
implementation(deps.okHttp.logging)
implementation(deps.retrofit)
implementation(deps.retrofit.moshi)
/** Time */
implementation(Library.jodatime)
implementation(deps.jodatime)
/** Security */
implementation(Library.spongecastleCryptoCore)
implementation(deps.spongecastle.core)
}

View file

@ -0,0 +1,23 @@
package com.tangem.datasource.asset
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
import javax.inject.Inject
/**
* Implementation of asset file reader
*
* @property context application context
*/
internal class AndroidAssetReader @Inject constructor(
@ApplicationContext private val context: Context,
) : AssetReader {
override fun readJson(fileName: String): String {
return context.assets
.open("$fileName.json")
.bufferedReader()
.use(BufferedReader::readText)
}
}

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.asset
/**
* Asset file reader
*
[REDACTED_AUTHOR]
*/
interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
}

View file

@ -102,6 +102,7 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager {
saltPayAuthToken = configValues.saltPay.credentials.basicAuthToken,
nowNodeCredentials = NowNodeCredentials(configValues.nowNodesApiKey),
getBlockCredentials = GetBlockCredentials(configValues.getBlockApiKey),
kaspaSecondaryApiUrl = configValues.kaspaSecondaryApiUrl,
),
appsFlyerDevKey = configValues.appsFlyer.appsFlyerDevKey,
amplitudeApiKey = configValues.amplitudeApiKey,

View file

@ -2,10 +2,10 @@ package com.tangem.datasource.config
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.models.ConfigModel
import com.tangem.datasource.config.models.ConfigValueModel
import com.tangem.datasource.config.models.FeatureModel
import com.tangem.datasource.utils.AssetReader
import timber.log.Timber
/**
@ -25,8 +25,8 @@ class FeaturesLocalLoader(
val featureAdapter: JsonAdapter<FeatureModel> = moshi.adapter(FeatureModel::class.java)
val valuesAdapter: JsonAdapter<ConfigValueModel> = moshi.adapter(ConfigValueModel::class.java)
val jsonFeatures = assetReader.readAssetAsString(featuresName)
val jsonConfigValues = assetReader.readAssetAsString(configValuesName)
val jsonFeatures = assetReader.readJson(featuresName)
val jsonConfigValues = assetReader.readJson(configValuesName)
ConfigModel(featureAdapter.fromJson(jsonFeatures), valuesAdapter.fromJson(jsonConfigValues))
} catch (ex: Exception) {

View file

@ -35,6 +35,7 @@ class ConfigValueModel(
val tronGridApiKey: String,
val amplitudeApiKey: String,
val swapReferrerAccount: SwapReferrerAccount?,
val kaspaSecondaryApiUrl: String,
)
data class AppsFlyer(

View file

@ -15,7 +15,6 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -36,9 +35,9 @@ internal class AndroidNetworkConnectionManager @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : NetworkConnectionManager {
override val connectionStatus: StateFlow<ConnectionStatus> get() = _connectionStatus
override val isOnline: Boolean get() = _isOnline.value
private val _connectionStatus = MutableStateFlow(ConnectionStatus.OFFLINE)
private val _isOnline = MutableStateFlow(value = false)
private val callbacks = NetworkConnectionManagerCallbacks()
private val receiver = NetworkConnectionBroadcastReceiver()
@ -78,28 +77,26 @@ internal class AndroidNetworkConnectionManager @Inject constructor(
CoroutineScope(SupervisorJob()).launch(dispatchers.io) {
try {
val status = getCurrentStatus()
val status = isConnected()
_connectionStatus.emit(value = status)
Timber.i("Status changed to $status")
_isOnline.emit(value = status)
Timber.i("Connection status is ${if (status) "ONLINE" else "OFFLINE"}")
} finally {
pendingResult.finish()
}
}
}
private fun getCurrentStatus(): ConnectionStatus {
connectivityManager ?: return ConnectionStatus.OFFLINE
private fun isConnected(): Boolean {
connectivityManager ?: return false
val isConnected = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
capabilities != null && capabilities.hasNetworkTransport()
} else {
val networkInfo = connectivityManager.activeNetworkInfo
networkInfo != null && networkInfo.isConnectedOrConnecting
}
return if (isConnected) ConnectionStatus.ONLINE else ConnectionStatus.OFFLINE
}
private fun NetworkCapabilities.hasNetworkTransport(): Boolean {

View file

@ -1,10 +0,0 @@
package com.tangem.datasource.connection
/**
* Network connection status
*
[REDACTED_AUTHOR]
*/
enum class ConnectionStatus {
ONLINE, OFFLINE
}

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