Updated on 2026-08-14

This commit is contained in:
Tangem 2024-05-27 11:42:24 +01:00
commit c3522320ae
660 changed files with 10672 additions and 6904 deletions

View file

@ -42,8 +42,8 @@ configurations.all {
dependencies {
implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(projects.domain.legacy)
implementation(projects.libs.blockchainSdk)
implementation(projects.domain.models)
implementation(projects.domain.core)
implementation(projects.domain.card)
@ -80,6 +80,7 @@ dependencies {
implementation(projects.core.deepLinks)
implementation(projects.libs.crypto)
implementation(projects.libs.auth)
implementation(projects.libs.blockchainSdk)
implementation(projects.data.appCurrency)
implementation(projects.data.appTheme)
@ -87,7 +88,6 @@ dependencies {
implementation(projects.data.card)
implementation(projects.data.common)
implementation(projects.data.settings)
implementation(projects.data.source.preferences)
implementation(projects.data.tokens)
implementation(projects.data.txhistory)
implementation(projects.data.wallets)
@ -121,6 +121,8 @@ dependencies {
implementation(projects.features.send.impl)
implementation(projects.features.qrScanning.api)
implementation(projects.features.qrScanning.impl)
implementation(projects.features.staking.api)
implementation(projects.features.staking.impl)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)
@ -183,7 +185,6 @@ dependencies {
implementation(deps.reKotlin)
implementation(deps.zxing.qrCore)
implementation(deps.coil)
implementation(deps.appsflyer)
implementation(deps.amplitude)
implementation(deps.kotsonGson)
implementation(deps.spongecastle.core)
@ -216,9 +217,12 @@ dependencies {
androidTestImplementation(deps.test.kaspresso.compose)
androidTestImplementation(deps.test.compose.junit)
androidTestImplementation(deps.test.hamcrest)
androidTestImplementation(deps.test.hilt)
kaptAndroidTest(deps.test.hilt.compiler)
/** Chucker */
debugImplementation(deps.chucker)
mockedImplementation(deps.chuckerStub)
externalImplementation(deps.chuckerStub)
internalImplementation(deps.chuckerStub)
releaseImplementation(deps.chuckerStub)

View file

@ -0,0 +1,28 @@
package com.tangem.common
import androidx.test.core.app.ApplicationProvider
import com.tangem.tap.ApplicationEntryPoint
import com.tangem.tap.TangemApplication
import dagger.hilt.android.testing.OnComponentReadyRunner
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
class ApplicationInjectionExecutionRule : TestRule {
private val tangemApplication: TangemApplication
get() = ApplicationProvider.getApplicationContext()
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
override fun evaluate() {
OnComponentReadyRunner.addListener(
tangemApplication, ApplicationEntryPoint::class.java
) { _: ApplicationEntryPoint ->
tangemApplication.init()
}
base.evaluate()
}
}
}
}

View file

@ -0,0 +1,56 @@
package com.tangem.common
import android.Manifest
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.espresso.intent.Intents
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.composesupport.config.withComposeSupport
import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.tangem.tap.MainActivity
import com.tangem.tap.domain.sdk.TangemSdkManager
import dagger.hilt.android.testing.HiltAndroidRule
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.runner.RunWith
import javax.inject.Inject
@RunWith(AndroidJUnit4::class)
abstract class BaseTestCase : TestCase(
kaspressoBuilder = Kaspresso.Builder.withComposeSupport()
) {
@Inject
lateinit var tangemSdkManager: TangemSdkManager
@get:Rule
open val composeTestRule = createAndroidComposeRule<MainActivity>()
@get:Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.CAMERA
)
private val hiltRule = HiltAndroidRule(this)
@Rule
@JvmField
val ruleChain = RuleChain
.outerRule(hiltRule)
.around(ApplicationInjectionExecutionRule())
protected fun setupHooks(
additionalBeforeSection: () -> Unit = {},
additionalAfterSection: () -> Unit = {},
) = before {
hiltRule.inject()
Intents.init()
additionalBeforeSection()
}.after {
additionalAfterSection()
Intents.release()
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.common
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import com.tangem.common.di.TangemMockedApplication_Application
class HiltTestRunner : AndroidJUnitRunner() {
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application {
return super.newApplication(cl, TangemMockedApplication_Application::class.java.name, context)
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.common
import com.tangem.tap.TangemApplication
open class TangemEmptyApplication : TangemApplication() {
override fun onCreate() {
// super.onCreate() is not called intentionally
}
}

View file

@ -0,0 +1,7 @@
package com.tangem.common.di
import com.tangem.common.TangemEmptyApplication
import dagger.hilt.android.testing.CustomTestApplication
@CustomTestApplication(TangemEmptyApplication::class)
internal class TangemMockedApplication

View file

@ -0,0 +1,28 @@
package com.tangem.common.di
import android.content.Context
import com.tangem.tap.di.TangemSdkManagerModule
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [TangemSdkManagerModule::class]
)
object TestModule {
@Provides
@Singleton
fun provideTangemSdkManager(
@ApplicationContext context: Context
): TangemSdkManager {
return MockTangemSdkManager(resources = context.resources)
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.helpers.base
import android.Manifest
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.components.composesupport.config.withComposeSupport
import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.tangem.tap.MainActivity
import org.junit.Rule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
open class BaseAutoTestCase : TestCase(
kaspressoBuilder = Kaspresso.Builder.withComposeSupport()
) {
@get:Rule
open val composeTestRule = createAndroidComposeRule<MainActivity>()
@get: Rule
val grantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.POST_NOTIFICATIONS,
Manifest.permission.CAMERA
)
}

View file

@ -0,0 +1,17 @@
package com.tangem.screens
import com.kaspersky.kaspresso.screens.KScreen
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.wallet.R
import io.github.kakaocup.kakao.text.KButton
object DisclaimerScreen : KScreen<DisclaimerScreen>(){
override val layoutId = R.layout.fragment_disclaimer
override val viewClass = DisclaimerFragment::class.java
val acceptButton: KButton = KButton {
withId(R.id.btn_accept)
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.tap.common.compose.resources.C
import com.tangem.core.ui.test.TestTags
import com.tangem.wallet.R
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.KNode
@ -11,15 +11,15 @@ import io.github.kakaocup.kakao.text.KButton
class StoriesScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<StoriesScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(C.Tag.STORIES_SCREEN) }
viewBuilderAction = { hasTestTag(TestTags.STORIES_SCREEN) }
) {
val scanButton: KNode = child {
hasTestTag(C.Tag.STORIES_SCREEN_SCAN_BUTTON)
hasTestTag(TestTags.STORIES_SCREEN_SCAN_BUTTON)
}
val orderButton: KNode = child {
hasTestTag(C.Tag.STORIES_SCREEN_ORDER_BUTTON)
hasTestTag(TestTags.STORIES_SCREEN_ORDER_BUTTON)
}
val enableNFCAlert: KView = KView {

View file

@ -0,0 +1,11 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.core.ui.test.TestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
class WalletScreen(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletScreen>(
semanticsProvider = semanticsProvider,
viewBuilderAction = { hasTestTag(TestTags.WALLET_SCREEN) }
)

View file

@ -0,0 +1,39 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerScreen
import com.tangem.screens.StoriesScreen
import com.tangem.screens.WalletScreen
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
class MainScreenTest : BaseTestCase() {
@Test
fun goToMain() =
setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) {
step("Click on \"Scan\" button") {
scanButton {
assertIsDisplayed()
performClick()
}
}
}
DisclaimerScreen {
step("Click on \"Accept\" button") {
acceptButton {
isVisible()
click()
}
}
}
ComposeScreen.onComposeScreen<WalletScreen>(composeTestRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
}
}

View file

@ -0,0 +1,48 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.screens.DisclaimerScreen
import com.tangem.screens.StoriesScreen
import com.tangem.screens.WalletScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import org.junit.Test
@HiltAndroidTest
class ScanErrorTest : BaseTestCase() {
@Test
fun goToMain() =
setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) {
step("Click on \"Scan\" button emulating scan error") {
MockProvider.setEmulateError()
scanButton {
assertIsDisplayed()
performClick()
}
}
step("Click on \"Scan\" button again without emulating error") {
MockProvider.resetEmulateError()
scanButton {
assertIsDisplayed()
performClick()
}
}
}
DisclaimerScreen {
step("Click on \"Accept\" button") {
acceptButton {
isVisible()
click()
}
}
}
ComposeScreen.onComposeScreen<WalletScreen>(composeTestRule) {
step("Make sure wallet screen is visible") {
assertIsDisplayed()
}
}
}
}

View file

@ -1,45 +1,43 @@
package com.tangem.tests
import android.content.Intent.ACTION_VIEW
import androidx.test.espresso.intent.Intents
import com.tangem.helpers.base.BaseAutoTestCase
import com.tangem.common.BaseTestCase
import com.tangem.screens.StoriesScreen
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
import dagger.hilt.android.testing.HiltAndroidTest
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.kakao.intent.KIntent
import org.junit.Test
class StoriesTest : BaseAutoTestCase() {
@HiltAndroidTest
class StoriesTest : BaseTestCase() {
@Test
fun clickOnButtons() = before {
Intents.init()
}.after {
Intents.release()
}.run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) {
step("Click on \"Scan\" button") {
scanButton {
assertIsDisplayed()
performClick()
fun clickOnButtons() =
setupHooks().run {
ComposeScreen.onComposeScreen<StoriesScreen>(composeTestRule) {
step("Click on \"Scan\" button") {
scanButton {
assertIsDisplayed()
performClick()
}
}
}
step("Assert: \"Scan card\" popup opened") {
enableNFCAlert.isDisplayed()
cancelButton.click()
device.uiDevice.pressBack()
}
step("Click on \"Order\" button") {
orderButton.performClick()
}
step("Assert: browser opened") {
val expectedIntent = KIntent {
hasAction(ACTION_VIEW)
hasData(NEW_BUY_WALLET_URL)
step("Assert: \"Scan card\" popup opened") {
enableNFCAlert.isDisplayed()
cancelButton.click()
device.uiDevice.pressBack()
}
step("Click on \"Order\" button") {
orderButton.performClick()
}
step("Assert: browser opened") {
val expectedIntent = KIntent {
hasAction(ACTION_VIEW)
hasData(NEW_BUY_WALLET_URL)
}
expectedIntent.intended()
device.uiDevice.pressBack()
}
expectedIntent.intended()
device.uiDevice.pressBack()
}
}
}
}

View file

@ -10,7 +10,6 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@ -36,7 +35,7 @@
</queries>
<application
android:name="com.tangem.tap.TapApplication"
android:name="com.tangem.tap.TangemHiltApplication"
android:allowBackup="false"
android:fullBackupContent="false"
android:hardwareAccelerated="true"

@ -1 +1 @@
Subproject commit 367aa6465ec6ee7fdccce6cc7d7abeed5aad1111
Subproject commit 7ae8d8956605c444a46a5b15cfa0a11cb595ca9f

View file

@ -0,0 +1,107 @@
package com.tangem.tap
import com.tangem.TangemSdkLogger
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
import com.tangem.domain.apptheme.GetAppThemeModeUseCase
import com.tangem.domain.apptheme.repository.AppThemeModeRepository
import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
@EntryPoint
@InstallIn(SingletonComponent::class)
@Suppress("TooManyFunctions")
interface ApplicationEntryPoint {
fun getConfigManager(): ConfigManager
fun getAppStateHolder(): AppStateHolder
fun getAssetReader(): AssetReader
fun getFeatureTogglesManager(): FeatureTogglesManager
fun getNetworkConnectionManager(): NetworkConnectionManager
fun getCustomTokenFeatureToggles(): CustomTokenFeatureToggles
fun getWalletConnect2Repository(): WalletConnect2Repository
fun getWalletConnectSessionsRepository(): WalletConnectSessionsRepository
fun getManageTokensFeatureToggles(): ManageTokensFeatureToggles
fun getScanCardProcessor(): ScanCardProcessor
fun getAppCurrencyRepository(): AppCurrencyRepository
fun getWalletManagersFacade(): WalletManagersFacade
fun getNetworksRepository(): NetworksRepository
fun getCurrenciesRepository(): CurrenciesRepository
fun getAppThemeModeRepository(): AppThemeModeRepository
fun getBalanceHidingRepository(): BalanceHidingRepository
fun getUserTokensStore(): UserTokensStore
fun getGetAppThemeModeUseCase(): GetAppThemeModeUseCase
fun getWalletsRepository(): WalletsRepository
fun getSendFeatureToggles(): SendFeatureToggles
fun getOneTimeEventFilter(): OneTimeEventFilter
fun getGeneralUserWalletsListManager(): UserWalletsListManager
fun getWasTwinsOnboardingShownUseCase(): WasTwinsOnboardingShownUseCase
fun getSaveTwinsOnboardingShownUseCase(): SaveTwinsOnboardingShownUseCase
fun getWalletNameGenerateUseCase(): GenerateWalletNameUseCase
fun getCardRepository(): CardRepository
fun getFeedbackManagerFeatureToggles(): FeedbackManagerFeatureToggles
fun getTangemSdkLogger(): TangemSdkLogger
fun getSettingsRepository(): SettingsRepository
fun getBlockchainSDKFactory(): BlockchainSDKFactory
fun getGetFeedbackEmailUseCase(): GetFeedbackEmailUseCase
fun getSaveBlockchainErrorUseCase(): SaveBlockchainErrorUseCase
}

View file

@ -5,6 +5,8 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchWithMain
@ -14,7 +16,9 @@ import kotlin.time.Duration
internal class LockUserWalletsTimer(
owner: LifecycleOwner,
private val settingsRepository: SettingsRepository,
private val duration: Duration = with(Duration) { 10.minutes },
private val userWalletsListManager: UserWalletsListManager,
) : LifecycleOwner by owner,
DefaultLifecycleObserver {
@ -29,24 +33,35 @@ internal class LockUserWalletsTimer(
}
override fun onResume(owner: LifecycleOwner) {
Timber.d(
"""
owner.lifecycleScope.launch {
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume()
Timber.d(
"""
Owner resumed
|- Was stopped: ${preferencesStorage.wasApplicationStopped}
|- Need to open welcome screen: ${preferencesStorage.shouldOpenWelcomeScreenOnResume}
""".trimIndent(),
)
preferencesStorage.wasApplicationStopped = false
start()
if (preferencesStorage.shouldOpenWelcomeScreenOnResume) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
preferencesStorage.shouldOpenWelcomeScreenOnResume = false
|- Was stopped: $wasApplicationStopped
|- Need to open welcome screen: $shouldOpenWelcomeScreenOnResume
""".trimIndent(),
)
settingsRepository.setWasApplicationStopped(value = false)
start()
if (shouldOpenWelcomeScreenOnResume) {
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Welcome))
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false)
}
}
}
override fun onStop(owner: LifecycleOwner) {
Timber.d("Owner stopped")
preferencesStorage.wasApplicationStopped = true
owner.lifecycleScope.launch {
settingsRepository.setWasApplicationStopped(value = true)
}
}
override fun onDestroy(owner: LifecycleOwner) {
@ -91,27 +106,30 @@ internal class LockUserWalletsTimer(
private fun createDelayJob(): Job = lifecycleScope.launch(Dispatchers.Default) {
val startTime = System.currentTimeMillis()
delay(duration)
if (isActive) {
val userWalletsListManager = userWalletsListManagerSafe?.asLockable()
?: return@launch
val userWalletsListManager = userWalletsListManager.asLockable() ?: return@launch
if (userWalletsListManager.hasUserWallets) {
val currentTime = System.currentTimeMillis()
val wasApplicationStopped = settingsRepository.wasApplicationStopped()
Timber.d(
"""
Finished
|- App is stopped: ${preferencesStorage.wasApplicationStopped}
|- App is stopped: $wasApplicationStopped
|- Millis passed: ${currentTime - startTime}
""".trimIndent(),
)
if (preferencesStorage.wasApplicationStopped) {
preferencesStorage.shouldOpenWelcomeScreenOnResume = true
userWalletsListManager.lock()
if (wasApplicationStopped) {
settingsRepository.setShouldOpenWelcomeScreenOnResume(value = true)
} else {
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Welcome))
}
userWalletsListManager.lock()
}
}
}

View file

@ -30,6 +30,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.deeplink.DeepLinksRegistry
import com.tangem.core.navigation.AppScreen
import com.tangem.core.navigation.NavigationAction
import com.tangem.core.navigation.email.EmailSender
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
@ -37,11 +38,10 @@ import com.tangem.data.card.sdk.CardSdkLifecycleObserver
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.feature.qrscanning.QrScanningRouter
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
import com.tangem.features.managetokens.navigation.ManageTokensUi
@ -57,8 +57,7 @@ import com.tangem.tap.common.OnActivityResultCallback
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.intentHandler.IntentProcessor
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
@ -94,12 +93,6 @@ private val mainCoroutineContext: CoroutineContext
get() = Job() + Dispatchers.Main + FeatureCoroutineExceptionHandler.create("mainScope")
val mainScope = CoroutineScope(mainCoroutineContext)
// TODO: Move to DI
val userWalletsListManagerSafe: UserWalletsListManager?
get() = store.state.globalState.userWalletsListManager
val userWalletsListManager: UserWalletsListManager
get() = userWalletsListManagerSafe!!
@Suppress("LargeClass")
@AndroidEntryPoint
class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder {
@ -145,7 +138,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lateinit var deepLinksRegistry: DeepLinksRegistry
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
lateinit var settingsRepository: SettingsRepository
@Inject
lateinit var getPolkadotCheckHasResetUseCase: GetPolkadotCheckHasResetUseCase
@ -156,6 +149,12 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var analyticsEventsHandler: AnalyticsEventHandler
@Inject
lateinit var userWalletsListManager: UserWalletsListManager
@Inject
lateinit var emailSender: EmailSender
internal val viewModel: MainViewModel by viewModels()
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode?>
@ -170,11 +169,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
val splashScreen = installSplashScreen()
installAppTheme() // We need to call it before onCreate to prevent unnecessary activity recreation
super.onCreate(savedInstanceState)
splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown }
installActivityDependencies()
observeAppThemeModeUpdates()
@ -217,7 +219,11 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
tangemSdkManager = injectedTangemSdkManager
appStateHolder.tangemSdkManager = tangemSdkManager
backupService = BackupService.init(cardSdkConfigRepository.sdk, this)
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
lockUserWalletsTimer = LockUserWalletsTimer(
owner = this,
settingsRepository = settingsRepository,
userWalletsListManager = userWalletsListManager,
)
initIntentHandlers()
@ -232,6 +238,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
cardSdkConfigRepository = cardSdkConfigRepository,
sendRouter = sendRouter,
qrScanningRouter = qrScanningRouter,
emailSender = emailSender,
),
)
}
@ -264,9 +271,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun createAppThemeModeFlow(): SharedFlow<AppThemeMode?> {
val tapApplication = application as TapApplication
val tangemApplication = application as TangemApplication
return tapApplication.getAppThemeModeUseCase()
return tangemApplication.getAppThemeModeUseCase()
.map { maybeMode ->
maybeMode.getOrElse { AppThemeMode.DEFAULT }
}
@ -303,7 +310,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true }
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
}
@ -437,15 +444,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
}
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
val canSaveWallets = if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) {
runCatching { userWalletsListManager.asLockable()?.isLockedSync }
.fold(onSuccess = { true }, onFailure = { false })
} else {
userWalletsListManager is BiometricUserWalletsListManager
}
val hasSavedWallets = userWalletsListManager.hasUserWallets
if (canSaveWallets && hasSavedWallets) {
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
store.dispatch(
NavigationAction.NavigateTo(
screen = AppScreen.Welcome,

View file

@ -11,17 +11,14 @@ import com.orhanobut.logger.Logger
import com.tangem.Log
import com.tangem.LogFormat
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.filter.OneTimeEventFilter
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.FeaturesLocalLoader
import com.tangem.datasource.config.models.Config
@ -35,20 +32,22 @@ import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
@ -61,134 +60,134 @@ import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.product.DerivationsFinder
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import dagger.hilt.EntryPoints
import kotlinx.coroutines.runBlocking
import org.rekotlin.Store
import timber.log.Timber
import javax.inject.Inject
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState>
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
internal lateinit var derivationsFinder: DerivationsFinder
@HiltAndroidApp
internal class TapApplication : Application(), ImageLoaderFactory {
abstract class TangemApplication : Application(), ImageLoaderFactory {
// region Injected
@Inject
lateinit var appStateHolder: AppStateHolder
// region DI
private val entryPoint: ApplicationEntryPoint
get() = EntryPoints.get(this, ApplicationEntryPoint::class.java)
@Inject
lateinit var configManager: ConfigManager
private val appStateHolder: AppStateHolder
get() = entryPoint.getAppStateHolder()
@Inject
lateinit var assetReader: AssetReader
private val configManager: ConfigManager
get() = entryPoint.getConfigManager()
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
private val assetReader: AssetReader
get() = entryPoint.getAssetReader()
@Inject
lateinit var networkConnectionManager: NetworkConnectionManager
private val featureTogglesManager: FeatureTogglesManager
get() = entryPoint.getFeatureTogglesManager()
@Inject
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
private val networkConnectionManager: NetworkConnectionManager
get() = entryPoint.getNetworkConnectionManager()
@Inject
lateinit var preferencesDataSource: PreferencesDataSource
private val customTokenFeatureToggles: CustomTokenFeatureToggles
get() = entryPoint.getCustomTokenFeatureToggles()
@Inject
lateinit var walletConnect2Repository: WalletConnect2Repository
private val walletConnect2Repository: WalletConnect2Repository
get() = entryPoint.getWalletConnect2Repository()
@Inject
lateinit var walletConnectSessionsRepository: WalletConnectSessionsRepository
private val walletConnectSessionsRepository: WalletConnectSessionsRepository
get() = entryPoint.getWalletConnectSessionsRepository()
@Inject
lateinit var manageTokensFeatureToggles: ManageTokensFeatureToggles
private val manageTokensFeatureToggles: ManageTokensFeatureToggles
get() = entryPoint.getManageTokensFeatureToggles()
@Inject
lateinit var scanCardProcessor: ScanCardProcessor
private val scanCardProcessor: ScanCardProcessor
get() = entryPoint.getScanCardProcessor()
@Inject
lateinit var appCurrencyRepository: AppCurrencyRepository
private val appCurrencyRepository: AppCurrencyRepository
get() = entryPoint.getAppCurrencyRepository()
@Inject
lateinit var walletManagersFacade: WalletManagersFacade
private val walletManagersFacade: WalletManagersFacade
get() = entryPoint.getWalletManagersFacade()
@Inject
lateinit var networksRepository: NetworksRepository
private val networksRepository: NetworksRepository
get() = entryPoint.getNetworksRepository()
@Inject
lateinit var currenciesRepository: CurrenciesRepository
private val currenciesRepository: CurrenciesRepository
get() = entryPoint.getCurrenciesRepository()
@Inject
lateinit var appThemeModeRepository: AppThemeModeRepository
private val appThemeModeRepository: AppThemeModeRepository
get() = entryPoint.getAppThemeModeRepository()
@Inject
lateinit var balanceHidingRepository: BalanceHidingRepository
private val balanceHidingRepository: BalanceHidingRepository
get() = entryPoint.getBalanceHidingRepository()
@Inject
lateinit var userTokensStore: UserTokensStore
private val userTokensStore: UserTokensStore
get() = entryPoint.getUserTokensStore()
@Inject
lateinit var getAppThemeModeUseCase: GetAppThemeModeUseCase
val getAppThemeModeUseCase: GetAppThemeModeUseCase
get() = entryPoint.getGetAppThemeModeUseCase()
@Inject
lateinit var walletsRepository: WalletsRepository
private val walletsRepository: WalletsRepository
get() = entryPoint.getWalletsRepository()
@Inject
lateinit var sendFeatureToggles: SendFeatureToggles
private val sendFeatureToggles: SendFeatureToggles
get() = entryPoint.getSendFeatureToggles()
@Inject
lateinit var oneTimeEventFilter: OneTimeEventFilter
private val oneTimeEventFilter: OneTimeEventFilter
get() = entryPoint.getOneTimeEventFilter()
@Inject
lateinit var blockchainDataStorage: BlockchainDataStorage
private val generalUserWalletsListManager: UserWalletsListManager
get() = entryPoint.getGeneralUserWalletsListManager()
@Inject
lateinit var accountCreator: AccountCreator
private val wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
get() = entryPoint.getWasTwinsOnboardingShownUseCase()
@Inject
lateinit var userWalletsListManagerFeatureToggles: UserWalletsListManagerFeatureToggles
private val saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
get() = entryPoint.getSaveTwinsOnboardingShownUseCase()
@Inject
lateinit var generalUserWalletsListManager: UserWalletsListManager
private val generateWalletNameUseCase: GenerateWalletNameUseCase
get() = entryPoint.getWalletNameGenerateUseCase()
@Inject
lateinit var wasTwinsOnboardingShownUseCase: WasTwinsOnboardingShownUseCase
private val cardRepository: CardRepository
get() = entryPoint.getCardRepository()
@Inject
lateinit var saveTwinsOnboardingShownUseCase: SaveTwinsOnboardingShownUseCase
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
get() = entryPoint.getFeedbackManagerFeatureToggles()
@Inject
lateinit var cardRepository: CardRepository
private val tangemSdkLogger: TangemSdkLogger
get() = entryPoint.getTangemSdkLogger()
@Inject
lateinit var feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles
private val settingsRepository: SettingsRepository
get() = entryPoint.getSettingsRepository()
@Inject
lateinit var blockchainSDKLogger: BlockchainSDKLogger
private val blockchainSDKFactory: BlockchainSDKFactory
get() = entryPoint.getBlockchainSDKFactory()
@Inject
lateinit var tangemSdkLogger: TangemSdkLogger
// endregion Injected
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase
get() = entryPoint.getGetFeedbackEmailUseCase()
private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase
get() = entryPoint.getSaveBlockchainErrorUseCase()
// endregion
override fun onCreate() {
super.onCreate()
init()
}
fun init() {
store = createReduxStore()
if (BuildConfig.LOG_ENABLED) {
@ -206,19 +205,10 @@ internal class TapApplication : Application(), ImageLoaderFactory {
activityResultCaller = foregroundActivityObserver
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
preferencesStorage = preferencesDataSource
walletConnectRepository = WalletConnectRepository(this)
// TODO: Try to performance and user experience.
// [REDACTED_JIRA]
runBlocking {
featureTogglesManager.init()
if (userWalletsListManagerFeatureToggles.isGeneralManagerEnabled) {
store.dispatch(GlobalAction.UpdateUserWalletsListManager(generalUserWalletsListManager))
} else {
initUserWalletsListManager()
}
}
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
@ -264,16 +254,16 @@ internal class TapApplication : Application(), ImageLoaderFactory {
balanceHidingRepository = balanceHidingRepository,
walletsRepository = walletsRepository,
sendFeatureToggles = sendFeatureToggles,
blockchainDataStorage = blockchainDataStorage,
accountCreator = accountCreator,
userWalletsListManagerFeatureToggles = userWalletsListManagerFeatureToggles,
generalUserWalletsListManager = generalUserWalletsListManager,
wasTwinsOnboardingShownUseCase = wasTwinsOnboardingShownUseCase,
saveTwinsOnboardingShownUseCase = saveTwinsOnboardingShownUseCase,
generateWalletNameUseCase = generateWalletNameUseCase,
cardRepository = cardRepository,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
tangemSdkLogger = tangemSdkLogger,
blockchainSDKLogger = blockchainSDKLogger,
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
saveBlockchainErrorUseCase = saveBlockchainErrorUseCase,
),
),
)
@ -305,7 +295,6 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initAnalytics(application: Application, config: Config) {
val factory = AnalyticsFactory()
factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder())
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(oneTimeEventFilter)
@ -366,6 +355,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
infoHolder = additionalFeedbackInfo,
logCollector = tangemLogCollector,
chatManager = ChatManager(foregroundActivityObserver),
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
getFeedbackEmailUseCase = getFeedbackEmailUseCase,
)
store.dispatch(GlobalAction.SetFeedbackManager(feedbackManager))
}
@ -373,14 +364,4 @@ internal class TapApplication : Application(), ImageLoaderFactory {
private fun initWarningMessagesManager() {
store.dispatch(GlobalAction.SetWarningManager(WarningMessagesManager()))
}
private suspend fun initUserWalletsListManager() {
val manager = if (walletsRepository.shouldSaveUserWalletsSync()) {
UserWalletsListManager.provideBiometricImplementation(applicationContext)
} else {
UserWalletsListManager.provideRuntimeImplementation()
}
store.dispatch(GlobalAction.UpdateUserWalletsListManager(manager))
}
}

View file

@ -0,0 +1,6 @@
package com.tangem.tap
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class TangemHiltApplication : TangemApplication()

View file

@ -86,10 +86,6 @@ class DialogManager : StoreSubscriber<GlobalState> {
context = context,
)
}
is WalletConnectDialog.ApproveWcSession ->
ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ChooseNetwork ->
ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ClipboardOrScanQr ->
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction -> TransactionDialog.create(state.dialog.data, context)

View file

@ -13,20 +13,20 @@ sealed class AnalyticsParam {
}
sealed class CardBalanceState(val value: String) {
object Empty : CardBalanceState("Empty")
object Full : CardBalanceState("Full")
data object Empty : CardBalanceState("Empty")
data object Full : CardBalanceState("Full")
companion object
}
sealed class RateApp(val value: String) {
object Liked : RateApp("Liked")
object Closed : RateApp("Close")
data object Liked : RateApp("Liked")
data object Closed : RateApp("Close")
}
sealed class OnOffState(val value: String) {
object On : OnOffState("On")
object Off : OnOffState("Off")
data object On : OnOffState("On")
data object Off : OnOffState("Off")
companion object {
@ -35,13 +35,13 @@ sealed class AnalyticsParam {
}
sealed class UserCode(val value: String) {
object AccessCode : UserCode("Access Code")
data object AccessCode : UserCode("Access Code")
}
sealed class SecurityMode(val value: String) {
object AccessCode : SecurityMode("Access Code")
object Passcode : SecurityMode("Passcode")
object LongTap : SecurityMode("Long Tap")
data object AccessCode : SecurityMode("Access Code")
data object Passcode : SecurityMode("Passcode")
data object LongTap : SecurityMode("Long Tap")
companion object {
fun from(option: SecurityOption): SecurityMode = when (option) {
@ -56,8 +56,8 @@ sealed class AnalyticsParam {
val key: String = "Status"
object Enabled : AccessCodeRecoveryStatus("Enabled")
object Disabled : AccessCodeRecoveryStatus("Disabled")
data object Enabled : AccessCodeRecoveryStatus("Enabled")
data object Disabled : AccessCodeRecoveryStatus("Disabled")
companion object {
fun from(enabled: Boolean): AccessCodeRecoveryStatus {
@ -67,21 +67,21 @@ sealed class AnalyticsParam {
}
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
object BlockchainSdk : Error("Blockchain Sdk Error")
data object App : Error("App Error")
data object CardSdk : Error("Card Sdk Error")
data object BlockchainSdk : Error("Blockchain Sdk Error")
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType(value = "Private Key")
object NewSeed : WalletCreationType(value = "New Seed")
object SeedImport : WalletCreationType(value = "Seed Import")
data object PrivateKey : WalletCreationType(value = "Private Key")
data object NewSeed : WalletCreationType(value = "New Seed")
data object SeedImport : WalletCreationType(value = "Seed Import")
}
sealed class AppTheme(val value: String) {
object System : AppTheme("System")
object Dark : AppTheme("Dark")
object Light : AppTheme("Light")
data object System : AppTheme("System")
data object Dark : AppTheme("Dark")
data object Light : AppTheme("Light")
companion object {
fun fromAppThemeMode(mode: AppThemeMode): AppTheme {
@ -104,6 +104,7 @@ sealed class AnalyticsParam {
const val PERMISSION_TYPE = "Permission Type"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val USER_WALLET_ID = "User Wallet ID"
const val CURRENCY = "Currency"
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.analytics.events
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.extensions.filterNotNull

View file

@ -1,38 +0,0 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.appsflyer.AFInAppEventType
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.events.Shop
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
) : AnalyticsHandler {
override fun id(): String = ID
override fun send(eventId: String, params: Map<String, String>) {
client.logEvent(eventId, params)
}
override fun send(event: AnalyticsEvent) {
if (event is Shop.Purchased) {
send(AFInAppEventType.PURCHASE, event.params)
} else {
super.send(event)
}
}
companion object {
const val ID = "AppsFlyer"
}
class Builder : AnalyticsHandlerBuilder {
override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? = when {
!data.isDebug -> AppsFlyerClient(data.application, data.config.appsFlyerDevKey)
data.isDebug && data.logConfig.appsFlyer -> AppsFlyerLogClient(data.jsonConverter)
else -> null
}?.let { AppsFlyerAnalyticsHandler(it) }
}
}

View file

@ -1,27 +0,0 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import android.content.Context
import com.appsflyer.AppsFlyerLib
import com.tangem.core.analytics.api.EventLogger
/**
[REDACTED_AUTHOR]
*/
interface AppsFlyerAnalyticsClient : EventLogger
internal class AppsFlyerClient(
private val context: Context,
key: String,
) : AppsFlyerAnalyticsClient {
private val appsFlyerLib: AppsFlyerLib = AppsFlyerLib.getInstance()
init {
appsFlyerLib.init(key, null, context)
appsFlyerLib.start(context)
}
override fun logEvent(event: String, params: Map<String, String>) {
appsFlyerLib.logEvent(context, event, params)
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.tap.common.analytics.AnalyticsEventsLogger
/**
[REDACTED_AUTHOR]
*/
internal class AppsFlyerLogClient(
jsonConverter: MoshiJsonConverter,
) : AppsFlyerAnalyticsClient {
private val logger = AnalyticsEventsLogger(AppsFlyerAnalyticsHandler.ID, jsonConverter)
override fun logEvent(event: String, params: Map<String, String>) {
logger.logEvent(event, params)
}
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.IntroductionProcess
@ -18,6 +19,8 @@ class CardContextInterceptor(
private val scanResponse: ScanResponse,
) : ParamsInterceptor {
private val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
override fun id(): String = CardContextInterceptor.id()
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean {
@ -32,6 +35,9 @@ class CardContextInterceptor(
params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse)
params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue
if (userWalletId != null) {
params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue
}
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let {
params[AnalyticsParam.CURRENCY] = it.value

View file

@ -0,0 +1,12 @@
package com.tangem.tap.common.clipboard
import android.content.ClipData
import com.tangem.core.ui.clipboard.ClipboardManager
import android.content.ClipboardManager as AndroidClipboardManager
internal class DefaultClipboardManager(private val clipboardManager: AndroidClipboardManager) : ClipboardManager {
override fun setText(label: String, text: String) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(label, text))
}
}

View file

@ -0,0 +1,11 @@
package com.tangem.tap.common.clipboard
import com.tangem.core.ui.clipboard.ClipboardManager
import timber.log.Timber
internal object MockClipboardManager : ClipboardManager {
override fun setText(label: String, text: String) {
/** Intentionnaly do nothing */
Timber.w("Clipboard Manager not available")
}
}

View file

@ -1,9 +0,0 @@
package com.tangem.tap.common.compose.resources
object C {
object Tag {
const val STORIES_SCREEN = "STORIES_SCREEN_CONTAINER"
const val STORIES_SCREEN_SCAN_BUTTON = "STORIES_SCREEN_SCAN_BUTTON"
const val STORIES_SCREEN_ORDER_BUTTON = "STORIES_SCREEN_ORDER_BUTTON"
}
}

View file

@ -4,8 +4,8 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.blockchainsdk.utils.amountToCreateAccount
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.amountToCreateAccount
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder

View file

@ -6,7 +6,7 @@ import com.tangem.blockchain.common.address.Address
import com.tangem.crypto.NetworkType
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import java.util.concurrent.CopyOnWriteArrayList

View file

@ -1,13 +1,22 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.core.navigation.email.EmailSender
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.common.TapWorkarounds
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.tap.common.chat.ChatManager
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.withForegroundActivity
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.io.FileWriter
@ -20,21 +29,46 @@ class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val chatManager: ChatManager,
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
private val getFeedbackEmailUseCase: GetFeedbackEmailUseCase,
) {
private var sessionFeedbackFile: File? = null
private var sessionLogsFile: File? = null
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity),
onFail = onFail,
)
if (feedbackManagerFeatureToggles.isLocalLogsEnabled) {
mainScope.launch {
val email = getFeedbackEmailUseCase(
when (feedbackData) {
is FeedbackEmail -> FeedbackEmailType.DirectUserRequest
is RateCanBeBetterEmail -> FeedbackEmailType.RateCanBeBetter
is ScanFailsEmail -> FeedbackEmailType.ScanningProblem
is SendTransactionFailedEmail -> FeedbackEmailType.TransactionSendingProblem
else -> FeedbackEmailType.DirectUserRequest
},
)
store.inject(DaggerGraphState::emailSender).send(
email = EmailSender.Email(
address = email.address,
subject = email.subject,
message = email.message,
attachment = email.file,
),
)
}
} else {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = getLogFile(activity),
onFail = onFail,
)
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.common.haptic
import android.os.Build
import android.os.VibrationEffect
import android.os.Vibrator
import com.tangem.core.ui.haptic.HapticManager
class DefaultHapticManager(private val vibrator: Vibrator) : HapticManager {
override fun vibrateShort() {
vibrate(VIBRATION_SHORT_DURATION)
}
override fun vibrateMeduim() {
vibrate(VIBRATION_MEDIUM_LOW_DURATION)
}
override fun vibrateLong() {
vibrate(VIBRATION_MEDIUM_DURATION)
}
private fun vibrate(durationMs: Long) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createOneShot(durationMs, VibrationEffect.DEFAULT_AMPLITUDE))
} else {
vibrator.vibrate(durationMs)
}
}
companion object {
const val VIBRATION_SHORT_DURATION = 50L
const val VIBRATION_MEDIUM_LOW_DURATION = 75L
const val VIBRATION_MEDIUM_DURATION = 100L
const val VIBRATION_LONG_DURATION = 200L
}
}

View file

@ -3,9 +3,10 @@ package com.tangem.tap.common.redux
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.mainScope
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
class AccessCodeRequestPolicyMiddleware {
@ -21,8 +22,12 @@ class AccessCodeRequestPolicyMiddleware {
}
private fun updateAccessCodeRequestPolicy(scanResponse: ScanResponse) {
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = preferencesStorage.shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet,
)
mainScope.launch {
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
isBiometricsRequestPolicy = shouldSaveAccessCodes && scanResponse.card.isAccessCodeSet,
)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.navigation.StateDialog
@ -9,7 +8,6 @@ import com.tangem.datasource.config.models.ChatConfig
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction
@ -85,7 +83,6 @@ sealed class GlobalAction : Action {
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData, val chatConfig: ChatConfig? = null) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : GlobalAction() {
@ -101,6 +98,5 @@ sealed class GlobalAction : Action {
data class Success(val countryCode: String) : GlobalAction()
}
data class UpdateUserWalletsListManager(val manager: UserWalletsListManager) : GlobalAction()
data class ChangeAppThemeMode(val appThemeMode: AppThemeMode) : GlobalAction()
}

View file

@ -3,9 +3,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.Basic
import com.tangem.core.navigation.StateDialog
import com.tangem.datasource.config.models.Config
import com.tangem.domain.appcurrency.model.AppCurrency
@ -15,7 +13,6 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.mainScope
import com.tangem.tap.network.exchangeServices.BuyExchangeService
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
@ -26,13 +23,10 @@ import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
import java.util.Locale
object GlobalMiddleware {
@ -97,16 +91,13 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
feedbackManager.openChat(chatConfig, action.feedbackData)
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.ExchangeManager.Init -> {
val appStateSafe = appState() ?: return
val config = appStateSafe.globalState.configManager?.config ?: return
scope.launch {
val scanResponseProvider: () -> ScanResponse? = {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
userWalletsListManager.selectedUserWalletSync?.scanResponse
}
val cardProvider: () -> CardDTO? = { scanResponseProvider.invoke()?.card }
@ -148,33 +139,6 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
}
}
}
is GlobalAction.UpdateUserWalletsListManager -> {
val walletManagersFacade = store.inject(DaggerGraphState::walletManagersFacade)
/*
* If implementation of the UserWalletsListManager is changed,
* then all observers of selectedUserWallet become irrelevant.
*/
action.manager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet.scanResponse)
Analytics.send(Basic.WalletOpened())
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
infoHolder.setCardInfo(userWallet.scanResponse)
walletManagersFacade
.getAll(userWallet.walletId)
.distinctUntilChanged()
.onEach(infoHolder::setWalletsInfo)
.catch { Timber.e(it) }
.launchIn(mainScope)
}
}
.flowOn(Dispatchers.IO)
.launchIn(scope)
}
}
}

View file

@ -3,12 +3,10 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.utils.extensions.replaceBy
import org.rekotlin.Action
@ -94,19 +92,6 @@ fun globalReducer(action: Action, state: AppState, appStateHolder: AppStateHolde
userCountryCode = action.countryCode,
)
}
is GlobalAction.UpdateUserWalletsListManager -> {
val featureToggles = store.inject(DaggerGraphState::userWalletsListManagerFeatureToggles)
if (featureToggles.isGeneralManagerEnabled) {
val generalUserWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
appStateHolder.userWalletsListManager = generalUserWalletsListManager
globalState.copy(userWalletsListManager = generalUserWalletsListManager)
} else {
appStateHolder.userWalletsListManager = action.manager
globalState.copy(userWalletsListManager = action.manager)
}
}
is GlobalAction.ChangeAppThemeMode -> globalState.copy(
appThemeMode = action.appThemeMode,
)

View file

@ -5,7 +5,6 @@ import com.tangem.datasource.config.ConfigManager
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.apptheme.model.AppThemeMode
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
@ -27,7 +26,6 @@ data class GlobalState(
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val userCountryCode: String? = null,
val userWalletsListManager: UserWalletsListManager? = null,
val appThemeMode: AppThemeMode = AppThemeMode.DEFAULT,
) : StateType

View file

@ -1,7 +1,7 @@
package com.tangem.tap.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.firstOrNull
@ -9,26 +9,24 @@ import kotlinx.coroutines.flow.firstOrNull
// FIXME: Workaround, remove it once the normal UserWalletsStore has been implemented
// [REDACTED_JIRA]
internal class RuntimeUserWalletsStore(
private val walletsStateHolder: WalletsStateHolder,
private val userWalletsListManager: UserWalletsListManager,
) : UserWalletsStore {
override val selectedUserWalletOrNull: UserWallet?
get() = walletsStateHolder.userWalletsListManager?.selectedUserWalletSync
get() = userWalletsListManager.selectedUserWalletSync
override suspend fun getSyncOrNull(key: UserWalletId): UserWallet? {
return walletsStateHolder.userWalletsListManager
?.userWallets
?.firstOrNull()
return userWalletsListManager
.userWallets
.firstOrNull()
?.singleOrNull { it.walletId == key }
}
override suspend fun getAllSyncOrNull(): List<UserWallet>? {
return walletsStateHolder.userWalletsListManager
?.userWallets
?.firstOrNull()
return userWalletsListManager.userWallets.firstOrNull()
}
override suspend fun update(userWalletId: UserWalletId, update: suspend (UserWallet) -> UserWallet) {
walletsStateHolder.userWalletsListManager?.update(userWalletId, update)
userWalletsListManager.update(userWalletId, update)
}
}

View file

@ -1,20 +1,20 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase
import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase
import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.feature.tester.ActivityClassWrapper
import com.tangem.tap.MainActivity
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.scanCard.repository.DefaultScanCardRepository
import com.tangem.tap.network.exchangeServices.DefaultRampManager
import com.tangem.tap.proxy.AppStateHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -25,15 +25,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal object ActivityModule {
@Provides
@Singleton
fun provideTangemSdkManager(
@ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository,
): TangemSdkManager {
return TangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
}
@Provides
@Singleton
fun provideScanCardUseCase(
@ -76,4 +67,10 @@ internal object ActivityModule {
): GetPolkadotCheckHasImmortalUseCase {
return GetPolkadotCheckHasImmortalUseCase(polkadotAccountHealthCheckRepository)
}
@Provides
@Singleton
fun provideActivityClassWrapper(): ActivityClassWrapper {
return ActivityClassWrapper(MainActivity::class.java)
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.di
import com.tangem.core.navigation.ReduxNavController
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.tap.proxy.AppStateHolder
import dagger.Binds
import dagger.Module
@ -14,10 +13,6 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
internal interface AppStateHolderModule {
@Binds
@Singleton
fun bindsWalletsStateHolder(appStateHolder: AppStateHolder): WalletsStateHolder
@Binds
@Singleton
fun bindsNavigationStateHolder(appStateHolder: AppStateHolder): ReduxNavController

View file

@ -0,0 +1,30 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.tap.common.clipboard.MockClipboardManager
import com.tangem.tap.common.clipboard.DefaultClipboardManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import android.content.ClipboardManager as AndroidClipboardManager
@Module
@InstallIn(SingletonComponent::class)
internal class ClipboardManagerModule {
@Provides
@Singleton
fun provideClipboardManager(@ApplicationContext context: Context): ClipboardManager {
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as? AndroidClipboardManager
return if (clipboardManager != null) {
DefaultClipboardManager(clipboardManager = clipboardManager)
} else {
MockClipboardManager
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.di
import android.content.Context
import android.os.Build
import android.os.Vibrator
import android.os.VibratorManager
import com.tangem.tap.common.haptic.DefaultHapticManager
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.haptic.MockHapticManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class HapticModule {
@Provides
@Singleton
fun provideHapticManager(@ApplicationContext context: Context): HapticManager {
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibratorManager.defaultVibrator
} else {
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
return if (vibrator.hasVibrator()) {
DefaultHapticManager(vibrator = vibrator)
} else {
MockHapticManager
}
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.tap.di
import android.content.Context
import com.tangem.domain.card.BuildConfig
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TangemSdkManagerModule {
@Provides
@Singleton
fun provideTangemSdkManager(
@ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources)
} else {
DefaultTangemSdkManager(cardSdkConfigRepository = cardSdkConfigRepository, resources = context.resources)
}
}
}

View file

@ -5,13 +5,15 @@ import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(ActivityComponent::class)
@InstallIn(SingletonComponent::class)
internal object ThemeModule {
@Provides
@Singleton
fun provideAppThemeModeHolder(): AppThemeModeHolder {
return MutableAppThemeModeHolder
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.di
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.haptic.HapticManager
import com.tangem.core.ui.theme.AppThemeModeHolder
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UiDependenciesModule {
@Provides
@Singleton
fun provideUiDependencies(hapticManager: HapticManager, appThemeModeHolder: AppThemeModeHolder): UiDependencies {
return object : UiDependencies {
override val hapticManager = hapticManager
override val appThemeModeHolder = appThemeModeHolder
}
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDerivationsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.data.RuntimeUserWalletsStore
import dagger.Module
import dagger.Provides
@ -15,7 +15,7 @@ internal object UserWalletsStoreModule {
@Provides
@Singleton
fun provideUserWalletsStore(walletsStateHolder: WalletsStateHolder): UserWalletsStore {
return RuntimeUserWalletsStore(walletsStateHolder)
fun provideUserWalletsStore(userWalletsListManager: UserWalletsListManager): UserWalletsStore {
return RuntimeUserWalletsStore(userWalletsListManager = userWalletsListManager)
}
}

View file

@ -6,10 +6,10 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.card.DefaultDeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.sdk.TangemSdkManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -36,14 +36,6 @@ internal object CardDomainModule {
return SetAccessCodeRequestPolicyUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@ViewModelScoped
fun provideGetAccessCodeSavingStatusUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
): GetAccessCodeSavingStatusUseCase {
return GetAccessCodeSavingStatusUseCase(cardSdkConfigRepository = cardSdkConfigRepository)
}
@Provides
@ViewModelScoped
fun provideWasWalletAlreadySignedHashesConfirmedUseCase(cardRepository: CardRepository): WasCardScannedUseCase {
@ -68,8 +60,8 @@ internal object CardDomainModule {
@Provides
@ViewModelScoped
fun provideIsNeedToBackupUseCase(walletStateHolder: WalletsStateHolder): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(walletStateHolder)
fun provideIsNeedToBackupUseCase(userWalletsListManager: UserWalletsListManager): IsNeedToBackupUseCase {
return IsNeedToBackupUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides

View file

@ -1,6 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.card.*
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GenerateWalletNameUseCase
import com.tangem.tap.domain.scanCard.DefaultScanCardProcessor
import dagger.Module
import dagger.Provides
@ -14,5 +16,11 @@ internal object CardLegacyDomainModule {
@Provides
@Singleton
fun provideScanCardUseCase(): ScanCardProcessor = DefaultScanCardProcessor()
fun provideScanCardProcessor(): ScanCardProcessor = DefaultScanCardProcessor()
@Provides
@Singleton
fun providesWalletNameGenerateUseCase(userWalletsListManager: UserWalletsListManager): GenerateWalletNameUseCase {
return GenerateWalletNameUseCase(userWalletsListManager)
}
}

View file

@ -1,7 +1,8 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetSupportFeedbackEmailUseCase
import com.tangem.domain.feedback.GetFeedbackEmailUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
import dagger.Module
import dagger.Provides
@ -16,10 +17,16 @@ internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetFeedbackToSupportUseCase(
fun provideGetFeedbackEmailUseCase(
feedbackRepository: FeedbackRepository,
@ApplicationContext context: Context,
): GetSupportFeedbackEmailUseCase {
return GetSupportFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
): GetFeedbackEmailUseCase {
return GetFeedbackEmailUseCase(feedbackRepository = feedbackRepository, resources = context.resources)
}
@Provides
@Singleton
fun provideSaveBlockchainErrorUseCase(feedbackRepository: FeedbackRepository): SaveBlockchainErrorUseCase {
return SaveBlockchainErrorUseCase(feedbackRepository = feedbackRepository)
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.domain.settings.*
import com.tangem.domain.settings.repositories.AppRatingRepository
import com.tangem.domain.settings.repositories.PromoSettingsRepository
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository
import dagger.Module
import dagger.Provides
@ -148,4 +148,20 @@ internal object SettingsDomainModule {
fun provideNeverShowTapHelpUseCase(settingsRepository: SettingsRepository): NeverShowTapHelpUseCase {
return NeverShowTapHelpUseCase(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun provideSetSaveWalletScreenShownUseCase(
settingsRepository: SettingsRepository,
): SetSaveWalletScreenShownUseCase {
return SetSaveWalletScreenShownUseCase(settingsRepository = settingsRepository)
}
@Provides
@ViewModelScoped
fun provideIncrementAppLaunchCounterUseCase(
settingsRepository: SettingsRepository,
): IncrementAppLaunchCounterUseCase {
return IncrementAppLaunchCounterUseCase(settingsRepository = settingsRepository)
}
}

View file

@ -6,7 +6,6 @@ import com.tangem.domain.tokens.*
import com.tangem.domain.tokens.repository.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.swap.domain.api.SwapRepository
import com.tangem.features.send.api.featuretoggles.SendFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -55,9 +54,8 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetTokenListUseCase {
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
return GetTokenListUseCase(currenciesRepository, quotesRepository, networksRepository)
}
@Provides
@ -66,9 +64,8 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
dispatchers: CoroutineDispatcherProvider,
): GetCardTokensListUseCase {
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository, dispatchers)
return GetCardTokensListUseCase(currenciesRepository, quotesRepository, networksRepository)
}
@Provides
@ -188,20 +185,20 @@ internal object TokensDomainModule {
@ViewModelScoped
fun provideGetCryptoCurrencyActionsUseCase(
rampStateManager: RampStateManager,
walletManagersFacade: WalletManagersFacade,
marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
networksRepository: NetworksRepository,
sendFeatureToggles: SendFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetCryptoCurrencyActionsUseCase {
return GetCryptoCurrencyActionsUseCase(
rampManager = rampStateManager,
walletManagersFacade = walletManagersFacade,
marketCryptoCurrencyRepository = marketCryptoCurrencyRepository,
currenciesRepository = currenciesRepository,
quotesRepository = quotesRepository,
networksRepository = networksRepository,
sendFeatureToggles = sendFeatureToggles,
dispatchers = dispatchers,
)
}

View file

@ -2,12 +2,11 @@ package com.tangem.tap.di.domain
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.tokens.repository.NetworksRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.usecase.CreateTransactionUseCase
import com.tangem.domain.transaction.usecase.GetFeeUseCase
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.transaction.usecase.*
import com.tangem.domain.walletmanager.WalletManagersFacade
import dagger.Module
import dagger.Provides
@ -43,6 +42,22 @@ internal object TransactionDomainModule {
)
}
@Provides
@ViewModelScoped
fun provideAssociateAssetUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
networksRepository: NetworksRepository,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
networksRepository = networksRepository,
)
}
@Provides
@ViewModelScoped
fun provideCreateTransactionUseCase(transactionRepository: TransactionRepository): CreateTransactionUseCase {
@ -54,4 +69,10 @@ internal object TransactionDomainModule {
fun provideIsFeeApproximateUseCase(feeRepository: FeeRepository): IsFeeApproximateUseCase {
return IsFeeApproximateUseCase(feeRepository)
}
@Provides
@ViewModelScoped
fun provideValidateTransactionUseCase(transactionRepository: TransactionRepository): ValidateTransactionUseCase {
return ValidateTransactionUseCase(transactionRepository)
}
}

View file

@ -1,18 +1,13 @@
package com.tangem.tap.di.domain
import com.squareup.moshi.Moshi
import com.tangem.blockchain.common.AccountCreator
import com.tangem.blockchain.common.datastorage.BlockchainDataStorage
import com.tangem.blockchain.common.logging.BlockchainSDKLogger
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.datasource.asset.reader.AssetReader
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
import com.tangem.domain.walletmanager.DefaultWalletManagersFacade
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.feature.onboarding.data.MnemonicRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -28,26 +23,16 @@ internal object WalletManagersFacadeModule {
fun provideWalletManagersFacade(
walletManagersStore: WalletManagersStore,
userWalletsStore: UserWalletsStore,
configManager: ConfigManager,
blockchainDataStorage: BlockchainDataStorage,
accountCreator: AccountCreator,
mnemonicRepository: MnemonicRepository,
assetReader: AssetReader,
@SdkMoshi moshi: Moshi,
blockchainSDKLogger: BlockchainSDKLogger,
feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
blockchainSDKFactory: BlockchainSDKFactory,
): WalletManagersFacade {
return DefaultWalletManagersFacade(
walletManagersStore = walletManagersStore,
userWalletsStore = userWalletsStore,
configManager = configManager,
blockchainDataStorage = blockchainDataStorage,
assetReader = assetReader,
moshi = moshi,
mnemonic = mnemonicRepository.generateDefaultMnemonic(),
accountCreator = accountCreator,
blockchainSDKLogger = blockchainSDKLogger,
feedbackManagerFeatureToggles = feedbackManagerFeatureToggles,
blockchainSDKFactory = blockchainSDKFactory,
)
}
}

View file

@ -2,10 +2,12 @@ package com.tangem.tap.di.domain
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.WalletsStateHolder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletAddressServiceRepository
import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.*
import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.Module
import dagger.Provides
@ -19,32 +21,46 @@ internal object WalletsDomainModule {
@Provides
@ViewModelScoped
fun providesGetWalletsUseCase(walletsStateHolder: WalletsStateHolder): GetWalletsUseCase {
return GetWalletsUseCase(walletsStateHolder = walletsStateHolder)
fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase {
return GetWalletsUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesGetUserWalletUseCase(walletsStateHolder: WalletsStateHolder): GetUserWalletUseCase {
return GetUserWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesWalletNameMigrationUseCase(
userWalletsListManager: UserWalletsListManager,
walletNamesMigrationRepository: WalletNamesMigrationRepository,
): WalletNameMigrationUseCase {
return WalletNameMigrationUseCase(
userWalletsListManager = userWalletsListManager,
walletNamesMigrationRepository = walletNamesMigrationRepository,
)
}
@Provides
@ViewModelScoped
fun providesGetSelectedWalletSyncUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(walletsStateHolder = walletsStateHolder)
fun providesGetUserWalletUseCase(userWalletsListManager: UserWalletsListManager): GetUserWalletUseCase {
return GetUserWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesGetSelectedWalletUseCase(walletsStateHolder: WalletsStateHolder): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesGetSelectedWalletSyncUseCase(
userWalletsListManager: UserWalletsListManager,
): GetSelectedWalletSyncUseCase {
return GetSelectedWalletSyncUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesSaveWalletUseCase(walletsStateHolder: WalletsStateHolder): SaveWalletUseCase {
return SaveWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesGetSelectedWalletUseCase(userWalletsListManager: UserWalletsListManager): GetSelectedWalletUseCase {
return GetSelectedWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesSaveWalletUseCase(userWalletsListManager: UserWalletsListManager): SaveWalletUseCase {
return SaveWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ -55,29 +71,41 @@ internal object WalletsDomainModule {
@Provides
@ViewModelScoped
fun providesUnlockWalletUseCase(walletsStateHolder: WalletsStateHolder): UnlockWalletsUseCase {
return UnlockWalletsUseCase(walletsStateHolder = walletsStateHolder)
fun providesUnlockWalletUseCase(userWalletsListManager: UserWalletsListManager): UnlockWalletsUseCase {
return UnlockWalletsUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesSelectWalletUseCase(
walletsStateHolder: WalletsStateHolder,
userWalletsListManager: UserWalletsListManager,
reduxStateHolder: ReduxStateHolder,
): SelectWalletUseCase {
return SelectWalletUseCase(walletsStateHolder, reduxStateHolder)
return SelectWalletUseCase(userWalletsListManager = userWalletsListManager, reduxStateHolder = reduxStateHolder)
}
@Provides
@ViewModelScoped
fun providesUpdateWalletUseCase(walletsStateHolder: WalletsStateHolder): UpdateWalletUseCase {
return UpdateWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesUpdateWalletUseCase(userWalletsListManager: UserWalletsListManager): UpdateWalletUseCase {
return UpdateWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesDeleteWalletUseCase(walletsStateHolder: WalletsStateHolder): DeleteWalletUseCase {
return DeleteWalletUseCase(walletsStateHolder = walletsStateHolder)
fun providesRenameWalletUseCase(userWalletsListManager: UserWalletsListManager): RenameWalletUseCase {
return RenameWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesGetWalletsSyncUseCase(userWalletsListManager: UserWalletsListManager): GetWalletNamesUseCase {
return GetWalletNamesUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides
@ViewModelScoped
fun providesDeleteWalletUseCase(userWalletsListManager: UserWalletsListManager): DeleteWalletUseCase {
return DeleteWalletUseCase(userWalletsListManager = userWalletsListManager)
}
@Provides

View file

@ -1,9 +1,7 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.datasource.config.ConfigManager
@ -15,7 +13,6 @@ import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -32,29 +29,12 @@ class TapWalletManager(
private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(),
) {
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private var loadUserWalletDataJob: Job? = null
set(value) {
field?.cancel()
field = value
}
val walletManagerFactory: WalletManagerFactory by lazy {
WalletManagerFactory(
config = blockchainSdkConfig,
accountCreator = store.inject(DaggerGraphState::accountCreator),
blockchainDataStorage = store.inject(DaggerGraphState::blockchainDataStorage),
loggers = if (store.inject(DaggerGraphState::feedbackManagerFeatureToggles).isLocalLogsEnabled) {
listOf(store.inject(DaggerGraphState::blockchainSDKLogger))
} else {
emptyList()
},
)
}
suspend fun onWalletSelected(userWallet: UserWallet, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,
// ensuring that only one job is active at any given time.
@ -73,15 +53,18 @@ class TapWalletManager(
val attestationFailed = card.attestation.status == Attestation.Status.Failed
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
val featureToggles = store.inject(DaggerGraphState::feedbackManagerFeatureToggles)
if (!featureToggles.isLocalLogsEnabled) {
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(scanResponse)
}
updateConfigManager(scanResponse)
withMainContext {
// Order is important
store.dispatch(DisclaimerAction.SetDisclaimer(card.createDisclaimer()))
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
store.dispatch(WalletConnectAction.ResetState)
store.dispatch(GlobalAction.SaveScanResponse(scanResponse))
store.dispatch(WalletConnectAction.RestoreSessions(scanResponse))
store.dispatch(GlobalAction.SetIfCardVerifiedOnline(!attestationFailed))
}
}

View file

@ -6,7 +6,7 @@ import arrow.core.right
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.card.DeleteSavedAccessCodesUseCase
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
internal class DefaultDeleteSavedAccessCodesUseCase(
private val tangemSdkManager: TangemSdkManager,

View file

@ -9,11 +9,11 @@ import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.domain.card.repository.DerivationsRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.runCatching
import timber.log.Timber

View file

@ -5,7 +5,7 @@ import com.tangem.common.services.Result
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.userwallets.Artwork
import com.tangem.domain.wallets.models.Artwork
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
@ -20,8 +20,8 @@ suspend fun CardDTO.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInf
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> {
when (getTwinCardNumber()) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
TwinCardNumber.First -> Artwork.TWIN_CARD_1_URL
TwinCardNumber.Second -> Artwork.TWIN_CARD_2_URL
else -> Artwork.DEFAULT_IMG_URL
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.tap.domain.scanCard.repository
import com.tangem.common.CompletionResult
import com.tangem.domain.card.repository.ScanCardRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.scanCard.utils.ScanCardExceptionConverter
// TODO: Move to the :data:card module

View file

@ -0,0 +1,128 @@
package com.tangem.tap.domain.sdk
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.common.*
import com.tangem.common.authentication.keystore.KeystoreManager
import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.secure.SecureStorage
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@Suppress("TooManyFunctions")
interface TangemSdkManager {
val canUseBiometry: Boolean
val needEnrollBiometrics: Boolean
val keystoreManager: KeystoreManager
val secureStorage: SecureStorage
val userCodeRequestPolicy: UserCodeRequestPolicy
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<ScanResponse>
suspend fun createProductWallet(
scanResponse: ScanResponse,
shouldReset: Boolean = false,
): CompletionResult<CreateProductWalletTaskResponse>
// Wallet2 specific
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
passphrase: String?,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse>
suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): CompletionResult<DerivationTaskResponse>
suspend fun deriveExtendedPublicKey(
cardId: String?,
walletPublicKey: ByteArray,
derivation: DerivationPath,
): CompletionResult<ExtendedPublicKey>
suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO>
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit>
suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit>
suspend fun clearSavedUserCodes(): CompletionResult<Unit>
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse>
suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult<SuccessResponse>
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
): CompletionResult<CardDTO>
@Deprecated(
"TangemSdkManager shouldn't run custom tasks. " +
"All of them should be specified in TangemSdkManager certain methods.",
)
suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,
@DrawableRes iconScanRes: Int? = null,
): CompletionResult<T>
@Suppress("MagicNumber")
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?)
@Deprecated("TangemSdkManager shouldn't returns a string from resources")
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String
fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy)
// region Twin-specific
suspend fun finalizeTwin(
secondCardPublicKey: ByteArray,
issuerKeyPair: KeyPair,
cardId: String,
initialMessage: Message,
): CompletionResult<ScanResponse>
suspend fun createFirstTwinWallet(cardId: String, initialMessage: Message): CompletionResult<CreateWalletResponse>
@Suppress("LongParameterList")
suspend fun createSecondTwinWallet(
firstPublicKey: String,
firstCardId: String,
issuerKeys: KeyPair,
preparingMessage: Message,
creatingWalletMessage: Message,
initialMessage: Message,
): CompletionResult<CreateWalletResponse>
// endregion
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.domain
package com.tangem.tap.domain.sdk.impl
import android.content.res.Resources
import androidx.annotation.DrawableRes
@ -28,11 +28,16 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.derivation.DeriveWalletPublicKeyTask
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.derivationsFinder
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
@ -40,10 +45,10 @@ import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions")
class TangemSdkManager(
class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources,
) {
) : TangemSdkManager {
private val tangemSdk: TangemSdk
get() = cardSdkConfigRepository.sdk
@ -55,25 +60,25 @@ class TangemSdkManager(
)
}
val canUseBiometry: Boolean
override val canUseBiometry: Boolean
get() = tangemSdk.authenticationManager.canAuthenticate || needEnrollBiometrics
val needEnrollBiometrics: Boolean
override val needEnrollBiometrics: Boolean
get() = tangemSdk.authenticationManager.needEnrollBiometrics
val keystoreManager: KeystoreManager
override val keystoreManager: KeystoreManager
get() = tangemSdk.keystoreManager
val secureStorage: SecureStorage
override val secureStorage: SecureStorage
get() = tangemSdk.secureStorage
val userCodeRequestPolicy: UserCodeRequestPolicy
override val userCodeRequestPolicy: UserCodeRequestPolicy
get() = tangemSdk.config.userCodeRequestPolicy
suspend fun scanProduct(
cardId: String? = null,
messageRes: Int? = null,
allowsRequestAccessCodeFromRepository: Boolean = false,
override suspend fun scanProduct(
cardId: String?,
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<ScanResponse> {
val message = Message(resources.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(
@ -87,9 +92,9 @@ class TangemSdkManager(
).also { sendScanResultsToAnalytics(it) }
}
suspend fun createProductWallet(
override suspend fun createProductWallet(
scanResponse: ScanResponse,
shouldReset: Boolean = false,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> {
return runTaskAsync(
runnable = CreateProductWalletTask(
@ -103,7 +108,7 @@ class TangemSdkManager(
)
}
suspend fun importWallet(
override suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
passphrase: String?,
@ -135,14 +140,14 @@ class TangemSdkManager(
}
}
suspend fun derivePublicKeys(
override suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): CompletionResult<DerivationTaskResponse> {
return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId)
}
suspend fun deriveExtendedPublicKey(
override suspend fun deriveExtendedPublicKey(
cardId: String?,
walletPublicKey: ByteArray,
derivation: DerivationPath,
@ -153,7 +158,7 @@ class TangemSdkManager(
)
}
suspend fun resetToFactorySettings(
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
@ -167,7 +172,7 @@ class TangemSdkManager(
.map { CardDTO(it) }
}
suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
override suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
return userCodeRepository.save(
cardsIds = cardsIds,
userCode = UserCode(
@ -177,15 +182,15 @@ class TangemSdkManager(
)
}
suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit> {
override suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit> {
return userCodeRepository.delete(cardsIds.toSet())
}
suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
override suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
return userCodeRepository.clear()
}
suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
override suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.changePasscode(null),
cardId,
@ -193,7 +198,7 @@ class TangemSdkManager(
)
}
suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse> {
override suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.changeAccessCode(null),
cardId,
@ -201,7 +206,7 @@ class TangemSdkManager(
)
}
suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse> {
override suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeCommand.resetUserCodes(),
cardId,
@ -209,7 +214,10 @@ class TangemSdkManager(
)
}
suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult<SuccessResponse> {
override suspend fun setAccessCodeRecoveryEnabled(
cardId: String?,
enabled: Boolean,
): CompletionResult<SuccessResponse> {
return runTaskAsyncReturnOnMain(
SetUserCodeRecoveryAllowedTask(enabled),
cardId,
@ -217,9 +225,9 @@ class TangemSdkManager(
)
}
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return runTaskAsyncReturnOnMain(
runnable = ScanTask(allowRequestAccessCodeFromRepository),
@ -229,12 +237,12 @@ class TangemSdkManager(
.map { CardDTO(it) }
}
suspend fun <T> runTaskAsync(
override suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
cardId: String? = null,
initialMessage: Message? = null,
accessCode: String? = null,
@DrawableRes iconScanRes: Int? = null,
cardId: String?,
initialMessage: Message?,
accessCode: String?,
@DrawableRes iconScanRes: Int?,
): CompletionResult<T> = withContext(Dispatchers.Main) {
suspendCancellableCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage, accessCode, iconScanRes) { result ->
@ -253,7 +261,7 @@ class TangemSdkManager(
}
@Suppress("MagicNumber")
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) {
override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) {
tangemSdk.config.cardIdDisplayFormat = when {
scanResponse == null -> CardIdDisplayFormat.Full
scanResponse.cardTypesResolver.isTangemTwins() -> CardIdDisplayFormat.LastLuhn(4)
@ -262,14 +270,60 @@ class TangemSdkManager(
}
@Deprecated("TangemSdkManager shouldn't returns a string from resources")
fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
return resources.getString(stringResId, *formatArgs)
}
fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
tangemSdk.config.userCodeRequestPolicy = policy
}
// region Twin-specific
override suspend fun createFirstTwinWallet(
cardId: String,
initialMessage: Message,
): CompletionResult<CreateWalletResponse> {
return runTaskAsync(
runnable = CreateFirstTwinWalletTask(cardId),
cardId = cardId,
initialMessage = initialMessage,
)
}
override suspend fun createSecondTwinWallet(
firstPublicKey: String,
firstCardId: String,
issuerKeys: KeyPair,
preparingMessage: Message,
creatingWalletMessage: Message,
initialMessage: Message,
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
firstPublicKey = firstPublicKey,
firstCardId = firstCardId,
issuerKeys = issuerKeys,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
)
return runTaskAsync(task, null, initialMessage)
}
override suspend fun finalizeTwin(
secondCardPublicKey: ByteArray,
issuerKeyPair: KeyPair,
cardId: String,
initialMessage: Message,
): CompletionResult<ScanResponse> {
return runTaskAsync(
runnable = FinalizeTwinTask(secondCardPublicKey, issuerKeyPair),
cardId = cardId,
initialMessage = initialMessage,
)
}
// endregion
companion object {
@Deprecated("Use [DefaultCardSdkProvider] instead")
val config = Config(

View file

@ -0,0 +1,175 @@
package com.tangem.tap.domain.sdk.impl
import android.content.res.Resources
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.common.*
import com.tangem.common.authentication.keystore.DummyKeystoreManager
import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.services.InMemoryStorage
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.TangemSdkManager
import com.tangem.tap.domain.sdk.mocks.MockProvider
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
@Suppress("TooManyFunctions")
class MockTangemSdkManager(
private val resources: Resources,
) : TangemSdkManager {
override val canUseBiometry = false
override val needEnrollBiometrics = false
override val keystoreManager = DummyKeystoreManager()
override val secureStorage = InMemoryStorage()
override val userCodeRequestPolicy: UserCodeRequestPolicy
get() = userCodeRequestPolicyInternal
private var userCodeRequestPolicyInternal: UserCodeRequestPolicy = UserCodeRequestPolicy.Default
override suspend fun scanProduct(
cardId: String?,
messageRes: Int?,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<ScanResponse> {
return MockProvider.getScanResponse()
}
override suspend fun createProductWallet(
scanResponse: ScanResponse,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> {
return MockProvider.getCreateProductWalletResponse()
}
override suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
passphrase: String?,
shouldReset: Boolean,
): CompletionResult<CreateProductWalletTaskResponse> {
return MockProvider.getImportWalletResponse()
}
override suspend fun derivePublicKeys(
cardId: String?,
derivations: Map<ByteArrayKey, List<DerivationPath>>,
): CompletionResult<DerivationTaskResponse> {
return MockProvider.getDerivationTaskResponse()
}
override suspend fun deriveExtendedPublicKey(
cardId: String?,
walletPublicKey: ByteArray,
derivation: DerivationPath,
): CompletionResult<ExtendedPublicKey> {
return MockProvider.getExtendedPublicKey()
}
override suspend fun resetToFactorySettings(
cardId: String,
allowsRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return MockProvider.getCardDto()
}
override suspend fun saveAccessCode(accessCode: String, cardsIds: Set<String>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun deleteSavedUserCodes(cardsIds: Set<String>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun clearSavedUserCodes(): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}
override suspend fun setPasscode(cardId: String?): CompletionResult<SuccessResponse> {
return MockProvider.getSuccessResponse()
}
override suspend fun setAccessCode(cardId: String?): CompletionResult<SuccessResponse> {
return MockProvider.getSuccessResponse()
}
override suspend fun setLongTap(cardId: String?): CompletionResult<SuccessResponse> {
return MockProvider.getSuccessResponse()
}
override suspend fun setAccessCodeRecoveryEnabled(
cardId: String?,
enabled: Boolean,
): CompletionResult<SuccessResponse> {
return MockProvider.getSuccessResponse()
}
override suspend fun scanCard(
cardId: String?,
allowRequestAccessCodeFromRepository: Boolean,
): CompletionResult<CardDTO> {
return MockProvider.getCardDto()
}
override suspend fun <T> runTaskAsync(
runnable: CardSessionRunnable<T>,
cardId: String?,
initialMessage: Message?,
accessCode: String?,
@DrawableRes iconScanRes: Int?,
): CompletionResult<T> = error("This method is deprecated")
override fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse?) {
// intentionally do nothing
}
@Deprecated("TangemSdkManager shouldn't returns a string from resources")
override fun getString(@StringRes stringResId: Int, vararg formatArgs: Any?): String {
return resources.getString(stringResId, *formatArgs)
}
override fun setUserCodeRequestPolicy(policy: UserCodeRequestPolicy) {
userCodeRequestPolicyInternal = policy
}
// region Twin-specific
override suspend fun createFirstTwinWallet(
cardId: String,
initialMessage: Message,
): CompletionResult<CreateWalletResponse> {
return MockProvider.createFirstTwinWallet()
}
override suspend fun createSecondTwinWallet(
firstPublicKey: String,
firstCardId: String,
issuerKeys: KeyPair,
preparingMessage: Message,
creatingWalletMessage: Message,
initialMessage: Message,
): CompletionResult<CreateWalletResponse> {
return MockProvider.createSecondTwinWallet()
}
override suspend fun finalizeTwin(
secondCardPublicKey: ByteArray,
issuerKeyPair: KeyPair,
cardId: String,
initialMessage: Message,
): CompletionResult<ScanResponse> {
return MockProvider.finalizeTwin()
}
// endregion
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.domain.sdk.mocks
import com.tangem.common.SuccessResponse
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
interface MockContent {
val successResponse: SuccessResponse
val scanResponse: ScanResponse
val derivationTaskResponse: DerivationTaskResponse
val cardDto: CardDTO
val extendedPublicKey: ExtendedPublicKey
val createProductWalletTaskResponse: CreateProductWalletTaskResponse
// Wallet2-specific
val importWalletResponse: CreateProductWalletTaskResponse
// Twin-specific
val finalizeTwinResponse: ScanResponse
// Twin-specific
val createFirstTwinResponse: CreateWalletResponse
// Twin-specific
val createSecondTwinResponse: CreateWalletResponse
}

View file

@ -0,0 +1,81 @@
package com.tangem.tap.domain.sdk.mocks
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.domain.models.scan.ProductType
import com.tangem.tap.domain.sdk.mocks.content.WalletMockContent
import com.tangem.tap.domain.sdk.mocks.content.Wallet2MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
object MockProvider {
private var content: MockContent = getMockContent(ProductType.Wallet)
private var emulateError: Boolean = false
private var emulatedError: TangemError = TangemSdkError.TagLost()
fun setEmulateError(error: TangemError? = null) {
emulateError = true
error?.let {
emulatedError = it
}
}
fun resetEmulateError() {
emulateError = false
}
fun setMocks(productType: ProductType) {
content = getMockContent(productType)
}
fun setMocks(mockContent: MockContent) {
content = mockContent
}
fun getSuccessResponse() = CompletionResult.Success(content.successResponse).orFailure()
fun getScanResponse() = CompletionResult.Success(content.scanResponse).orFailure()
fun getDerivationTaskResponse() = CompletionResult.Success(content.derivationTaskResponse).orFailure()
fun getCardDto() = CompletionResult.Success(content.cardDto).orFailure()
fun getExtendedPublicKey() = CompletionResult.Success(content.extendedPublicKey).orFailure()
fun getCreateProductWalletResponse(): CompletionResult<CreateProductWalletTaskResponse> {
return CompletionResult.Success(content.createProductWalletTaskResponse).orFailure()
}
fun getImportWalletResponse(): CompletionResult<CreateProductWalletTaskResponse> {
return CompletionResult.Success(content.importWalletResponse).orFailure()
}
// region Twin-specific
fun finalizeTwin() = CompletionResult.Success(content.finalizeTwinResponse).orFailure()
fun createFirstTwinWallet() = CompletionResult.Success(content.createFirstTwinResponse).orFailure()
fun createSecondTwinWallet() = CompletionResult.Success(content.createSecondTwinResponse).orFailure()
// endregion
private fun getMockContent(productType: ProductType): MockContent {
return when (productType) {
ProductType.Wallet -> WalletMockContent
ProductType.Wallet2 -> Wallet2MockContent
else -> TODO()
}
}
private fun <T> CompletionResult.Success<T>.orFailure(): CompletionResult<T> {
return if (emulateError) {
CompletionResult.Failure(emulatedError)
} else {
this
}
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
object Wallet2MockContent : MockContent {
override val scanResponse: ScanResponse
get() = TODO("Not yet implemented")
override val cardDto: CardDTO
get() = TODO("Not yet implemented")
override val derivationTaskResponse: DerivationTaskResponse
get() = TODO("Not yet implemented")
override val extendedPublicKey: ExtendedPublicKey
get() = TODO("Not yet implemented")
override val successResponse: SuccessResponse
get() = TODO("Not yet implemented")
override val createProductWalletTaskResponse: CreateProductWalletTaskResponse
get() = TODO("Not yet implemented")
override val importWalletResponse: CreateProductWalletTaskResponse
get() = TODO("Not yet implemented")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -0,0 +1,247 @@
package com.tangem.tap.domain.sdk.mocks.content
import com.tangem.common.SuccessResponse
import com.tangem.common.card.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.sdk.mocks.MockContent
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import java.util.Date
object WalletMockContent : MockContent {
private val primaryCard = PrimaryCard(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
linkingKey = byteArrayOf(
2, 121, 98, 127, -70, 14, 5, -23, -76, 115, -30, -26, 111, 17, 110, 34, -100, -121,
-57, -123, 74, 3, -91, 56, -20, 56, 50, -40, -101, 96, 82, 70, -91,
),
existingWalletsCount = 3, isHDWalletAllowed = true,
issuer = Card.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
walletCurves = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519, EllipticCurve.Bls12381G2Aug),
firmwareVersion = FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
isKeysImportAllowed = false,
certificate = null,
)
override val cardDto = CardDTO(
cardId = "AC05000000086747",
batchId = "AC05",
cardPublicKey = byteArrayOf(2, -120, -3, -32, -122, -127, -120, -104, 59, 72, 76, 114, 94, 75, -37, -55, 55, 99, 66, 123, 85, -87, 80, 106, 105, -116, 87, -83, -12, 70, 108, -68, -39),
firmwareVersion = CardDTO.FirmwareVersion(
major = 4,
minor = 52,
patch = 0,
type = FirmwareVersion.FirmwareType.Release,
),
manufacturer = CardDTO.Manufacturer(
name = "TANGEM",
manufactureDate = Date(1649635200000),
signature = byteArrayOf(-20, 98, -101, 94, -23, 73, 122, -21, 74, 76, 79, -55, -102, -62, -30, 44, -38, 118, 75, 121, -36, 118, -62, 60, -38, -63, 33, -14, -98, -69, 112, 22, 48, -43, 47, 65, -61, -56, 38, -94, -45, 44, 95, -22, 6, -31, -40, -25, 42, 38, 94, -99, 71, 98, -33, 27, -102, 30, 52, -7, 51, -27, 84, 66),
),
issuer = CardDTO.Issuer(
name = "TANGEM AG",
publicKey = byteArrayOf(3, 86, -25, -61, 55, 99, 41, -33, -82, 115, -120, -33, 22, -107, 103, 3, -122, 16, 60, -110, 72, 106, -121, 100, 79, -87, -27, 18, -55, -49, 78, -110, -2),
),
settings = CardDTO.Settings(
securityDelay = 15000,
maxWalletsCount = 20,
isSettingAccessCodeAllowed = false,
isSettingPasscodeAllowed = false,
isResettingUserCodesAllowed = true,
isLinkedTerminalEnabled = true,
isBackupAllowed = true,
supportedEncryptionModes = listOf(EncryptionMode.Strong, EncryptionMode.Fast, EncryptionMode.None),
isFilesAllowed = true,
isHDWalletAllowed = true,
isKeysImportAllowed = false,
),
userSettings = CardDTO.UserSettings(isUserCodeRecoveryAllowed = true),
linkedTerminalStatus = CardDTO.LinkedTerminalStatus.None,
isAccessCodeSet = false,
isPasscodeSet = false,
supportedCurves = listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
EllipticCurve.Bls12381G2,
EllipticCurve.Bls12381G2Aug,
EllipticCurve.Bls12381G2Pop,
EllipticCurve.Bip0340,
),
wallets = listOf(
CardDTO.Wallet(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
curve = EllipticCurve.Secp256k1,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 0,
hasBackup = false,
derivedKeys = mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
),
DerivationPath("m/44'/1'/0'/0/0") to ExtendedPublicKey(
publicKey = byteArrayOf(2, -40, 115, -15, 80, 104, 100, -29, 80, 29, 112, -35, -54, 64, -98, 45, 115, 108, 93, 113, -71, 89, 17, 72, 98, 21, 126, 82, -50, 50, -12, -87, -62),
chainCode = byteArrayOf(-27, 56, 57, 54, 27, -40, 65, 109, 119, -54, -94, 88, -29, -115, -45, -112, -31, 57, 53, 56, 118, 87, 34, -16, -64, -45, 105, 77, 91, -106, -92, 41),
),
),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
chainCode = byteArrayOf(80, 13, -8, -108, -35, 116, -92, 125, -65, -28, 85, 72, -113, 59, 83, 13, 5, -83, -102, 123, 124, -22, 94, 108, -71, 95, 65, -2, 38, 38, -108, 14),
),
isImported = false,
),
CardDTO.Wallet(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
curve = EllipticCurve.Ed25519,
settings = CardWallet.Settings(isPermanent = false),
totalSignedHashes = 0,
remainingSignatures = null,
index = 1,
hasBackup = false,
derivedKeys = emptyMap(),
extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(-65, -53, -62, -12, -57, -32, -38, -9, -128, -52, -83, -61, 73, 39, 41, 15, -74, -97, 38, 52, -101, 63, 74, -56, -20, 15, 57, -127, 114, -93, -17, -109),
chainCode = byteArrayOf(-81, 15, 125, 28, -115, -22, 87, 81, 87, -123, -25, -74, 86, 2, 1, 110, -115, 65, -110, 63, -64, 83, -93, -97, -104, 123, 12, -26, 94, 27, 84, -6),
),
isImported = false,
),
),
attestation = Attestation(
cardKeyAttestation = Attestation.Status.Verified,
walletKeysAttestation = Attestation.Status.Skipped,
firmwareAttestation = Attestation.Status.Skipped,
cardUniquenessAttestation = Attestation.Status.Skipped,
),
backupStatus = CardDTO.BackupStatus.NoBackup,
)
override val scanResponse = ScanResponse(
card = cardDto,
productType = ProductType.Wallet,
walletData = null,
secondTwinPublicKey = null,
derivedKeys = emptyMap(),
primaryCard = null,
)
override val derivationTaskResponse = DerivationTaskResponse(
entries = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/145'/0'/0/0") to ExtendedPublicKey( // bch
publicKey = byteArrayOf(2, 38, -6, 92, -37, -91, -59, -108, -18, -119, -55, 41, 38, -33, 44, 59, 24, -79, -14, -38, -10, -123, 106, 56, 39, 8, 112, 29, -41, 99, 70, -104, -121),
chainCode = byteArrayOf(105, -21, -61, -50, 68, -89, 119, 53, -96, -40, 119, 77, -122, 121, 16, 40, -50, -48, -105, -101, -74, -7, -94, -59, -90, 96, 59, 99, 43, -91, 115, -29),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/3'/0'/0/0") to ExtendedPublicKey( // doge
publicKey = byteArrayOf(3, -25, -24, -97, -124, 24, -89, 44, 75, 123, 92, -86, -73, -93, 25, -90, -89, -95, 88, 3, 107, 37, -1, -85, -32, -57, -123, -41, 108, -9, -96, 77, -124),
chainCode = byteArrayOf(119, 3, 41, 112, 71, 54, 72, 30, 39, 25, 25, -104, 92, 46, -109, 63, 93, 67, 43, -102, -87, 39, -95, 106, 45, 67, 109, -29, -35, 10, -107, 104),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
)
override val extendedPublicKey = ExtendedPublicKey(
publicKey = byteArrayOf(3, 2, 95, 53, 40, -87, -60, 11, -8, -47, 41, 37, 100, 15, -69, 1, -122, 127, -20, -81, -32, -20, -24, 5, -28, 113, 106, -90, -59, -30, -27, -110, -110),
chainCode = byteArrayOf(-95, -87, -95, -25, 27, 96, -57, -92, -69, -106, -45, 10, 85, 4, -92, -68, 49, -24, -28, -50, -49, -77, -20, 118, -50, -27, 104, -93, 115, -50, -46, -34),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
)
override val successResponse = SuccessResponse(cardId = "AC05000000086747")
override val createProductWalletTaskResponse = CreateProductWalletTaskResponse(
card = cardDto,
derivedKeys = mapOf(
ByteArrayKey(
byteArrayOf(2, -109, 28, -27, -124, -58, -97, -61, 43, -84, 90, -9, -5, 4, 90, 17, 112, -125, -108, 44, 19, -79, -60, -23, 34, -20, -20, 61, 84, 113, 120, -90, -5),
)
to
ExtendedPublicKeysMap(
mapOf(
DerivationPath("m/44'/0'/0'/0/0") to ExtendedPublicKey( // btc
publicKey = byteArrayOf(3, 45, 58, -110, -52, -51, -83, -4, -45, -118, 119, 37, 123, -17, 66, -83, 61, -106, 115, 47, 121, 66, 84, -122, -57, -45, 7, -79, 70, -13, 28, -125, -52),
chainCode = byteArrayOf(93, 51, 52, -66, -39, -38, 34, -84, 50, 1, -127, -20, 80, -20, -30, -72, 2, 1, -78, -81, -17, 51, -52, -25, 12, 108, 50, 89, -66, 18, 65, 70),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
DerivationPath("m/44'/60'/0'/0/0") to ExtendedPublicKey( // eth
publicKey = byteArrayOf(2, 34, 6, 119, -106, 5, -119, 111, -22, 8, 23, -108, -72, -56, 6, 77, -17, -61, -101, -85, 16, 28, 18, 3, -3, -89, -81, -108, 48, -7, -86, -82, -67),
chainCode = byteArrayOf(-75, 55, 107, -106, -37, -81, -15, 72, -102, 94, 55, -39, 9, -112, 1, 90, -50, 103, 53, 120, -92, -36, -85, -39, -65, 1, 88, 46, 92, 104, -13, -109),
depth = 0,
parentFingerprint = byteArrayOf(0, 0, 0, 0),
childNumber = 0,
),
),
),
),
primaryCard = primaryCard,
)
override val importWalletResponse: CreateProductWalletTaskResponse
get() = error("Available only for Wallet 2")
override val createFirstTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val createSecondTwinResponse: CreateWalletResponse
get() = error("Available only for Twin")
override val finalizeTwinResponse: ScanResponse
get() = error("Available only for Twin")
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.domain.settings
import com.tangem.domain.settings.repositories.LegacySettingsRepository
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.sdk.TangemSdkManager
internal class DefaultLegacySettingsRepository(
private val tangemSdkManager: TangemSdkManager,

View file

@ -3,13 +3,13 @@ package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.derivation.DerivationStyle
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.datasource.local.token.UserTokensStore
import com.tangem.domain.common.DerivationStyleProvider
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.utils.coroutines.CoroutineDispatcherProvider

View file

@ -9,7 +9,7 @@ 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.datasource.asset.reader.AssetReader
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse
@ -27,8 +27,7 @@ class TwinCardsManager(
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
val response = tangemSdkManager.runTaskAsync(
runnable = CreateFirstTwinWalletTask(firstCardId),
val response = tangemSdkManager.createFirstTwinWallet(
cardId = firstCardId,
initialMessage = message,
)
@ -44,14 +43,15 @@ class TwinCardsManager(
preparingMessage: Message,
creatingWalletMessage: Message,
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
val response = tangemSdkManager.createSecondTwinWallet(
firstPublicKey = currentCardPublicKey!!,
firstCardId = firstCardId,
issuerKeys = issuerKeyPair,
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage,
initialMessage = initialMessage,
)
val response = tangemSdkManager.runTaskAsync(task, null, initialMessage)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
@ -62,8 +62,9 @@ class TwinCardsManager(
}
suspend fun complete(message: Message): Result<ScanResponse> {
val response = tangemSdkManager.runTaskAsync(
runnable = FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
val response = tangemSdkManager.finalizeTwin(
secondCardPublicKey = secondCardPublicKey!!.hexToBytes(),
issuerKeyPair = issuerKeyPair,
cardId = firstCardId,
initialMessage = message,
)
@ -88,6 +89,7 @@ class TwinCardsManager(
)
}
@Deprecated(message = "Use AssetReader instead")
private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readJson(fileName = "tangem-app-config/issuers")
return getAdapter().fromJson(file)!!

View file

@ -1,12 +0,0 @@
package com.tangem.tap.domain.userWalletList
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
internal class DefaultUserWalletsListManagerFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : UserWalletsListManagerFeatureToggles {
override val isGeneralManagerEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "GENERAL_USER_WALLETS_LIST_MANAGER_ENABLED")
}

View file

@ -1,23 +0,0 @@
package com.tangem.tap.domain.userWalletList.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.domain.wallets.legacy.UserWalletsListManagerFeatureToggles
import com.tangem.tap.domain.userWalletList.DefaultUserWalletsListManagerFeatureToggles
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object UserWalletsListManagerFeatureTogglesModule {
@Provides
@Singleton
fun provideUserWalletsListManagerFeatureToggles(
featureTogglesManager: FeatureTogglesManager,
): UserWalletsListManagerFeatureToggles {
return DefaultUserWalletsListManagerFeatureToggles(featureTogglesManager)
}
}

View file

@ -66,7 +66,7 @@ internal object UserWalletsListManagerModule {
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = USER_WALLETS_STORAGE_NAME,
storageName = "user_wallets_storage",
),
)

View file

@ -1,86 +0,0 @@
package com.tangem.tap.domain.userWalletList.di
import android.content.Context
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.common.authentication.storage.AuthenticatedStorage
import com.tangem.common.json.TangemSdkAdapter
import com.tangem.common.services.secure.SecureStorage
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.tap.domain.userWalletList.implementation.BiometricUserWalletsListManager
import com.tangem.tap.domain.userWalletList.implementation.RuntimeUserWalletsListManager
import com.tangem.tap.domain.userWalletList.repository.DelegatedKeystoreManager
import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysStoreDecorator
import com.tangem.tap.domain.userWalletList.repository.implementation.BiometricUserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultSelectedUserWalletRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.*
import com.tangem.tap.tangemSdkManager
import com.tangem.utils.Provider
internal const val USER_WALLETS_STORAGE_NAME = "user_wallets_storage"
fun UserWalletsListManager.Companion.provideBiometricImplementation(
applicationContext: Context,
): UserWalletsListManager {
val moshi = Moshi.Builder()
.add(WalletDerivedKeysMapAdapter())
.add(ScanResponseDerivedKeysMapAdapter())
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())
.build()
val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = USER_WALLETS_STORAGE_NAME,
),
)
val authenticatedStorage = AuthenticatedStorage(
secureStorage = UserWalletsKeysStoreDecorator(
featureStorage = secureStorage,
cardSdkStorageProvider = Provider { tangemSdkManager.secureStorage },
),
keystoreManager = DelegatedKeystoreManager(
keystoreManagerProvider = Provider { tangemSdkManager.keystoreManager },
),
)
val keysRepository = BiometricUserWalletsKeysRepository(
moshi = moshi,
secureStorage = secureStorage,
authenticatedStorage = authenticatedStorage,
)
val publicInformationRepository = DefaultUserWalletsPublicInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val sensitiveInformationRepository = DefaultUserWalletsSensitiveInformationRepository(
moshi = moshi,
secureStorage = secureStorage,
)
val selectedUserWalletRepository = DefaultSelectedUserWalletRepository(
secureStorage = secureStorage,
)
return BiometricUserWalletsListManager(
keysRepository = keysRepository,
publicInformationRepository = publicInformationRepository,
sensitiveInformationRepository = sensitiveInformationRepository,
selectedUserWalletRepository = selectedUserWalletRepository,
)
}
fun UserWalletsListManager.Companion.provideRuntimeImplementation(): UserWalletsListManager {
return RuntimeUserWalletsListManager()
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.domain.userWalletList.repository.UserWalletsKeysRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsPublicInformationRepository
import com.tangem.tap.domain.userWalletList.repository.UserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.encryptionKey
import com.tangem.tap.domain.userWalletList.utils.lockAll
import com.tangem.tap.domain.userWalletList.utils.toUserWallets
import com.tangem.tap.domain.userWalletList.utils.updateWith
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -27,11 +28,16 @@ internal class BiometricUserWalletsListManager(
) : UserWalletsListManager.Lockable {
private val state = MutableStateFlow(State())
override val isLockable: Boolean = true
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { it.userWallets }
.distinctUntilChanged()
override val userWalletsSync: List<UserWallet>
get() = state.value.userWallets
override val selectedUserWallet: Flow<UserWallet>
get() = state
.mapLatest { state ->
@ -78,11 +84,13 @@ internal class BiometricUserWalletsListManager(
}
override fun lock() {
state.update { State() }
}
override fun isLockable(): Boolean {
return true
state.update { prevState ->
prevState.copy(
encryptionKeys = emptyList(),
userWallets = prevState.userWallets.lockAll(),
isLocked = true,
)
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {

View file

@ -33,31 +33,52 @@ internal class GeneralUserWalletsListManager(
) : UserWalletsListManager.Lockable {
private val applicationScope = CoroutineScope(dispatchers.io)
private val implementation = MutableStateFlow(runtimeUserWalletsListManager)
private val implementation: MutableStateFlow<UserWalletsListManager?> = MutableStateFlow(value = null)
private val requireImplementation: UserWalletsListManager
get() = requireNotNull(implementation.value) {
"UserWalletsListManager is not initialized"
}
init {
subscribeOnCurrentManager()
}
override val isLockable: Boolean
get() = requireImplementation.isLockable
override val userWallets: Flow<List<UserWallet>>
get() = implementation.flatMapLatest { it.userWallets }
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.userWallets)
}
}
override val userWalletsSync: List<UserWallet>
get() = requireImplementation.userWalletsSync
override val selectedUserWallet: Flow<UserWallet>
get() = implementation.flatMapLatest { it.selectedUserWallet }
get() = implementation.transformLatest { impl ->
if (impl != null && impl.hasUserWallets) {
emitAll(impl.selectedUserWallet)
}
}
override val selectedUserWalletSync: UserWallet?
get() = implementation.value.selectedUserWalletSync
get() = requireImplementation.selectedUserWalletSync
override val hasUserWallets: Boolean
get() = implementation.value.hasUserWallets
get() = requireImplementation.hasUserWallets
override val walletsCount: Int
get() = implementation.value.walletsCount
get() = requireImplementation.walletsCount
override val isLocked: Flow<Boolean>
get() = implementation.flatMapLatest {
if (it is UserWalletsListManager.Lockable) {
it.isLocked
get() = implementation.transformLatest { impl ->
if (impl == null) return@transformLatest
if (impl is UserWalletsListManager.Lockable) {
emitAll(impl.isLocked)
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
@ -65,43 +86,45 @@ internal class GeneralUserWalletsListManager(
override val isLockedSync: Boolean
get() {
val implementation = implementation.value
return if (implementation is UserWalletsListManager.Lockable) {
implementation.isLockedSync
val impl = requireImplementation
return if (impl is UserWalletsListManager.Lockable) {
impl.isLockedSync
} else {
error("RuntimeUserWalletsListManager is not lockable")
}
}
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.select(userWalletId)
return requireImplementation.select(userWalletId)
}
override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult<Unit> {
return implementation.value.save(userWallet, canOverride)
return requireImplementation.save(userWallet, canOverride)
}
override suspend fun update(
userWalletId: UserWalletId,
update: suspend (UserWallet) -> UserWallet,
): CompletionResult<UserWallet> {
return implementation.value.update(userWalletId, update)
return requireImplementation.update(userWalletId, update)
}
override suspend fun delete(userWalletIds: List<UserWalletId>): CompletionResult<Unit> {
return implementation.value.delete(userWalletIds)
return requireImplementation.delete(userWalletIds)
}
override suspend fun clear(): CompletionResult<Unit> {
return implementation.value.clear()
return requireImplementation.clear()
}
override suspend fun get(userWalletId: UserWalletId): CompletionResult<UserWallet> {
return implementation.value.get(userWalletId)
return requireImplementation.get(userWalletId)
}
override suspend fun unlock(type: UserWalletsListManager.Lockable.UnlockType): CompletionResult<UserWallet> {
val implementation = implementation.value
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.unlock(type)
} else {
@ -110,7 +133,8 @@ internal class GeneralUserWalletsListManager(
}
override fun lock() {
val implementation = implementation.value
val implementation = requireImplementation
return if (implementation is UserWalletsListManager.Lockable) {
implementation.lock()
} else {
@ -118,10 +142,6 @@ internal class GeneralUserWalletsListManager(
}
}
override fun isLockable(): Boolean {
return implementation.value.isLockable()
}
private fun subscribeOnCurrentManager() {
appPreferencesStore.get(key = PreferencesKeys.SAVE_USER_WALLETS_KEY, default = false)
.distinctUntilChanged()
@ -144,17 +164,17 @@ internal class GeneralUserWalletsListManager(
destinationManager = possibleManager,
)
previousManager.clear()
previousManager?.clear()
}
.flowOn(dispatchers.io)
.launchIn(applicationScope)
}
private suspend fun copySelectedUserWallet(
sourceManager: UserWalletsListManager,
sourceManager: UserWalletsListManager?,
destinationManager: UserWalletsListManager,
): UserWalletsListManager {
sourceManager.selectedUserWalletSync?.let { selectedWallet ->
sourceManager?.selectedUserWalletSync?.let { selectedWallet ->
destinationManager.save(selectedWallet, canOverride = true)
}

View file

@ -13,6 +13,8 @@ import kotlinx.coroutines.flow.*
internal class RuntimeUserWalletsListManager : UserWalletsListManager {
private val state = MutableStateFlow(State())
override val isLockable: Boolean = false
override val userWallets: Flow<List<UserWallet>>
get() = state
.mapLatest { listOfNotNull(it.userWallet) }
@ -24,6 +26,9 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
.filterNotNull()
.distinctUntilChanged()
override val userWalletsSync: List<UserWallet>
get() = listOfNotNull(state.value.userWallet)
override val selectedUserWalletSync: UserWallet?
get() = state.value.userWallet
@ -34,7 +39,7 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
* only 1 wallet stored in runtime implementation
*/
override val walletsCount: Int
get() = 1
get() = if (hasUserWallets) 1 else 0
override suspend fun select(userWalletId: UserWalletId): CompletionResult<UserWallet> = catching {
state.value.userWallet
@ -85,10 +90,6 @@ internal class RuntimeUserWalletsListManager : UserWalletsListManager {
state.value.userWallet ?: walletNotFound()
}
override fun isLockable(): Boolean {
return false
}
private fun saveInternal(userWallet: UserWallet): CompletionResult<Unit> = catching {
state.update { prevState ->
prevState.copy(

View file

@ -61,4 +61,14 @@ internal fun List<UserWallet>.updateWith(
?: wallet
}
}
}
}
internal fun List<UserWallet>.lockAll(): List<UserWallet> = map(UserWallet::lock)
internal fun UserWallet.lock(): UserWallet = copy(
scanResponse = scanResponse.copy(
card = scanResponse.card.copy(
wallets = emptyList(),
),
),
)

View file

@ -10,13 +10,11 @@ import com.tangem.tap.domain.walletconnect2.domain.models.binance.WcBinanceTrans
import com.tangem.tap.domain.walletconnect2.domain.models.binance.tradeOrderSerializer
import com.tangem.tap.features.details.redux.walletconnect.BinanceMessageData
import com.tangem.tap.features.details.redux.walletconnect.TradeData
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import timber.log.Timber
internal object BnbHelper {
fun createMessageData(order: WCBinanceTransferOrder): BinanceMessageData.Transfer {
fun createMessageData(order: WcBinanceTransferOrder): BinanceMessageData.Transfer {
val input = order.msgs.first().inputs.first()
val output = order.msgs.first().inputs.first()
@ -42,51 +40,7 @@ internal object BnbHelper {
)
}
fun WcBinanceTradeOrder.toWCBinanceTradeOrder(): WCBinanceTradeOrder {
return WCBinanceTradeOrder(
account_number = accountNumber,
chain_id = chainId,
data = data,
memo = memo,
sequence = sequence,
source = source,
msgs = msgs.map { it.toWCBinanceTradeOrderMessage() },
)
}
private fun WcBinanceTradeOrder.Message.toWCBinanceTradeOrderMessage(): WCBinanceTradeOrder.Message {
return WCBinanceTradeOrder.Message(id, orderType, price, quantity, sender, side, symbol, timeInforce)
}
fun WcBinanceTransferOrder.toWCBinanceTransferOrder(): WCBinanceTransferOrder {
return WCBinanceTransferOrder(
account_number = accountNumber,
chain_id = chainId,
data = data,
memo = memo,
sequence = sequence,
source = source,
msgs = msgs.map { it.toWCBinanceTransferOrderMessage() },
)
}
private fun WcBinanceTransferOrder.Message.toWCBinanceTransferOrderMessage(): WCBinanceTransferOrder.Message {
return WCBinanceTransferOrder.Message(
inputs.map { it.toWCBinanceItem() },
outputs.map { it.toWCBinanceItem() },
)
}
private fun WcBinanceTransferOrder.Message.Item.toWCBinanceItem(): WCBinanceTransferOrder.Message.Item {
return WCBinanceTransferOrder.Message.Item(
address,
coins.map {
WCBinanceTransferOrder.Message.Item.Coin(it.amount, it.denom)
},
)
}
fun createMessageData(order: WCBinanceTradeOrder): BinanceMessageData.Trade {
fun createMessageData(order: WcBinanceTradeOrder): BinanceMessageData.Trade {
val address = order.msgs.first().sender
val tradeData = order.msgs.map {

View file

@ -1,562 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.datasource.api.common.createNetworkLoggingInterceptor
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.walletconnect.extensions.isDappSupported
import com.tangem.tap.domain.walletconnect.extensions.toWcEthereumSignMessage
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.walletConnectRepository
import com.trustwallet.walletconnect.WCClient
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.binance.WCBinanceCancelOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTradeOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTransferOrder
import com.trustwallet.walletconnect.models.binance.WCBinanceTxConfirmParam
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
import com.trustwallet.walletconnect.models.session.WCAddNetwork
import com.trustwallet.walletconnect.models.session.WCSession
import com.trustwallet.walletconnect.models.session.WCSessionUpdate
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import timber.log.Timber
import java.util.UUID
import java.util.concurrent.TimeUnit
import kotlin.collections.set
@Suppress("LargeClass")
internal class WalletConnectManager {
private var cardId: String? = null
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(interceptor)
.addInterceptor(RetryInterceptor())
.build()
}
private val interceptor by lazy {
createNetworkLoggingInterceptor()
}
private var sessions: MutableMap<Topic, WalletConnectActiveData> = mutableMapOf()
fun connect(wcUri: String) {
val session = WCSession.from(wcUri).guard {
store.dispatchOnMain(
WalletConnectAction.FailureEstablishingSession(
session = null,
error = TapError.WalletConnect.UnsupportedLink,
),
)
return
}
if (sessions[session.topic] != null) {
store.dispatchOnMain(WalletConnectAction.RefuseOpeningSession)
return
}
val client = WCClient(httpClient = okHttpClient)
setListeners(client)
val peerId = UUID.randomUUID().toString()
try {
client.connect(session, tangemPeerMeta, peerId)
} catch (exception: IllegalArgumentException) {
store.dispatchOnMain(
WalletConnectAction.FailureEstablishingSession(
session = null,
error = TapError.WalletConnect.UnsupportedLink,
),
)
return
}
sessions[session.topic] = WalletConnectActiveData(
peerId = peerId,
remotePeerId = null,
session = session,
client = client,
wallet = WalletForSession(),
)
setupConnectionTimeoutCheck(session)
}
fun updateSession(session: WalletConnectSession) {
val updatedSession = sessions[session.session.topic]?.copy(
wallet = session.wallet,
)
if (updatedSession != null) {
sessions[session.session.topic] = updatedSession
}
}
fun updateBlockchain(session: WalletConnectSession) {
sessions[session.session.topic]?.client?.updateSession(
accounts = listOfNotNull(session.getAddress()),
chainId = session.wallet.blockchain?.getChainId(),
approved = true,
)
val updatedSession = sessions[session.session.topic]?.copy(
wallet = session.wallet,
)
if (updatedSession != null) {
sessions[session.session.topic] = updatedSession
}
}
@Suppress("MagicNumber")
private fun setupConnectionTimeoutCheck(session: WCSession) {
scope.launch {
delay(20_000)
val data = sessions[session.topic]
if (data != null && data.peerMeta == null) {
disconnect(session)
store.dispatchOnMain(WalletConnectAction.OpeningSessionTimeout(session))
}
}
}
fun restoreSessions(scanResponse: ScanResponse) {
val walletPublicKey = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 }?.publicKey
?: return
if (scanResponse.card.backupStatus?.isActive != true) cardId = scanResponse.card.cardId
val sessions = walletConnectRepository.loadSavedSessions()
// filter sessions for this particular card
.filter { it.wallet.walletPublicKey.contentEquals(walletPublicKey) }
this.sessions = sessions
.map { session ->
WalletConnectActiveData(
peerId = session.peerId,
remotePeerId = session.remotePeerId,
client = WCClient(httpClient = okHttpClient),
session = session.session,
peerMeta = session.peerMeta,
wallet = session.wallet,
)
.also {
setListeners(it.client)
it.client.connect(it.session, tangemPeerMeta, it.peerId, it.remotePeerId)
}
}.associateBy { it.session.topic }.toMutableMap()
store.dispatchOnMain(WalletConnectAction.SetSessionsRestored(sessions))
}
fun approve(session: WCSession) {
val activeData = sessions[session.topic] ?: return
removeSimilarSessions(activeData)
val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey ?: return
val blockchain = activeData.wallet.getBlockchainForSession()
val accounts = listOf(blockchain.makeAddresses(key).first().value)
val approved = activeData.client.approveSession(
accounts = accounts,
chainId = blockchain.getChainId() ?: Blockchain.Ethereum.getChainId()!!,
)
if (approved) {
val walletConnectSession = WalletConnectSession(
peerId = activeData.peerId,
remotePeerId = activeData.remotePeerId,
wallet = activeData.wallet,
session = session,
peerMeta = activeData.peerMeta!!,
)
walletConnectRepository.saveSession(walletConnectSession)
store.dispatchOnMain(
WalletConnectAction.ApproveSession.Success(
walletConnectSession,
),
)
}
}
private fun removeSimilarSessions(activeData: WalletConnectActiveData) {
val sessionsToRemove = sessions.filter {
it.value.wallet.walletPublicKey?.equals(activeData.wallet.walletPublicKey) == true &&
it.value.peerMeta?.url == activeData.peerMeta?.url &&
it.value.session != activeData.session
}
Timber.d("RemoveSimilarSessions: ${sessionsToRemove.values.map { it.client.session }}")
sessionsToRemove.forEach { disconnect(it.value.session) }
}
fun rejectRequest(topic: String, id: Long) {
val activeData = sessions[topic] ?: return
activeData.client.rejectRequest(id)
}
private fun acceptRequest(topic: String, id: Long, data: String) {
val activeData = sessions[topic] ?: return
activeData.client.approveRequest(id, data)
}
fun disconnect(session: WCSession) {
val activeData = sessions[session.topic] ?: return
val disconnected = if (activeData.client.isConnected) {
activeData.client.killSession()
} else {
true
}
if (disconnected) {
onSessionClosed(session)
}
}
private fun onSessionClosed(session: WCSession) {
sessions.remove(session.topic)
walletConnectRepository.removeSession(session)
store.dispatchOnMain(WalletConnectAction.RemoveSession(session))
}
fun handleTransactionRequest(
transaction: WcEthereumTransaction,
session: WalletConnectSession,
id: Long,
type: WcEthTransactionType,
) {
val activeData = sessions[session.session.topic] ?: return
scope.launch {
val data = WalletConnectSdkHelper().prepareTransactionData(
EthTransactionData(
transaction = transaction,
networkId = session.wallet.blockchain?.toNetworkId() ?: "",
rawDerivationPath = session.wallet.derivationPath?.rawPath,
id = id,
topic = session.session.topic,
type = type,
metaName = session.peerMeta.name,
metaUrl = session.peerMeta.url,
),
).guard {
sessions[session.session.topic] = activeData.copy(transactionData = null)
store.dispatchOnMain(
WalletConnectAction.RejectRequest(
session.session.topic,
id,
),
)
return@launch
}
sessions[session.session.topic] = activeData.copy(transactionData = data)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.RequestTransaction(
WcPreparedRequest.EthTransaction(
preparedRequestData = data,
topic = session.session.topic,
requestId = id,
derivationPath = data.walletManager.wallet.publicKey.derivationPath?.rawPath,
),
),
),
)
}
}
fun completeTransaction(topic: Topic) {
val activeData = sessions[topic]
val data = activeData?.transactionData ?: return
scope.launch {
val hash = WalletConnectSdkHelper().completeTransaction(data, cardId).guard {
sessions[topic] = activeData.copy(transactionData = null)
store.dispatchOnMain(
WalletConnectAction.RejectRequest(
topic,
data.id,
),
)
return@launch
}
acceptRequest(topic, data.id, hash)
sessions[topic] = activeData.copy(transactionData = null)
}
}
fun signBnb(id: Long, data: ByteArray, topic: Topic) {
val activeData = sessions[topic] ?: return
scope.launch {
val hash = WalletConnectSdkHelper().signBnbTransaction(
data = data,
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
derivationPath = activeData.wallet.derivationPath?.rawPath,
cardId = cardId,
).guard {
store.dispatchOnMain(
WalletConnectAction.RejectRequest(
topic,
id,
),
)
return@launch
}
acceptRequest(topic, id, hash)
}
}
fun handlePersonalSignRequest(message: WCEthereumSignMessage, session: WalletConnectSession, id: Long) {
val activeData = sessions[session.session.topic] ?: return
scope.launch {
val data = WalletConnectSdkHelper().prepareDataForPersonalSign(
message = message.toWcEthereumSignMessage(),
topic = session.session.topic,
id = id,
metaName = session.peerMeta.name,
)
sessions[session.session.topic] = activeData.copy(personalSignData = data)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.PersonalSign(
WcPreparedRequest.EthSign(
preparedRequestData = data,
topic = session.session.topic,
requestId = id,
derivationPath = session.wallet.derivationPath?.rawPath,
),
),
),
)
}
}
fun sendSignedMessage(topic: Topic) {
val activeData = sessions[topic]
val data = activeData?.personalSignData ?: return
scope.launch {
val hash = WalletConnectSdkHelper().signPersonalMessage(
hashToSign = data.hash,
networkId = activeData.wallet.blockchain?.toNetworkId() ?: "",
type = data.type,
derivationPath = activeData.wallet.derivationPath?.rawPath,
cardId = cardId,
)
.guard {
sessions[topic] = activeData.copy(transactionData = null)
store.dispatchOnMain(
WalletConnectAction.RejectRequest(
topic,
data.id,
),
)
return@launch
}
sessions[topic] = activeData.copy(personalSignData = data)
acceptRequest(topic, data.id, hash)
sessions[topic] = activeData.copy(transactionData = null)
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun setListeners(client: WCClient) {
client.onSessionRequest = { id: Long, peer: WCPeerMeta ->
Timber.d("OnSessionRequest: $peer")
val session = client.session
val data = sessions[session?.topic]?.copy(peerMeta = peer, remotePeerId = client.remotePeerId)
if (data != null && session != null) {
if (!peer.isDappSupported()) {
store.dispatchOnMain(
WalletConnectAction.FailureEstablishingSession(
session = session,
error = TapError.WalletConnect.UnsupportedDapp,
),
)
} else {
sessions[session.topic] = data
val sessionData = data.toWalletConnectSession()
sessionData?.let {
store.dispatchOnMain(
WalletConnectAction.ScanCard(
session = sessionData,
chainId = client.chainId?.toIntOrNull(),
),
)
}
}
}
}
client.onSessionUpdate = { id: Long, update: WCSessionUpdate ->
Timber.d("onSessionUpdate: $update")
val session = client.session
if (session != null && !update.approved) onSessionClosed(session)
}
client.onEthSendTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSendTransaction: $transaction")
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.SendTransaction
// )
// )
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
session = sessionData,
id = id,
type = WcEthTransactionType.EthSendTransaction,
),
)
}
}
client.onEthSignTransaction = { id: Long, transaction: WCEthereumTransaction ->
Timber.d("onEthSignTransaction: $transaction")
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.SignTransaction
// )
// )
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandleTransactionRequest(
transaction = transaction,
session = sessionData,
id = id,
type = WcEthTransactionType.EthSignTransaction,
),
)
}
}
client.onEthSign = { id: Long, message: WCEthereumSignMessage ->
Timber.d("onEthSign: $message")
// Analytics.logWcEvent(
// AnalyticsAnOld.WcAnalyticsEvent.Action(
// AnalyticsAnOld.WcAction.PersonalSign
// )
// )
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.HandlePersonalSignRequest(
message,
sessionData,
id,
),
)
}
}
client.onBnbCancel = { id: Long, order: WCBinanceCancelOrder ->
}
client.onBnbTrade = { id: Long, order: WCBinanceTradeOrder ->
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.BinanceTransaction.Trade(
id = id,
order = order,
sessionData = sessionData,
),
)
}
}
client.onBnbTransfer = { id: Long, order: WCBinanceTransferOrder ->
sessions[client.session?.topic]?.toWalletConnectSession()?.let { sessionData ->
store.dispatchOnMain(
WalletConnectAction.BinanceTransaction.Transfer(
id = id,
order = order,
sessionData = sessionData,
),
)
}
}
client.onBnbTxConfirm = { id: Long, order: WCBinanceTxConfirmParam ->
// send empty approve request if status is OK
if (order.ok) client.approveRequest(id, "")
}
client.onDisconnect = { code: Int, reason: String ->
val session = client.session
if (session != null) {
onSessionClosed(session)
}
}
client.onWalletChangeNetwork = { id: Long, chainId: Int ->
switchChain(chainId, client)
}
client.onWalletAddNetwork = { id: Long, network: WCAddNetwork ->
// TODO
// In fact this method is used to add a EVM network. It provides RPC url and chain ID.
// Here now it just tries to switch to a EVM network with a provided chain ID.
try {
val chainId = Integer.decode(network.chainIdHex)
switchChain(chainId, client)
} catch (exception: Exception) {
Timber.d("WC add network error: chain could not be parsed")
}
}
}
private fun switchChain(chainId: Int, client: WCClient) {
val blockchain = Blockchain.fromChainId(chainId)
Timber.d("WC switch chainID\nNew Blockchain: $blockchain")
val session = sessions[client.session?.topic]?.toWalletConnectSession()
if (session != null) {
store.dispatchOnMain(WalletConnectAction.SwitchBlockchain(blockchain, session))
}
}
companion object {
const val WC_SCHEME = "wc"
private val tangemPeerMeta = WCPeerMeta(name = "Tangem Wallet", url = "https://tangem.com")
fun isCorrectWcUri(string: String): Boolean = WCSession.from(string) != null
}
}
internal typealias Topic = String
internal data class WalletConnectActiveData(
val peerId: String,
val remotePeerId: String?,
val client: WCClient,
val session: WCSession,
val peerMeta: WCPeerMeta? = null,
val wallet: WalletForSession,
val transactionData: WcTransactionData? = null,
val personalSignData: WcPersonalSignData? = null,
) {
fun toWalletConnectSession(): WalletConnectSession? {
if (peerMeta == null) return null
return WalletConnectSession(
peerId = peerId,
remotePeerId = remotePeerId,
wallet = wallet,
session = session,
peerMeta = peerMeta,
)
}
}
@Suppress("MagicNumber")
internal class RetryInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val response = chain.proceed(request)
when (response.code) {
502 -> {
return chain.proceed(request)
}
}
return response
}
}

View file

@ -1,41 +0,0 @@
package com.tangem.tap.domain.walletconnect
import com.tangem.blockchain.common.Blockchain
import com.trustwallet.walletconnect.models.WCPeerMeta
object WalletConnectNetworkUtils {
fun parseBlockchain(chainId: Int?, peer: WCPeerMeta): Blockchain? {
return when {
peer.url.contains("pancakeswap.finance") -> {
Blockchain.BSC
}
peer.url.contains("optimism") -> {
Blockchain.Optimism
}
chainId != null -> {
Blockchain.fromChainId(chainId)
}
peer.url.contains("matic.network") || peer.name == "Polygon" -> {
Blockchain.Polygon
}
peer.url.contains("binance.org") || peer.name.contains("Binance") -> {
if (peer.icons.firstOrNull()?.contains("testnet") == true) {
Blockchain.BinanceTestnet
} else {
Blockchain.Binance
}
}
peer.name.contains("BSC") -> {
Blockchain.BSC
}
peer.url.contains("honeyswap.1hive.eth.limo") -> {
// Check if something's changed after this bug report:
// https://github.com/1Hive/honeyswap-interface/issues/83
Blockchain.Gnosis
}
else -> {
Blockchain.Ethereum
}
}
}
}

View file

@ -1,105 +0,0 @@
package com.tangem.tap.domain.walletconnect
import android.app.Application
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
import timber.log.Timber
import java.io.FileNotFoundException
import java.nio.charset.Charset
class WalletConnectRepository(val context: Application) {
private val walletConnectAdapter: JsonAdapter<List<SessionDao>> = MoshiConverter.sdkMoshi.adapter(
Types.newParameterizedType(List::class.java, SessionDao::class.java),
)
fun saveSession(session: WalletConnectSession) {
val sessions = loadSavedSessions() + session
saveSessions(sessions)
}
fun removeSession(session: WCSession) {
val sessions = loadSavedSessions().filterNot { it.session == session }
saveSessions(sessions)
}
fun loadSavedSessions(): List<WalletConnectSession> {
return try {
val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS)
.hexToUtf8()
walletConnectAdapter.fromJson(json)!!.map { it.toSession() }
} catch (e: FileNotFoundException) {
emptyList()
} catch (exception: Exception) {
Timber.w(exception)
emptyList()
}
}
private fun saveSessions(sessions: List<WalletConnectSession>) {
val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) })
.utf8ToHex() // convert to hex to solve problems with saving text with emojis
Timber.e("WC sessions, saving following json: $json")
context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS)
}
private fun String.utf8ToHex(): String {
return this.toByteArray().toHexString()
}
private fun String.hexToUtf8(): String {
return this.hexToBytes().toString(Charset.defaultCharset())
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()
private fun Context.rewriteFile(content: String, fileName: String) {
this.openFileOutput(fileName, Context.MODE_PRIVATE).use {
it.write(content.toByteArray(), 0, content.length)
}
}
companion object {
private const val FILE_NAME_PREFIX_SESSIONS = "wc_sessions"
}
}
@JsonClass(generateAdapter = true)
data class SessionDao(
val peerId: String,
val remotePeerId: String?,
val wallet: WalletForSession,
val session: WCSession,
val peerMeta: WCPeerMeta,
) {
fun toSession(): WalletConnectSession {
return WalletConnectSession(
peerId = peerId,
remotePeerId = remotePeerId,
wallet = wallet,
session = session,
peerMeta = peerMeta,
)
}
companion object {
fun fromSession(session: WalletConnectSession): SessionDao {
return SessionDao(
peerId = session.peerId,
remotePeerId = session.remotePeerId,
wallet = session.wallet,
session = session.session,
peerMeta = session.peerMeta,
)
}
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.extensions.*
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toDecompressedPublicKey
@ -15,13 +16,10 @@ import com.tangem.common.extensions.toHexString
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.models.Basic
import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTradeOrder
import com.tangem.tap.domain.walletconnect.BnbHelper.toWCBinanceTransferOrder
import com.tangem.tap.domain.walletconnect2.domain.TransactionType
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
@ -38,7 +36,6 @@ import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionReque
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userWalletsListManager
import timber.log.Timber
import java.math.BigDecimal
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
@ -46,20 +43,35 @@ import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
@Suppress("LargeClass")
class WalletConnectSdkHelper {
private val userWalletsListManager by lazy {
store.inject(DaggerGraphState::generalUserWalletsListManager)
}
@Suppress("MagicNumber")
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData? {
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData {
val transaction = data.transaction
val blockchain = Blockchain.fromNetworkId(data.networkId) ?: return null
val walletManager = getWalletManager(blockchain, data.rawDerivationPath) ?: return null
val blockchain = requireNotNull(Blockchain.fromNetworkId(data.networkId)) {
"Blockchain not found"
}
val walletManager = requireNotNull(getWalletManager(blockchain, data.rawDerivationPath)) {
"WalletManager not found"
}
walletManager.safeUpdate(isDemoCard())
val wallet = walletManager.wallet
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
val balance = requireNotNull(wallet.amounts[AmountType.Coin]?.value) {
"Coin balance not found"
}
val decimals = wallet.blockchain.decimals()
val value = (transaction.value ?: "0").hexToBigDecimal()
.movePointLeft(decimals) ?: return null
val value = (transaction.value ?: "0")
.hexToBigDecimal()
.movePointLeft(decimals)
requireNotNull(value) {
"Transaction amount is null"
}
val gasLimit = getGasLimitFromTx(value, walletManager, transaction)
@ -68,20 +80,23 @@ class WalletConnectSdkHelper {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
(result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") }
return null
error("Unable to get gas price: ${result.error}")
}
null -> return null
null -> error("Gas price is null")
}
val fee = (gasLimit * gasPrice).movePointLeft(decimals)
val total = value + fee
val destinationAddress = requireNotNull(transaction.to) { "Destination address is null" }
val transactionData = TransactionData(
amount = Amount(value, wallet.blockchain),
// TODO refactoring
fee = Fee.Common(Amount(fee, wallet.blockchain)),
sourceAddress = transaction.from,
destinationAddress = transaction.to!!,
destinationAddress = destinationAddress,
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
gasLimit = gasLimit.toBigInteger(),
@ -101,6 +116,7 @@ class WalletConnectSdkHelper {
id = data.id,
type = data.type,
)
return WcTransactionData(
type = data.type,
transaction = transactionData,
@ -227,11 +243,11 @@ class WalletConnectSdkHelper {
}
fun prepareBnbTradeOrder(data: WcBinanceTradeOrder): BinanceMessageData.Trade {
return BnbHelper.createMessageData(data.toWCBinanceTradeOrder())
return BnbHelper.createMessageData(data)
}
fun prepareBnbTransferOrder(data: WcBinanceTransferOrder): BinanceMessageData.Transfer {
return BnbHelper.createMessageData(data.toWCBinanceTransferOrder())
return BnbHelper.createMessageData(data)
}
suspend fun signBnbTransaction(

View file

@ -1,39 +0,0 @@
package com.tangem.tap.domain.walletconnect.extensions
import com.tangem.tap.domain.walletconnect2.domain.WcEthereumTransaction
import com.tangem.tap.domain.walletconnect2.domain.WcSignMessage
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.ethereum.WCEthereumSignMessage
import com.trustwallet.walletconnect.models.ethereum.WCEthereumTransaction
internal fun WCPeerMeta.isDappSupported(): Boolean {
return !unsupportedDappsList.any { this.url.contains(it) }
}
private val unsupportedDappsList: List<String> = listOf("dydx.exchange")
internal fun WCEthereumTransaction.toWcEthTransaction(): WcEthereumTransaction {
return WcEthereumTransaction(
from = from,
to = to,
nonce = nonce,
gasPrice = gasPrice,
maxFeePerGas = maxFeePerGas,
maxPriorityFeePerGas = maxPriorityFeePerGas,
gas = gas,
gasLimit = gasLimit,
value = value,
data = data,
)
}
internal fun WCEthereumSignMessage.toWcEthereumSignMessage(): WcSignMessage {
return WcSignMessage(
raw = raw,
type = when (type) {
WCEthereumSignMessage.WCSignType.MESSAGE -> WcSignMessage.WCSignType.MESSAGE
WCEthereumSignMessage.WCSignType.PERSONAL_MESSAGE -> WcSignMessage.WCSignType.PERSONAL_MESSAGE
WCEthereumSignMessage.WCSignType.TYPED_MESSAGE -> WcSignMessage.WCSignType.TYPED_MESSAGE
},
)
}

View file

@ -1,13 +1,13 @@
package com.tangem.tap.domain.walletconnect2.app
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.tap.domain.walletconnect2.domain.WcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles
internal class TangemWcBlockchainHelper(
private val featureToggles: WalletConnectFeatureToggles,
featureToggles: WalletConnectFeatureToggles,
) : WcBlockchainHelper {
private val supportedNonEvmBlockchains = if (featureToggles.isSolanaTxSignEnabled) {

View file

@ -374,6 +374,7 @@ internal class DefaultWalletConnectRepository(
override fun rejectRequest(requestData: RequestData, error: WalletConnectError) {
val session = currentSessions.find { it.topic == requestData.topic }
analyticsHandler.send(
WalletConnect.RequestHandled(
WalletConnect.RequestHandledParams(
@ -385,17 +386,18 @@ internal class DefaultWalletConnectRepository(
),
),
)
cancelRequest(requestData.topic, requestData.requestId)
cancelRequest(requestData.topic, requestData.requestId, error.error)
}
override fun cancelRequest(topic: String, id: Long) {
override fun cancelRequest(topic: String, id: Long, message: String) {
Web3Wallet.respondSessionRequest(
params = Wallet.Params.SessionRequestResponse(
sessionTopic = topic,
jsonRpcResponse = Wallet.Model.JsonRpcResponse.JsonRpcError(
id = id,
code = 0,
message = "",
message = message,
),
),
onSuccess = {},
@ -406,8 +408,8 @@ internal class DefaultWalletConnectRepository(
override fun reject() {
Web3Wallet.rejectSession(
params = Wallet.Params.SessionReject(
sessionProposal?.proposerPublicKey ?: "",
"",
proposerPublicKey = sessionProposal?.proposerPublicKey ?: "",
reason = "",
),
onSuccess = {
Timber.d("Rejected successfully: $it")

View file

@ -6,6 +6,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl
@ -35,6 +38,9 @@ internal object WalletConnectInteractorModule {
wcRepository: WalletConnectRepository,
wcSessionsRepository: WalletConnectSessionsRepository,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
): WalletConnectInteractor {
return WalletConnectInteractor(
handler = WalletConnectEventsHandlerImpl(),
@ -42,7 +48,10 @@ internal object WalletConnectInteractorModule {
sessionsRepository = wcSessionsRepository,
sdkHelper = WalletConnectSdkHelper(),
blockchainHelper = TangemWcBlockchainHelper(walletConnectFeatureToggles),
dispatcher = AppCoroutineDispatcherProvider(),
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
dispatchers = AppCoroutineDispatcherProvider(),
)
}
}

View file

@ -1,26 +1,52 @@
package com.tangem.tap.domain.walletconnect2.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.tap.common.extensions.filterNotNull
import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.models.*
import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onEach
import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LargeClass", "LongParameterList")
class WalletConnectInteractor(
private val handler: WalletConnectEventsHandler,
private val walletConnectRepository: WalletConnectRepository,
private val sessionsRepository: WalletConnectSessionsRepository,
private val sdkHelper: WalletConnectSdkHelper,
private val dispatcher: CoroutineDispatcherProvider,
private val dispatchers: CoroutineDispatcherProvider,
private val walletManagersFacade: WalletManagersFacade,
private val currenciesRepository: CurrenciesRepository,
private val userWalletsListManager: UserWalletsListManager,
val blockchainHelper: WcBlockchainHelper,
) {
private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) {
GetSelectedWalletUseCase(userWalletsListManager)
}
private val wcScope = CoroutineScope(
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"),
)
private val listenerScope = CoroutineScope(
Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"),
)
private val events = walletConnectRepository.events
private val sessions = walletConnectRepository.activeSessions
@ -34,19 +60,56 @@ class WalletConnectInteractor(
sdkHelper = sdkHelper,
)
suspend fun startListening(userWalletId: String, cardId: String?) {
init {
getSelectedWalletUseCase().onRight { userWalletFlow ->
userWalletFlow
.conflate()
.distinctUntilChanged()
.onEach(::initWithWallet)
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
}
private suspend fun initWithWallet(userWallet: UserWallet) {
if (userWallet.isMultiCurrency) {
Timber.d("WalletConnect: initialize and setup networks for ${userWallet.walletId}")
startListeningWc(userWallet.walletId.stringValue, getCardId(userWallet))
subscribeOnCurrenciesUpdates(userWallet)
}
}
private fun subscribeOnCurrenciesUpdates(userWallet: UserWallet) {
currenciesRepository.getMultiCurrencyWalletCurrenciesUpdates(userWallet.walletId)
.conflate()
.distinctUntilChanged()
.onEach { currencies ->
setupUserChains(userWallet, currencies)
}
.flowOn(dispatchers.io)
.launchIn(wcScope)
}
private suspend fun setupUserChains(userWallet: UserWallet, currencies: List<CryptoCurrency>) {
val accounts = getAccountsForWc(
userWallet = userWallet,
networks = currencies.map { it.network },
)
setUserChains(accounts)
}
private suspend fun startListeningWc(userWalletId: String, cardId: String?) {
this.userWalletId = userWalletId
this.cardId = cardId
coroutineScope {
listenerScope.coroutineContext.cancelChildren()
listenerScope.launch {
launch { subscribeToEvents() }
launch { subscribeToSessions() }
walletConnectRepository.updateSessions()
}
}
fun setUserChains(accounts: List<Account>) {
private fun setUserChains(accounts: List<Account>) {
val userNamespaces: Map<NetworkNamespace, List<Account>> = accounts
.groupBy { account ->
blockchainHelper.getNamespaceFromFullChainIdOrNull(account.chainId)
@ -106,7 +169,7 @@ class WalletConnectInteractor(
}
}
}
.flowOn(dispatcher.io)
.flowOn(dispatchers.io)
.collect()
}
@ -117,7 +180,7 @@ class WalletConnectInteractor(
val filteredSessions = filterSessionsForUserWallet(listOfSessions, relevantTopics)
handler.onListOfSessionsUpdated(filteredSessions)
}
.flowOn(dispatcher.io)
.flowOn(dispatchers.io)
.collect()
}
@ -197,10 +260,18 @@ class WalletConnectInteractor(
)
else -> {
currentRequest = sessionRequest
val data = prepareRequestData(sessionRequest)
if (data != null) {
handler.onSessionRequest(data)
val data = prepareRequestData(sessionRequest).getOrElse { e ->
val wrappedError = e as? WalletConnectError ?: WalletConnectError.UnknownError(
message = e.localizedMessage ?: "Unknown error",
)
walletConnectRepository.rejectRequest(requestData, wrappedError)
handler.onSessionRejected(wrappedError)
return
}
handler.onSessionRequest(data)
}
}
}
@ -258,7 +329,41 @@ class WalletConnectInteractor(
return uri.lowercase().startsWith(WC_SCHEME)
}
private suspend fun prepareRequestData(sessionRequest: WalletConnectEvents.SessionRequest): WcPreparedRequest? {
private fun getCardId(userWallet: UserWallet): String? {
return if (userWallet.scanResponse.card.backupStatus?.isActive != true) {
userWallet.cardId
} else { // if wallet has backup, any card from wallet can be used to sign
null
}
}
private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List<Network>): List<Account> {
val walletManagers = networks.mapNotNull {
val blockchain = Blockchain.fromId(it.id.value)
walletManagersFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
blockchain = blockchain,
derivationPath = it.derivationPath.value,
)
}
return walletManagers.mapNotNull {
val wallet = it.wallet
val chainId = blockchainHelper.networkIdToChainIdOrNull(
wallet.blockchain.toNetworkId(),
)
chainId?.let {
Account(
chainId,
wallet.address,
wallet.publicKey.derivationPath?.rawPath,
)
}
}
}
private suspend fun prepareRequestData(
sessionRequest: WalletConnectEvents.SessionRequest,
): Result<WcPreparedRequest> {
return sessionRequestConverter.prepareRequest(sessionRequest, userWalletId)
}

View file

@ -27,5 +27,5 @@ interface WalletConnectRepository {
fun rejectRequest(requestData: RequestData, error: WalletConnectError)
fun cancelRequest(topic: String, id: Long)
fun cancelRequest(topic: String, id: Long, message: String = "")
}

View file

@ -4,6 +4,7 @@ import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper
import com.tangem.tap.domain.walletconnect2.domain.mapper.mapToTransaction
import com.tangem.tap.domain.walletconnect2.domain.models.BnbData
import com.tangem.tap.domain.walletconnect2.domain.models.EthTransactionData
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError
import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents
import com.tangem.tap.features.details.redux.walletconnect.WcEthTransactionType
@ -17,15 +18,18 @@ internal class WcSessionRequestConverter(
suspend fun prepareRequest(
sessionRequest: WalletConnectEvents.SessionRequest,
userWalletId: String,
): WcPreparedRequest? {
val networkId = blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId ?: "") ?: return null
): Result<WcPreparedRequest> = runCatching {
val networkId = requireNotNull(blockchainHelper.chainIdToNetworkIdOrNull(sessionRequest.chainId.orEmpty())) {
"Failed to get network ID for chain ID: ${sessionRequest.chainId}"
}
val derivationPath = getDerivationPath(
sessionsRepository = sessionsRepository,
sessionRequest = sessionRequest,
userWalletId = userWalletId,
walletAddress = getWalletAddress(sessionRequest.request),
)
return when (val request = sessionRequest.request) {
when (val request = sessionRequest.request) {
is WcRequest.EthSendTransaction -> {
val data = sdkHelper.prepareTransactionData(
EthTransactionData(
@ -38,7 +42,8 @@ internal class WcSessionRequestConverter(
metaName = sessionRequest.metaName,
metaUrl = sessionRequest.metaUrl,
),
) ?: return null
)
WcPreparedRequest.EthTransaction(
preparedRequestData = data,
topic = sessionRequest.topic,
@ -58,7 +63,8 @@ internal class WcSessionRequestConverter(
metaName = sessionRequest.metaName,
metaUrl = sessionRequest.metaUrl,
),
) ?: return null
)
WcPreparedRequest.EthTransaction(
preparedRequestData = data,
topic = sessionRequest.topic,
@ -122,7 +128,7 @@ internal class WcSessionRequestConverter(
derivationPath = derivationPath,
)
}
else -> null
else -> throw WalletConnectError.UnsupportedMethod
}
}

View file

@ -20,8 +20,12 @@ sealed class WalletConnectError(val error: String) : Exception() {
override val message: String?,
) : WalletConnectError("ExternalApprovalError")
object WrongUserWallet : WalletConnectError("WrongUserWallet")
object UnsupportedMethod : WalletConnectError("UnsupportedMethod")
object SigningError : WalletConnectError("SigningError")
object ValidationError : WalletConnectError("ValidationError")
data class UnknownError(
override val message: String,
) : WalletConnectError(message)
data object WrongUserWallet : WalletConnectError("WrongUserWallet")
data object UnsupportedMethod : WalletConnectError("UnsupportedMethod")
data object SigningError : WalletConnectError("SigningError")
data object ValidationError : WalletConnectError("ValidationError")
}

View file

@ -1,10 +1,10 @@
package com.tangem.tap.features.customtoken.impl.data
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.domain.common.extensions.supportedBlockchains
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository

View file

@ -1,9 +1,9 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.data.tokens.utils.CryptoCurrencyFactory
import com.tangem.domain.card.DerivePublicKeysUseCase
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.models.scan.ScanResponse

View file

@ -5,10 +5,10 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.hilt.navigation.compose.hiltViewModel
import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.screen.ComposeFragment
import com.tangem.core.ui.theme.AppThemeModeHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel
import dagger.hilt.android.AndroidEntryPoint
@ -23,7 +23,7 @@ import javax.inject.Inject
internal class AddCustomTokenFragment : ComposeFragment() {
@Inject
override lateinit var appThemeModeHolder: AppThemeModeHolder
override lateinit var uiDependencies: UiDependencies
@Composable
override fun ScreenContent(modifier: Modifier) {

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
@ -70,7 +71,7 @@ internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, mod
@Preview
@Composable
private fun Preview_AddCustomTokenContent() {
TangemTheme {
TangemThemePreview {
AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent())
}
}

View file

@ -1,11 +1,12 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
/**
@ -24,21 +25,12 @@ internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifi
}
@Preview(showSystemUi = true)
@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_AddCustomTokenScreen_Light(
private fun Preview_AddCustomTokenScreen(
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
) {
TangemTheme(isDark = false) {
AddCustomTokenScreen(stateHolder)
}
}
@Preview(showSystemUi = true)
@Composable
private fun Preview_AddCustomTokenScreen_Dark(
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
) {
TangemTheme(isDark = true) {
TangemThemePreview {
AddCustomTokenScreen(stateHolder)
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
@ -242,7 +243,7 @@ private fun TestBlock(
@Preview
@Composable
private fun Preview_AddCustomTokenTestContent() {
TangemTheme {
TangemThemePreview {
AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent())
}
}

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