Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-04 12:47:51 +03:00
commit ff24b00af4
404 changed files with 12634 additions and 3818 deletions

View file

@ -14,6 +14,8 @@ import com.kaspersky.kaspresso.kaspresso.Kaspresso
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
import com.tangem.common.allure.FailedStepScreenshotInterceptor
import com.tangem.common.rules.ApiEnvironmentRule
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
import com.tangem.datasource.api.common.config.managers.ApiConfigsManager
import com.tangem.datasource.local.preferences.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys
@ -46,6 +48,9 @@ abstract class BaseTestCase : TestCase(
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
@Inject
lateinit var featureTogglesManager: FeatureTogglesManager
private val hiltRule = HiltAndroidRule(this)
private val apiEnvironmentRule = ApiEnvironmentRule()
private val permissionRule = GrantPermissionRule.grant(
@ -90,6 +95,7 @@ abstract class BaseTestCase : TestCase(
apiEnvironmentRule.setup(apiConfigsManager)
ActivityScenario.launch(MainActivity::class.java)
Intents.init()
setFeatureToggles()
additionalBeforeSection()
}.after {
additionalAfterSection()
@ -113,4 +119,18 @@ abstract class BaseTestCase : TestCase(
{
composeTestRule.onRoot(useUnmergedTree = useUnmergedTree).printToLog(tag, maxDepth)
}
private fun setFeatureToggles() {
runBlocking {
with(featureTogglesManager as MutableFeatureTogglesManager) {
changeToggle("WALLET_CONNECT_REDESIGN_ENABLED", true)
changeToggle("WALLET_BALANCE_FETCHER_ENABLED", true)
changeToggle("SEND_VIA_SWAP_ENABLED", true)
changeToggle("SWAP_REDESIGN_ENABLED", true)
changeToggle("SEND_REDESIGN_ENABLED", true)
changeToggle("NEW_ONRAMP_MAIN_ENABLED", true)
}
}
}
}

View file

@ -0,0 +1,105 @@
package com.tangem.common.utils
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONObject
import timber.log.Timber
import java.util.concurrent.TimeUnit
/**
*
*/
fun getWcUri(
network: String = "ethereum",
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Getting WC URI for network: $network")
val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения
.readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа
.writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи
.callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса
.build()
val request = Request.Builder()
.url("$baseUrl/wc_uri?network=$network")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
val jsonObject = JSONObject(body)
if (jsonObject.getBoolean("success")) {
val wcUri = jsonObject.getString("wcUri")
Timber.i("Got WC URI successfully: $wcUri")
wcUri
} else {
Timber.e("API returned error: ${jsonObject.optString("error", "Unknown")}")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error getting WC URI")
null
}
}
fun checkServiceHealth(
baseUrl: String = "[REDACTED_ENV_URL]"
): String? {
Timber.i("Checking service health")
val client = OkHttpClient()
val request = Request.Builder()
.url("$baseUrl/health")
.get()
.build()
return try {
client.newCall(request).execute().use { response ->
Timber.i("Response code: ${response.code}")
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
Timber.i("Response body: $body")
if (body.isEmpty()) {
Timber.e("Response body is empty")
return null
}
val jsonObject = JSONObject(body)
val status = jsonObject.optString("status", "")
if (status.isNotEmpty()) {
Timber.i("Got status successfully: $status")
status
} else {
Timber.e("Status field is missing or empty")
null
}
} else {
val errorBody = response.body?.string() ?: "No error body"
Timber.e("Request failed: ${response.code}, body: $errorBody")
null
}
}
} catch (e: Exception) {
Timber.e(e, "Error checking health")
null
}
}

View file

@ -35,6 +35,10 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
}
)
val screenContainer: KNode = child {
hasTestTag(MainScreenTestTags.SCREEN_CONTAINER)
}
val synchronizeAddressesButton: KNode = child {
hasText(getResourceString(R.string.common_generate_addresses))
}

View file

@ -0,0 +1,108 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect))
hasTestTag(WalletConnectBottomSheetTestTags.TITLE)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val connectionRequestIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_ICON)
useUnmergedTree = true
}
val connectionRequestText: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_TEXT)
useUnmergedTree = true
}
val connectionRequestChevron: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.CONNECTION_REQUEST_CHEVRON)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletNameTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val networksIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICON)
useUnmergedTree = true
}
val networksTitle: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val networksIcons: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_ICONS)
useUnmergedTree = true
}
val networksSelectorIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.NETWORKS_SELECTOR_ICON)
useUnmergedTree = true
}
val cancelButton: KNode = child {
hasText(getResourceString(R.string.common_cancel))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
val connectButton: KNode = child {
hasText(getResourceString(R.string.wc_common_connect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectBottomSheet(function: WalletConnectBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,107 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectBottomSheetTestTags
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
import com.tangem.features.walletconnect.impl.R as WalletConnectImplR
class WalletConnectDetailsBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectDetailsBottomSheetPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.TITLE)
hasText(getResourceString(WalletConnectImplR.string.wc_connected_app_title))
useUnmergedTree = true
}
val date: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.DATE)
useUnmergedTree = true
}
val closeButton: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON)
useUnmergedTree = true
}
val networksBlockTitle: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.wc_connected_networks))
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_ICON)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_NAME)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectBottomSheetTestTags.APP_URL)
useUnmergedTree = true
}
val walletIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON)
useUnmergedTree = true
}
val walletTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME)
useUnmergedTree = true
}
val connectedNetworksTitle: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORKS_TITLE)
useUnmergedTree = true
}
val connectedNetworkItem: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ITEM)
useUnmergedTree = true
}
val connectedNetworkIcon: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_ICON)
useUnmergedTree = true
}
val connectedNetworkName: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_NAME)
useUnmergedTree = true
}
val connectedNetworkSymbol: KNode = child {
hasTestTag(WalletConnectDetailsBottomSheetTestTags.NETWORK_SYMBOL)
useUnmergedTree = true
}
val disconnectButton: KNode = child {
hasText(getResourceString(WalletConnectImplR.string.common_disconnect))
hasTestTag(BaseButtonTestTags.TEXT)
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectDetailsBottomSheet(function: WalletConnectDetailsBottomSheetPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,59 @@
package com.tangem.screens
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import com.tangem.common.BaseTestCase
import com.tangem.core.ui.R
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.test.WalletConnectScreenTestTags
import io.github.kakaocup.compose.node.element.ComposeScreen
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
import io.github.kakaocup.compose.node.element.KNode
import io.github.kakaocup.kakao.common.utilities.getResourceString
class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
ComposeScreen<WalletConnectPageObject>(semanticsProvider = semanticsProvider) {
val title: KNode = child {
hasText(getResourceString(R.string.wc_connections))
useUnmergedTree = true
}
val moreButton: KNode = child {
hasTestTag(WalletConnectScreenTestTags.MORE_BUTTON)
useUnmergedTree = true
}
val walletName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.WALLET_NAME)
useUnmergedTree = true
}
val appIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_ICON)
useUnmergedTree = true
}
val appName: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_NAME)
useUnmergedTree = true
}
val approveIcon: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APPROVE_ICON)
useUnmergedTree = true
}
val appUrl: KNode = child {
hasTestTag(WalletConnectScreenTestTags.APP_URL)
useUnmergedTree = true
}
val newConnectionButton: KNode = child {
hasTestTag(BaseButtonTestTags.TEXT)
hasText(getResourceString(R.string.wc_new_connection))
useUnmergedTree = true
}
}
internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) =
onComposeScreen(composeTestRule, function)

View file

@ -0,0 +1,38 @@
package com.tangem.steps
import com.tangem.common.BaseTestCase
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.domain.models.scan.ProductType
import com.tangem.screens.onDisclaimerScreen
import com.tangem.screens.onMainScreen
import com.tangem.screens.onMarketsTooltipScreen
import com.tangem.screens.onStoriesScreen
import com.tangem.tap.domain.sdk.mocks.MockProvider
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.openMainScreen(productType: ProductType? = null) {
if (productType != null) {
MockProvider.setMocks(productType)
}
step("Click on 'Accept' button") {
onDisclaimerScreen { acceptButton.clickWithAssertion() }
}
step("Click on 'Accept' button") {
onStoriesScreen { scanButton.clickWithAssertion() }
}
step("Assert main screen is displayed") {
onMainScreen { screenContainer.assertIsDisplayed() }
}
step("Assert main screen is displayed") {
onMarketsTooltipScreen { contentContainer.clickWithAssertion() }
}
}
fun BaseTestCase.synchronizeAddresses(balance: String) {
step("Click on 'Synchronize addresses' button") {
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
}
step("Assert wallet balance = '$balance'") {
onMainScreen { walletBalance().assertTextContains(balance) }
}
}

View file

@ -0,0 +1,15 @@
package com.tangem.steps
import android.content.Intent
import android.net.Uri
import androidx.test.core.app.ApplicationProvider
fun openAppByDeepLink(deepLinkUri: String?) {
val deeplinkScheme = "tangem://wc?uri="
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(deeplinkScheme + deepLinkUri)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(intent)
}

View file

@ -0,0 +1,139 @@
package com.tangem.steps
import com.tangem.common.BaseTestCase
import com.tangem.screens.onWalletConnectBottomSheet
import com.tangem.screens.onWalletConnectDetailsBottomSheet
import com.tangem.screens.onWalletConnectScreen
import io.qameta.allure.kotlin.Allure.step
fun BaseTestCase.checkWalletConnectBottomSheet() {
step("Assert 'Wallet Connect' bottom sheet title is displayed") {
onWalletConnectBottomSheet { title.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app icon is displayed") {
onWalletConnectBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app name is displayed") {
onWalletConnectBottomSheet { appName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet approve icon is displayed") {
onWalletConnectBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet app URL is displayed") {
onWalletConnectBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request icon is displayed") {
onWalletConnectBottomSheet { connectionRequestIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request text is displayed") {
onWalletConnectBottomSheet { connectionRequestText.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet connection request chevron is displayed") {
onWalletConnectBottomSheet { connectionRequestChevron.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet icon is displayed") {
onWalletConnectBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet title is displayed") {
onWalletConnectBottomSheet { walletNameTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet wallet name is displayed") {
onWalletConnectBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks icon is displayed") {
onWalletConnectBottomSheet { networksIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet networks title is displayed") {
onWalletConnectBottomSheet { networksTitle.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet right networks icons is displayed") {
onWalletConnectBottomSheet { networksIcons.assertIsDisplayed() }
}
step("'Wallet Connect' bottom sheet networks selector icon is displayed") {
onWalletConnectBottomSheet { networksSelectorIcon.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Cancel' button is displayed") {
onWalletConnectBottomSheet { cancelButton.assertIsDisplayed() }
}
step("Assert 'Wallet Connect' bottom sheet 'Connect' button is displayed") {
onWalletConnectBottomSheet { connectButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectScreen() {
step("Assert 'Wallet Connect' title is displayed") {
onWalletConnectScreen { title.assertIsDisplayed() }
}
step("Assert 'More' button is displayed") {
onWalletConnectScreen { moreButton.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectScreen { walletName.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectScreen { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectScreen { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectScreen { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectScreen { appUrl.assertIsDisplayed() }
}
step("Assert 'New Connection' button is displayed") {
onWalletConnectScreen { newConnectionButton.assertIsDisplayed() }
}
}
fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) {
step("Assert connection details title is displayed") {
onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() }
}
step("Assert date is displayed") {
onWalletConnectDetailsBottomSheet { date.assertIsDisplayed() }
}
step("Assert 'Close' button is displayed") {
onWalletConnectDetailsBottomSheet { closeButton.assertIsDisplayed() }
}
step("Assert app icon is displayed") {
onWalletConnectDetailsBottomSheet { appIcon.assertIsDisplayed() }
}
step("Assert app name is displayed") {
onWalletConnectDetailsBottomSheet { appName.assertIsDisplayed() }
}
step("Assert approve icon is displayed") {
onWalletConnectDetailsBottomSheet { approveIcon.assertIsDisplayed() }
}
step("Assert app URL is displayed") {
onWalletConnectDetailsBottomSheet { appUrl.assertIsDisplayed() }
}
step("Assert wallet icon is displayed") {
onWalletConnectDetailsBottomSheet { walletIcon.assertIsDisplayed() }
}
step("Assert wallet title is displayed") {
onWalletConnectDetailsBottomSheet { walletTitle.assertIsDisplayed() }
}
step("Assert wallet name is displayed") {
onWalletConnectDetailsBottomSheet { walletName.assertIsDisplayed() }
}
step("Assert 'Connected networks' title is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworksTitle.assertIsDisplayed() }
}
step("Assert connected network item is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkItem.assertIsDisplayed() }
}
step("Assert connected network icon is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() }
}
step("Assert connected dApp name: '$dAppName'") {
onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) }
}
step("Assert connected network symbol is displayed") {
onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() }
}
step("Assert 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.assertIsDisplayed() }
}
}

View file

@ -0,0 +1,181 @@
package com.tangem.tests
import com.tangem.common.BaseTestCase
import com.tangem.common.constants.TestConstants.TOTAL_BALANCE
import com.tangem.common.extensions.clickWithAssertion
import com.tangem.common.extensions.swipeUp
import com.tangem.common.utils.getWcUri
import com.tangem.scenarios.OpenMainScreenScenario
import com.tangem.screens.*
import com.tangem.steps.*
import dagger.hilt.android.testing.HiltAndroidTest
import io.qameta.allure.kotlin.AllureId
import io.qameta.allure.kotlin.junit4.DisplayName
import org.junit.Ignore
import org.junit.Test
@HiltAndroidTest
class WalletConnectTest : BaseTestCase() {
@AllureId("3833")
@DisplayName("WC (React App): open session from deeplink on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("3834")
@DisplayName("WC (React App): open session from deeplink not on main screen")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSessionNotOnMainScreen() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Click on 'Buy' button") {
onMainScreen { buyButton.clickWithAssertion() }
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Assert 'Wallet Connect' bottom sheet is displayed") {
onWalletConnectBottomSheet { connectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
@AllureId("886")
@DisplayName("WC (React App): open session from deeplink ")
@Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work")
@Test
fun openWalletConnectSession() {
val balance = TOTAL_BALANCE
val dAppName = "React App"
val deepLinkUri = getWcUri()
setupHooks().run {
step("Open 'Main Screen'") {
openMainScreen()
}
step("Synchronize addresses") {
synchronizeAddresses(balance)
}
step("Open recent apps") {
device.uiDevice.pressRecentApps()
}
step("Stop app by swipe") {
swipeUp(startHeightRatio = 0.8f)
}
step("Create WC session buy deeplink") {
openAppByDeepLink(deepLinkUri)
}
step("Open 'Main Screen'") {
scenario(OpenMainScreenScenario(composeTestRule))
}
step("Check 'Wallet Connect' bottom sheet") {
checkWalletConnectBottomSheet()
}
step("Click on 'Connect' button") {
onWalletConnectBottomSheet { connectButton.performClick() }
}
step("Click 'More' button on TopBar") {
onTopBar { moreButton.clickWithAssertion() }
}
step("Click on 'Wallet Connect' button") {
onDetailsScreen { walletConnectButton.clickWithAssertion() }
}
step("Check 'Wallet Connect' screen") {
checkWalletConnectScreen()
}
step("Click on app icon") {
onWalletConnectScreen { appIcon.performClick() }
}
step("Check 'Wallet Connect' details bottom sheet") {
checkWalletConnectDetailsBottomSheet(dAppName)
}
step("Click on 'Disconnect button' is displayed") {
onWalletConnectDetailsBottomSheet { disconnectButton.performClick() }
}
step("Assert connection is not displayed") {
onWalletConnectScreen { appName.assertIsNotDisplayed() }
}
}
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.data
package com.tangem.tap
import com.google.firebase.messaging.FirebaseMessaging
import com.tangem.utils.notifications.PushNotificationsTokenProvider

View file

@ -1,6 +1,6 @@
package com.tangem.tap.di.data
package com.tangem.tap.di
import com.tangem.tap.data.FirebasePushNotificationsTokenProvider
import com.tangem.tap.FirebasePushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
@ -10,7 +10,7 @@ import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface PushNotificationsModule {
internal interface GooglePushModule {
@Binds
@Singleton

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name="com.tangem.tap.HuaweiPushService"
android:exported="false">
<intent-filter>
<action android:name="com.huawei.push.action.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="push_kit_auto_init_enabled"
android:value="true" />
</application>
</manifest>

View file

@ -0,0 +1,50 @@
package com.tangem.tap
import android.content.Context
import com.google.firebase.messaging.FirebaseMessaging
import com.huawei.agconnect.AGConnectOptionsBuilder
import com.huawei.hms.aaid.HmsInstanceId
import com.huawei.hms.common.ApiException
import com.tangem.google.GoogleServicesHelper
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.tasks.await
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
internal class HuaweiPushNotificationsTokenProvider @Inject constructor(
@ApplicationContext private val context: Context,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider,
) : PushNotificationsTokenProvider {
override suspend fun getToken(): String {
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(context)
return if (isGoogleServicesAvailable) {
try {
FirebaseMessaging.getInstance().token.await()
} catch (ex: Exception) {
Timber.e(ex)
""
}
} else {
withContext(coroutineDispatcherProvider.io) {
try {
val appId = AGConnectOptionsBuilder().build(context).getString(APP_ID_KEY)
val token = HmsInstanceId.getInstance(context).getToken(appId, TOKEN_REQUEST_MODE)
Timber.i("Requested token from HuaweiService: $token")
token
} catch (e: ApiException) {
Timber.i("Fetching token from HuaweiService failed cause: ${e.message}")
""
}
}
}
}
companion object {
private const val APP_ID_KEY = "client/app_id"
private const val TOKEN_REQUEST_MODE = "HCM"
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.tap
import android.os.Bundle
import com.huawei.hms.push.HmsMessageService
import com.huawei.hms.push.RemoteMessage
import com.tangem.google.GoogleServicesHelper
import com.tangem.tap.common.pushes.PushNotificationDelegate
import timber.log.Timber
class HuaweiPushService : HmsMessageService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String?, bundle: Bundle?) {
super.onNewToken(token, bundle)
Timber.i("HuaweiPushService: On new token from HuaweiService: $token")
}
override fun onTokenError(e: Exception?, bundle: Bundle?) {
super.onTokenError(e, bundle)
Timber.i("HuaweiPushService: Fetching token from HuaweiService failed cause: ${e?.message}")
}
override fun onMessageReceived(message: RemoteMessage?) {
super.onMessageReceived(message)
val isGoogleServicesAvailable = GoogleServicesHelper.checkGoogleServicesAvailability(this)
if (isGoogleServicesAvailable) return
val notification = message?.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
pushNotificationDelegate.showNotification(
dataMap = message.dataOfMap,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.urgency,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateConfig,
)
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.HuaweiPushNotificationsTokenProvider
import com.tangem.utils.notifications.PushNotificationsTokenProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface HuaweiPushModule {
@Binds
@Singleton
fun bindPushNotificationsTokenProvider(impl: HuaweiPushNotificationsTokenProvider): PushNotificationsTokenProvider
}

View file

@ -1,160 +1,165 @@
{
"coins" : [
"coins": [
{
"id" : "matic-token",
"symbol" : "MATIC",
"name" : "Polygon",
"networks" : [
"id": "matic-token",
"symbol": "MATIC",
"name": "Polygon",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x0000000000000000000000000000000000001010",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x0000000000000000000000000000000000001010",
"decimalCount": 18
}
]
},
{
"id" : "kaspa",
"symbol" : "KAS",
"name" : "Kaspa",
"networks" : [
"id": "kaspa",
"symbol": "KAS",
"name": "Kaspa",
"networks": [
{
"networkId" : "kaspa/test"
"networkId": "kaspa/test"
}
]
},
{
"id" : "Dai Stablecoin-DAI",
"symbol" : "DAI",
"name" : "Dai Stablecoin",
"networks" : [
"id": "Dai Stablecoin-DAI",
"symbol": "DAI",
"name": "Dai Stablecoin",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xcB1e72786A6eb3b44C2a2429e317c8a2462CFeb1",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xec5dcb5dbf4b114c9d0f65bccab49ec54f6a0867",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x8a9424745056eb399fd19a0ec26a14316684e274",
"decimalCount": 18
}
]
},
{
"id" : "Dummy ERC20-DERC20",
"symbol" : "DERC20",
"name" : "Dummy ERC20",
"networks" : [
"id": "Dummy ERC20-DERC20",
"symbol": "DERC20",
"name": "Dummy ERC20",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xfe4F5145f6e09952a5ba9e956ED0C25e3Fa4c7F1",
"decimalCount": 18
}
]
},
{
"id" : "Ether-ETH",
"symbol" : "ETH",
"name" : "Ether",
"networks" : [
"id": "Ether-ETH",
"symbol": "ETH",
"name": "Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x714550C2C1Ea08688607D86ed8EeF4f5E4F22323",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xd66c6b4f0be8ce5b39d52e0fd1344c389929b378",
"decimalCount": 18
}
]
},
{
"id" : "Test Token-TST",
"symbol" : "TST",
"name" : "Test Token",
"networks" : [
"id": "Test Token-TST",
"symbol": "TST",
"name": "Test Token",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e",
"decimalCount": 18
}
]
},
{
"id" : "tether",
"symbol" : "USDT",
"name" : "Tether USD",
"networks" : [
"id": "tether",
"symbol": "USDT",
"name": "Tether USD",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount" : 6
"networkId": "ethereum/test",
"contractAddress": "0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0x3813e82e6f7098b9583FC0F33a962D02018B6803",
"decimalCount": 6
},
{
"networkId" : "tron/test",
"contractAddress" : "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount" : 6
"networkId": "binance-smart-chain/test",
"contractAddress": "0x337610d27c682e347c9cd60bd4b3b107c9d34ddd",
"decimalCount": 18
},
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj",
"decimalCount": 6
},
{
"networkId": "binance-smart-chain/test",
"contractAddress": "0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684",
"decimalCount": 18
}
]
},
{
"id" : "just-gov",
"symbol" : "JST",
"name" : "JUST GOV",
"networks" : [
"id": "just-gov",
"symbol": "JST",
"name": "JUST GOV",
"networks": [
{
"networkId" : "tron/test",
"contractAddress" : "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount" : 18
"networkId": "tron/test",
"contractAddress": "TF17BgPaZYbz8oxbjhriubPDsA7ArKoLX3",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Ether-WETH",
"symbol" : "WETH",
"name" : "Wrapped Ether",
"networks" : [
"id": "Wrapped Ether-WETH",
"symbol": "WETH",
"name": "Wrapped Ether",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xA6FA4fB5f76172d178d61B04b0ecd319C5d1C0aa",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Matic-WMATIC",
"symbol" : "WMATIC",
"name" : "Wrapped Matic",
"networks" : [
"id": "Wrapped Matic-WMATIC",
"symbol": "WMATIC",
"name": "Wrapped Matic",
"networks": [
{
"networkId" : "polygon-pos/test",
"contractAddress" : "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount" : 18
"networkId": "polygon-pos/test",
"contractAddress": "0xd0A1E359811322d97991E03f863a0C30C2cF029C",
"decimalCount": 18
}
]
},
{
"id" : "solana",
"symbol" : "SOL",
"name" : "Solana",
"networks" : [
"id": "solana",
"symbol": "SOL",
"name": "Solana",
"networks": [
{
"networkId" : "solana/test"
"networkId": "solana/test"
}
]
},
@ -162,263 +167,262 @@
"id": "the-open-network",
"symbol": "TON",
"name": "Toncoin",
"networks":
[
"networks": [
{
"networkId": "the-open-network/test"
}
]
},
{
"id" : "Tangem Coin A-TCA",
"symbol" : "TCA",
"name" : "Tangem Coin A",
"networks" : [
"id": "Tangem Coin A-TCA",
"symbol": "TCA",
"name": "Tangem Coin A",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "22PTNbX31Zuztd6nD82fC8nQdT2hUfWv9XWXKuDkrFqR",
"decimalCount": 9
}
]
},
{
"id" : "Tangem Coin B-TCB",
"symbol" : "TCB",
"name" : "Tangem Coin B",
"networks" : [
"id": "Tangem Coin B-TCB",
"symbol": "TCB",
"name": "Tangem Coin B",
"networks": [
{
"networkId" : "solana/test",
"contractAddress" : "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount" : 9
"networkId": "solana/test",
"contractAddress": "HmSghNPg6KCk711YJA92aPejt8auyFkvmaED6jbHfUs4",
"decimalCount": 9
}
]
},
{
"id" : "binancecoin",
"symbol" : "BNB",
"name" : "Binance",
"networks" : [
"id": "binancecoin",
"symbol": "BNB",
"name": "Binance",
"networks": [
{
"networkId" : "binancecoin/test"
"networkId": "binancecoin/test"
},
{
"networkId" : "binance-smart-chain/test"
"networkId": "binance-smart-chain/test"
}
]
},
{
"id" : "Hemster - 452-HEM",
"symbol" : "HEM",
"name" : "Hemster - 452",
"networks" : [
"id": "Hemster - 452-HEM",
"symbol": "HEM",
"name": "Hemster - 452",
"networks": [
{
"networkId" : "binancecoin/test",
"contractAddress" : "HEM-452",
"decimalCount" : 8
"networkId": "binancecoin/test",
"contractAddress": "HEM-452",
"decimalCount": 8
}
]
},
{
"id" : "Binance-Peg BTCB Token-BTCB",
"symbol" : "BTCB",
"name" : "Binance-Peg BTCB Token",
"networks" : [
"id": "Binance-Peg BTCB Token-BTCB",
"symbol": "BTCB",
"name": "Binance-Peg BTCB Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x6ce8da28e2f864420840cf74474eff5fd80e65b8",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg BUSD Token-BUSD",
"symbol" : "BUSD",
"name" : "Binance-Peg BUSD Token",
"networks" : [
"id": "Binance-Peg BUSD Token-BUSD",
"symbol": "BUSD",
"name": "Binance-Peg BUSD Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xed24fc36d5ee211ea25a80239fb8c4cfd80f12ee",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg USDC Token-USDC",
"symbol" : "USDC",
"name" : "Binance-Peg USDC Token",
"networks" : [
"id": "Binance-Peg USDC Token-USDC",
"symbol": "USDC",
"name": "Binance-Peg USDC Token",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0x64544969ed7ebf5f083679233325356ebe738930",
"decimalCount": 18
}
]
},
{
"id" : "Binance-Peg XRP-XRP",
"symbol" : "XRP",
"name" : "Binance-Peg XRP",
"networks" : [
"id": "Binance-Peg XRP-XRP",
"symbol": "XRP",
"name": "Binance-Peg XRP",
"networks": [
{
"networkId" : "binance-smart-chain/test",
"contractAddress" : "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount" : 18
"networkId": "binance-smart-chain/test",
"contractAddress": "0xa83575490d7df4e2f47b7d38ef351a2722ca45b9",
"decimalCount": 18
}
]
},
{
"id" : "ethereum",
"symbol" : "ETH",
"name" : "Ethereum",
"networks" : [
"id": "ethereum",
"symbol": "ETH",
"name": "Ethereum",
"networks": [
{
"networkId" : "ethereum/test"
"networkId": "ethereum/test"
}
]
},
{
"id" : "ethereum-classic",
"symbol" : "ETC",
"name" : "Ethereum Classic",
"networks" : [
"id": "ethereum-classic",
"symbol": "ETC",
"name": "Ethereum Classic",
"networks": [
{
"networkId" : "ethereum-classic/test"
"networkId": "ethereum-classic/test"
}
]
},
{
"id" : "Weenus-WEENUS",
"symbol" : "WEENUS",
"name" : "Weenus",
"networks" : [
"id": "Weenus-WEENUS",
"symbol": "WEENUS",
"name": "Weenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0xaFF4481D10270F50f203E0763e2597776068CBc5",
"decimalCount": 18
}
]
},
{
"id" : "Xeenus-XEENUS",
"symbol" : "XEENUS",
"name" : "Xeenus",
"networks" : [
"id": "Xeenus-XEENUS",
"symbol": "XEENUS",
"name": "Xeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount" : 18
"networkId": "ethereum/test",
"contractAddress": "0x022E292b44B5a146F2e8ee36Ff44D3dd863C915c",
"decimalCount": 18
}
]
},
{
"id" : "Yeenus-YEENUS",
"symbol" : "YEENUS",
"name" : "Yeenus",
"networks" : [
"id": "Yeenus-YEENUS",
"symbol": "YEENUS",
"name": "Yeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount" : 8
"networkId": "ethereum/test",
"contractAddress": "0xc6fDe3FD2Cc2b173aEC24cc3f267cb3Cd78a26B7",
"decimalCount": 8
}
]
},
{
"id" : "Zeenus-ZEENUS",
"symbol" : "ZEENUS",
"name" : "Zeenus",
"networks" : [
"id": "Zeenus-ZEENUS",
"symbol": "ZEENUS",
"name": "Zeenus",
"networks": [
{
"networkId" : "ethereum/test",
"contractAddress" : "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount" : 0
"networkId": "ethereum/test",
"contractAddress": "0x1f9061B953bBa0E36BF50F21876132DcF276fC6e",
"decimalCount": 0
}
]
},
{
"id" : "avalanche-2",
"symbol" : "AVAX",
"name" : "Avalanche",
"networks" : [
"id": "avalanche-2",
"symbol": "AVAX",
"name": "Avalanche",
"networks": [
{
"networkId" : "avalanche/test"
"networkId": "avalanche/test"
}
]
},
{
"id" : "The Fuji stablecoin-FUJISTABLE",
"symbol" : "FUJISTABLE",
"name" : "The Fuji stablecoin",
"networks" : [
"id": "The Fuji stablecoin-FUJISTABLE",
"symbol": "FUJISTABLE",
"name": "The Fuji stablecoin",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount" : 6
"networkId": "avalanche/test",
"contractAddress": "0x2058ec2791dD28b6f67DB836ddf87534F4Bbdf22",
"decimalCount": 6
}
]
},
{
"id" : "To the Moon-FUJIMOON",
"symbol" : "FUJIMOON",
"name" : "To the Moon",
"networks" : [
"id": "To the Moon-FUJIMOON",
"symbol": "FUJIMOON",
"name": "To the Moon",
"networks": [
{
"networkId" : "avalanche/test",
"contractAddress" : "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount" : 9
"networkId": "avalanche/test",
"contractAddress": "0x97132C109c6816525F7f338DCb7435E1412A7668",
"decimalCount": 9
}
]
},
{
"id" : "fantom",
"symbol" : "FTM",
"name" : "Fantom",
"networks" : [
"id": "fantom",
"symbol": "FTM",
"name": "Fantom",
"networks": [
{
"networkId" : "fantom/test"
"networkId": "fantom/test"
}
]
},
{
"id" : "Fantom USD-FUSD",
"symbol" : "FUSD",
"name" : "Fantom USD",
"networks" : [
"id": "Fantom USD-FUSD",
"symbol": "FUSD",
"name": "Fantom USD",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074",
"decimalCount": 18
}
]
},
{
"id" : "Wrapped Fantom-WFTM",
"symbol" : "WFTM",
"name" : "Wrapped Fantom",
"networks" : [
"id": "Wrapped Fantom-WFTM",
"symbol": "WFTM",
"name": "Wrapped Fantom",
"networks": [
{
"networkId" : "fantom/test",
"contractAddress" : "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount" : 18
"networkId": "fantom/test",
"contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621",
"decimalCount": 18
}
]
},
{
"id" : "bitcoin",
"symbol" : "BTC",
"name" : "Bitcoin",
"networks" : [
"id": "bitcoin",
"symbol": "BTC",
"name": "Bitcoin",
"networks": [
{
"networkId" : "bitcoin/test"
"networkId": "bitcoin/test"
}
]
},
{
"id" : "vechain",
"symbol" : "VET",
"name" : "VeChain",
"networks" : [
"id": "vechain",
"symbol": "VET",
"name": "VeChain",
"networks": [
{
"networkId" : "vechain/test"
"networkId": "vechain/test"
}
]
},
@ -435,34 +439,34 @@
]
},
{
"id" : "tron",
"symbol" : "TRX",
"name" : "Tron",
"networks" : [
{
"networkId" : "tron/test"
}
]
"id": "tron",
"symbol": "TRX",
"name": "Tron",
"networks": [
{
"networkId": "tron/test"
}
]
},
{
"id" : "algorand",
"symbol" : "ALGO",
"name" : "Algorand",
"networks" : [
{
"networkId" : "algorand/test"
}
]
"id": "algorand",
"symbol": "ALGO",
"name": "Algorand",
"networks": [
{
"networkId": "algorand/test"
}
]
},
{
"id" : "arbitrum-one",
"symbol" : "ETH",
"name" : "Arbitrum",
"networks" : [
{
"networkId" : "arbitrum-one/test"
}
]
"id": "arbitrum-one",
"symbol": "ETH",
"name": "Arbitrum",
"networks": [
{
"networkId": "arbitrum-one/test"
}
]
},
{
"id": "stellar",
@ -485,12 +489,12 @@
]
},
{
"id" : "polkadot",
"symbol" : "DOT",
"name" : "Polkadot",
"networks" : [
"id": "polkadot",
"symbol": "DOT",
"name": "Polkadot",
"networks": [
{
"networkId" : "polkadot/test"
"networkId": "polkadot/test"
}
]
},
@ -509,8 +513,7 @@
"id": "kava",
"symbol": "KAVA",
"name": "Kava EVM",
"networks":
[
"networks": [
{
"networkId": "kava/test"
}
@ -520,8 +523,7 @@
"id": "telos",
"symbol": "TLOS",
"name": "Telos EVM",
"networks":
[
"networks": [
{
"networkId": "telos/test"
}
@ -531,8 +533,7 @@
"id": "ravencoin",
"symbol": "RVN",
"name": "Ravencoin",
"networks":
[
"networks": [
{
"networkId": "ravencoin/test"
}
@ -542,8 +543,7 @@
"id": "cosmos",
"symbol": "ATOM",
"name": "Cosmos Hub",
"networks":
[
"networks": [
{
"networkId": "cosmos/test"
}
@ -798,6 +798,30 @@
"networkId": "alephium/test"
}
]
},
{
"id": "chainlink",
"name": "Chainlink",
"symbol": "LINK",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xf8Fb3713D459D7C1018BD0A49D19b4C44290EBE5",
"decimalCount": 18
}
]
},
{
"id": "gho",
"name": "GHO",
"symbol": "GHO",
"networks": [
{
"networkId": "ethereum/test",
"contractAddress": "0xc4bF5CbDaBE595361438F8c6a187bDc330539c60",
"decimalCount": 18
}
]
}
]
}

View file

@ -29,7 +29,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -107,7 +107,7 @@ interface ApplicationEntryPoint {
fun getSendFeedbackEmailUseCase(): SendFeedbackEmailUseCase
fun getGetCardInfoUseCase(): GetCardInfoUseCase
fun getWalletMetaInfoUseCase(): GetWalletMetaInfoUseCase
fun getUrlOpener(): UrlOpener

View file

@ -47,7 +47,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
import com.tangem.domain.card.ScanCardProcessor
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.LogConfig
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -77,6 +77,7 @@ import com.tangem.wallet.BuildConfig
import dagger.hilt.EntryPoints
import kotlinx.coroutines.*
import org.rekotlin.Store
import timber.log.Timber
import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository as WalletConnect2Repository
lateinit var store: Store<AppState>
@ -165,8 +166,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase
get() = entryPoint.getSendFeedbackEmailUseCase()
private val getCardInfoUseCase: GetCardInfoUseCase
get() = entryPoint.getGetCardInfoUseCase()
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase
get() = entryPoint.getWalletMetaInfoUseCase()
private val urlOpener
get() = entryPoint.getUrlOpener()
@ -289,15 +290,16 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
tangemAppLoggerInitializer.initialize()
Timber.i("APP STARTED")
if (BuildConfig.TESTER_MENU_ENABLED) {
Timber.i(featureTogglesManager.toString())
Timber.i(excludedBlockchainsManager.toString())
}
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
// We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them.
runBlocking {
awaitAll(
async { featureTogglesManager.init() },
async { excludedBlockchainsManager.init() },
)
initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize())
}
@ -357,7 +359,7 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat
settingsRepository = settingsRepository,
blockchainSDKFactory = blockchainSDKFactory,
sendFeedbackEmailUseCase = sendFeedbackEmailUseCase,
getCardInfoUseCase = getCardInfoUseCase,
getWalletMetaInfoUseCase = getWalletMetaInfoUseCase,
issuersConfigStorage = issuersConfigStorage,
urlOpener = urlOpener,
shareManager = shareManager,

View file

@ -0,0 +1,13 @@
package com.tangem.tap.common.buildconfig
import com.tangem.utils.buildConfig.AppConfigurationProvider
import com.tangem.wallet.BuildConfig
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
internal class AppConfigurationProviderImpl @Inject constructor() : AppConfigurationProvider {
override fun isDebug(): Boolean = BuildConfig.BUILD_TYPE == "debug"
override fun isHuawei(): Boolean = BuildConfig.FLAVOR_NAME == "huawei"
}

View file

@ -0,0 +1,102 @@
package com.tangem.tap.common.pushes
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
class PushNotificationDelegate(private val context: Context) {
@Suppress("LongParameterList")
fun showNotification(
dataMap: Map<String, String>,
title: String?,
body: String?,
channelId: String,
priority: Int,
imageUrl: Uri? = null,
vibratePattern: LongArray?,
) {
val intent = Intent(context, MainActivity::class.java).apply {
dataMap.forEach { (key, value) ->
putExtra(key, value)
}
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ context,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
val notificationBuilder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(title)
.setContentText(body)
.setPriority(priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(vibratePattern)
.apply {
imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(context, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
context,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(context)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -1,30 +1,17 @@
package com.tangem.tap.common.pushes
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import coil.executeBlocking
import coil.request.ImageRequest
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.tangem.domain.common.LogConfig
import com.tangem.tap.MainActivity
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
import com.tangem.wallet.R
import timber.log.Timber
@SuppressLint("MissingFirebaseInstanceTokenRefresh")
internal class TangemPushNotificationService : FirebaseMessagingService() {
private val pushNotificationDelegate: PushNotificationDelegate by lazy {
PushNotificationDelegate(applicationContext)
}
override fun onNewToken(token: String) {
super.onNewToken(token)
Timber.d("New FCM token received: $token")
@ -36,73 +23,18 @@ internal class TangemPushNotificationService : FirebaseMessagingService() {
val notification = message.notification ?: return
val channelId = notification.channelId ?: TANGEM_CHANNEL_ID
val intent = Intent(applicationContext, MainActivity::class.java).apply {
message.data.forEach {
putExtra(it.key, it.value)
}
putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val pendingIntent = PendingIntent.getActivity(
/* context = */ this,
/* requestCode = */ PUSH_NOTIFICATION_REQUEST_CODE,
/* intent = */ intent,
/* flags = */ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
pushNotificationDelegate.showNotification(
dataMap = message.data,
title = notification.title,
body = notification.body,
channelId = channelId,
priority = message.priority,
imageUrl = notification.imageUrl,
vibratePattern = notification.vibrateTimings,
)
val notificationBuilder =
NotificationCompat.Builder(applicationContext, channelId)
.setSmallIcon(R.drawable.ic_tangem_24)
.setContentTitle(notification.title)
.setContentText(notification.body)
.setPriority(message.priority)
.setAutoCancel(true)
.setContentIntent(pendingIntent)
.setVibrate(notification.vibrateTimings)
.apply {
notification.imageUrl?.let { uri ->
val bitmap = getBitmapImageFromUrl(uri)
setStyle(
NotificationCompat
.BigPictureStyle()
.bigPicture(bitmap),
).setLargeIcon(bitmap)
}
}
val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel = NotificationChannel(
channelId,
ContextCompat.getString(applicationContext, R.string.tangem_app_name),
NotificationManager.IMPORTANCE_HIGH,
)
notificationManager.createNotificationChannel(notificationChannel)
}
// Generating unique notification id
val uniqueId = (System.currentTimeMillis() % Integer.MAX_VALUE).toInt()
notificationManager.notify(
/* id = */ uniqueId,
/* notification = */ notificationBuilder.build(),
)
}
private fun getBitmapImageFromUrl(url: Uri): Bitmap? {
return createCoilImageLoader(
applicationContext,
logEnabled = LogConfig.imageLoader,
).executeBlocking(
ImageRequest.Builder(applicationContext)
.data(url)
.build(),
).drawable?.toBitmap()
}
private companion object {
const val TANGEM_CHANNEL_ID = "Tangem General" // General channel for notifications
const val PUSH_NOTIFICATION_REQUEST_CODE = 123
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.data
import android.content.Context
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.sdk.storage.AndroidSecureStorageV2
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
private const val DEFAULT_KEY = "tangem_pay_default_key"
@Singleton
internal class DefaultTangemPayStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : TangemPayStorage {
private val secureStorage by lazy {
AndroidSecureStorageV2(
appContext = applicationContext,
useStrongBox = false,
name = "tangem_pay_storage",
)
}
override suspend fun store(authHeader: String) = withContext(dispatcherProvider.io) {
secureStorage.store(authHeader.encodeToByteArray(throwOnInvalidSequence = true), DEFAULT_KEY)
}
override suspend fun get(): String? = withContext(dispatcherProvider.io) {
secureStorage.get(DEFAULT_KEY)?.decodeToString(throwOnInvalidSequence = true)
}
override suspend fun clear() = withContext(dispatcherProvider.io) {
secureStorage.delete(DEFAULT_KEY)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.di
import com.tangem.tap.common.buildconfig.AppConfigurationProviderImpl
import com.tangem.utils.buildConfig.AppConfigurationProvider
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface AppConfigurationModule {
@Binds
@Singleton
fun bindAppConfigurationProvider(impl: AppConfigurationProviderImpl): AppConfigurationProvider
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.TangemPayStorage
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultTangemPayStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds
@ -21,4 +23,8 @@ internal interface VisaStorageModule {
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
@Binds
@Singleton
fun bindTangemPayStorage(impl: DefaultTangemPayStorage): TangemPayStorage
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.di.domain
import android.content.Context
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SaveBlockchainErrorUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.repository.FeedbackRepository
@ -18,8 +18,8 @@ internal object FeedbackDomainModule {
@Provides
@Singleton
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetCardInfoUseCase {
return GetCardInfoUseCase(feedbackRepository = feedbackRepository)
fun provideGetCardInfoUseCase(feedbackRepository: FeedbackRepository): GetWalletMetaInfoUseCase {
return GetWalletMetaInfoUseCase(feedbackRepository = feedbackRepository)
}
@Provides

View file

@ -6,7 +6,6 @@ import com.tangem.domain.nft.repository.NFTRepository
import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -33,29 +32,21 @@ internal object NFTDomainModule {
@Provides
@Singleton
fun providesFetchNFTCollectionsUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): FetchNFTCollectionsUseCase = FetchNFTCollectionsUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@Singleton
fun providesRefreshAllNFTUseCase(
currenciesRepository: CurrenciesRepository,
nftRepository: NFTRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshAllNFTUseCase = RefreshAllNFTUseCase(
currenciesRepository = currenciesRepository,
nftRepository = nftRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
@Provides
@ -127,16 +118,12 @@ internal object NFTDomainModule {
fun provideDisableWalletNFTUseCase(
walletsRepository: WalletsRepository,
nftRepository: NFTRepository,
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): DisableWalletNFTUseCase {
return DisableWalletNFTUseCase(
walletsRepository = walletsRepository,
nftRepository = nftRepository,
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -120,6 +120,12 @@ internal object OnrampDomainModule {
return OnrampSaveTransactionUseCase(onrampTransactionRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideOnrampSepaAvailableUseCase(onrampRepository: OnrampRepository): OnrampSepaAvailableUseCase {
return OnrampSepaAvailableUseCase(onrampRepository)
}
@Provides
@Singleton
fun provideOnrampUpdateTransactionStatusUseCase(

View file

@ -47,7 +47,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): AddCryptoCurrenciesUseCase {
return AddCryptoCurrenciesUseCase(
@ -56,7 +55,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -111,13 +109,11 @@ internal object TokensDomainModule {
currenciesRepository: CurrenciesRepository,
walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RemoveCurrencyUseCase {
return RemoveCurrencyUseCase(
currenciesRepository = currenciesRepository,
walletManagersFacade = walletManagersFacade,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -156,7 +152,6 @@ internal object TokensDomainModule {
dispatchers: CoroutineDispatcherProvider,
baseCurrencyStatusOperations: BaseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCurrencyWarningsUseCase {
return GetCurrencyWarningsUseCase(
walletManagersFacade = walletManagersFacade,
@ -165,7 +160,6 @@ internal object TokensDomainModule {
currencyChecksRepository = currencyChecksRepository,
currencyStatusOperations = baseCurrencyStatusOperations,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -177,7 +171,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
stakingIdFactory: StakingIdFactory,
): FetchCurrencyStatusUseCase {
return FetchCurrencyStatusUseCase(
@ -186,7 +179,6 @@ internal object TokensDomainModule {
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}
@ -214,9 +206,8 @@ internal object TokensDomainModule {
fun provideGetCryptoCurrencyUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): GetCryptoCurrencyUseCase {
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier, tokensFeatureToggles)
return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier)
}
@Provides
@ -238,13 +229,11 @@ internal object TokensDomainModule {
fun provideApplyTokenListSortingUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): ApplyTokenListSortingUseCase {
return ApplyTokenListSortingUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -304,14 +293,10 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideIsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): IsCryptoCurrencyCoinCouldHideUseCase {
return IsCryptoCurrencyCoinCouldHideUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -330,13 +315,11 @@ internal object TokensDomainModule {
fun provideGetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository: CurrenciesRepository,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
dispatchers: CoroutineDispatcherProvider,
): GetBalanceNotEnoughForFeeWarningUseCase {
return GetBalanceNotEnoughForFeeWarningUseCase(
currenciesRepository = currenciesRepository,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
dispatchers = dispatchers,
)
}
@ -377,16 +360,12 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideRefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository: CurrenciesRepository,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): RefreshMultiCurrencyWalletQuotesUseCase {
return RefreshMultiCurrencyWalletQuotesUseCase(
currenciesRepository = currenciesRepository,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}
@ -402,18 +381,13 @@ internal object TokensDomainModule {
@Provides
@Singleton
fun provideBaseCurrencyStatusOperations(
tokensFeatureToggles: TokensFeatureToggles,
currenciesRepository: CurrenciesRepository,
quotesRepository: QuotesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
singleNetworkStatusFetcher: SingleNetworkStatusFetcher,
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
stakingIdFactory: StakingIdFactory,
): BaseCurrencyStatusOperations {
@ -422,15 +396,10 @@ internal object TokensDomainModule {
quotesRepository = quotesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
singleNetworkStatusFetcher = singleNetworkStatusFetcher,
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
stakingIdFactory = stakingIdFactory,
)
}

View file

@ -6,8 +6,6 @@ import com.tangem.domain.demo.models.DemoConfig
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
import com.tangem.domain.networks.single.SingleNetworkStatusSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.transaction.FeeRepository
import com.tangem.domain.transaction.TransactionRepository
import com.tangem.domain.transaction.WalletAddressServiceRepository
@ -63,18 +61,14 @@ internal object TransactionDomainModule {
fun provideAssociateAssetUseCase(
cardSdkConfigRepository: CardSdkConfigRepository,
walletManagersFacade: WalletManagersFacade,
currenciesRepository: CurrenciesRepository,
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles: TokensFeatureToggles,
): AssociateAssetUseCase {
return AssociateAssetUseCase(
cardSdkConfigRepository = cardSdkConfigRepository,
walletManagersFacade = walletManagersFacade,
currenciesRepository = currenciesRepository,
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
tokensFeatureToggles = tokensFeatureToggles,
)
}

View file

@ -25,7 +25,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
@Module
@InstallIn(SingletonComponent::class)
internal object WalletsDomainModule {
@ -352,4 +352,24 @@ internal object WalletsDomainModule {
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesIsUpgradeWalletNotificationEnabledUseCase(
walletsRepository: WalletsRepository,
): IsUpgradeWalletNotificationEnabledUseCase {
return IsUpgradeWalletNotificationEnabledUseCase(
walletsRepository = walletsRepository,
)
}
@Provides
@Singleton
fun providesDismissUpgradeWalletNotificationUseCase(
walletsRepository: WalletsRepository,
): DismissUpgradeWalletNotificationUseCase {
return DismissUpgradeWalletNotificationUseCase(
walletsRepository = walletsRepository,
)
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.di.hot
import com.tangem.data.wallets.hot.DefaultHotWalletAccessor
import com.tangem.domain.wallets.hot.HotWalletAccessor
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.tap.features.hot.TangemHotSDKProxy
import dagger.Binds
@ -15,4 +17,8 @@ internal interface TangemHotSdkModule {
@Binds
@Singleton
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
@Binds
@Singleton
fun bindHotWalletAccessor(default: DefaultHotWalletAccessor): HotWalletAccessor
}

View file

@ -265,7 +265,7 @@ internal class LegacyScanProcessor @Inject constructor(
onOk = { mainScope.launch { onSuccess() } },
onSupportClick = {
val cardInfo =
store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {

View file

@ -22,7 +22,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.visa.error.VisaActivationError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
@ -46,7 +46,7 @@ class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val coroutineScope: CoroutineScope,
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
) : CardSessionRunnable<VisaCardActivationResponse> {
@ -168,7 +168,7 @@ class VisaCardActivationTask @AssistedInject constructor(
signedChallenge: VisaAuthSignedChallenge,
cardWalletAddress: String,
): Either<TangemError, VisaDataToSignByCardWallet> = either {
val tokens = visaAuthRepository.getAccessTokens(signedChallenge)
val tokens = visaAuthRemoteDataSource.getAccessTokens(signedChallenge)
.getOrElse { raise(it.tangemError) }
visaAuthTokenStorage.store(cardId, tokens)

View file

@ -4,9 +4,5 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.domain.tokens.TokensFeatureToggles
internal class DefaultTokensFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles {
override val isWalletBalanceFetcherEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "WALLET_BALANCE_FETCHER_ENABLED")
}
@Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager,
) : TokensFeatureToggles

View file

@ -62,7 +62,7 @@ internal class DefaultUserWalletsListRepository(
// If we don't save persistent information, we don't need to load user wallets
// and we should clear any existing data
clearPersistentData()
userWallets.value = emptyList()
updateWallets { emptyList() }
return
}
@ -118,7 +118,7 @@ internal class DefaultUserWalletsListRepository(
}
// update the userWallets state and add if it doesn't exist
userWallets.update { currentWallets ->
updateWallets { currentWallets ->
val wallets = currentWallets ?: emptyList()
if (wallets.any { it.walletId == userWallet.walletId }) {
wallets.map { if (it.walletId == userWallet.walletId) userWallet else it }
@ -248,7 +248,7 @@ internal class DefaultUserWalletsListRepository(
removePasswordAttempts(userWallet)
sensitiveInformationRepository.getAll(listOf(encryptionKey))
.doOnSuccess { sensitiveInfo -> userWallets.update { it?.updateWith(sensitiveInfo) } }
.doOnSuccess { sensitiveInfo -> updateWallets { it?.updateWith(sensitiveInfo) } }
.doOnFailure { error ->
raise(UnlockWalletError.UnableToUnlock)
}
@ -306,7 +306,7 @@ internal class DefaultUserWalletsListRepository(
sensitiveInformationRepository.getAll(allKeys)
.doOnSuccess { sensitiveInfo ->
userWallets.update { it?.updateWith(sensitiveInfo) }
updateWallets { it?.updateWith(sensitiveInfo) }
}
.doOnFailure { raise(UnlockWalletError.UnableToUnlock) }
}
@ -318,7 +318,7 @@ internal class DefaultUserWalletsListRepository(
raise(LockWalletsError.NothingToLock)
}
userWallets.update {
updateWallets {
it?.map {
if (it.walletId !in unsecuredWalletIds) {
it.lock()
@ -389,6 +389,15 @@ internal class DefaultUserWalletsListRepository(
return tangemSdkManagerProvider.invoke().canUseBiometry && useBiometricAuthentication
}
private fun updateWallets(block: (List<UserWallet>?) -> List<UserWallet>?) {
userWallets.update(block)
selectedUserWallet.update { currentSelected ->
if (currentSelected == null) return@update null
userWallets.value?.find { it.walletId == currentSelected.walletId }
}
}
/**
* Find the nearest available wallet that can be selected
*

View file

@ -15,7 +15,7 @@ import com.tangem.domain.visa.error.VisaApiError
import com.tangem.domain.visa.error.VisaCardScanError
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.attestation.AttestCardKeyResponse
import com.tangem.operations.attestation.AttestWalletKeyResponse
@ -26,7 +26,7 @@ import javax.inject.Inject
import kotlin.coroutines.resume
internal class VisaCardScanHandler @Inject constructor(
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaActivationRepositoryFactory: VisaActivationRepository.Factory,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
) {
@ -83,7 +83,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting challenge for wallet authorization")
val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(
val challengeResponse = visaAuthRemoteDataSource.getCardWalletAuthChallenge(
cardId = card.cardId,
// This is the wallet public key, not the address and it's alright, as the API expects it in this format
cardWalletAddress = wallet.publicKey.toHexString(),
@ -122,7 +122,7 @@ internal class VisaCardScanHandler @Inject constructor(
cardWalletAddress: String,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationStatus> {
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge)
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(signedChallenge = signedChallenge)
.getOrElse {
Timber.i("Failed to get Access token for Wallet public key authorization.")
return if (
@ -149,7 +149,7 @@ internal class VisaCardScanHandler @Inject constructor(
Timber.i("Requesting authorization challenge to sign")
val challengeResponse = visaAuthRepository.getCardAuthChallenge(
val challengeResponse = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = card.cardId,
cardPublicKey = card.cardPublicKey.toHexString(),
).getOrElse {
@ -174,7 +174,7 @@ internal class VisaCardScanHandler @Inject constructor(
}
}
val authorizationTokensResponse = visaAuthRepository.getAccessTokens(
val authorizationTokensResponse = visaAuthRemoteDataSource.getAccessTokens(
signedChallenge = challengeResponse.toSignedChallenge(
signedChallenge = attestCardKeyResponse.cardSignature.toHexString(),
salt = attestCardKeyResponse.salt.toHexString(),

View file

@ -47,6 +47,14 @@ class WalletConnectSdkHelper {
store.inject(DaggerGraphState::generalUserWalletsListManager)
}
private val userWalletsListRepository by lazy {
store.inject(DaggerGraphState::userWalletsListRepository)
}
private val hotWalletFeatureToggles by lazy {
store.inject(DaggerGraphState::hotWalletFeatureToggles)
}
@Suppress("MagicNumber")
suspend fun prepareTransactionData(data: EthTransactionData): WcTransactionData {
val transaction = data.transaction
@ -128,13 +136,13 @@ class WalletConnectSdkHelper {
)
}
fun isDemoCard(): Boolean {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return false
suspend fun isDemoCard(): Boolean {
val userWallet = getSelectedWallet() ?: return false
return userWallet is UserWallet.Cold && userWallet.scanResponse.isDemoCard()
}
private suspend fun getWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager? {
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return null
val userWallet = getSelectedWallet() ?: return null
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
return walletManagerFacade.getOrCreateWalletManager(
userWalletId = userWallet.walletId,
@ -481,6 +489,14 @@ class WalletConnectSdkHelper {
}
}
private suspend fun getSelectedWallet(): UserWallet? {
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
private fun getSolanaResultString(signedHash: ByteArray) = "{ signature: \"${signedHash.encodeBase58()}\" }"
/**

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.toNetworkId
import com.tangem.common.routing.AppRoute
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.inject
@ -183,9 +184,19 @@ class WalletConnectMiddleware {
private suspend fun getWalletManagers(): List<WalletManager> {
val walletManagerFacade = store.inject(DaggerGraphState::walletManagersFacade)
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWallet = userWalletsListManager.selectedUserWalletSync ?: return emptyList()
val userWallet = getSelectedWallet() ?: return emptyList()
return walletManagerFacade.getStoredWalletManagers(userWallet.walletId)
}
private suspend fun getSelectedWallet(): UserWallet? {
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
userWalletsListRepository.selectedUserWalletSync()
} else {
userWalletsListManager.selectedUserWalletSync
}
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchNavigationAction
import com.tangem.tap.common.extensions.onUserWalletSelected
@ -50,6 +51,7 @@ internal class ResetCardModel @Inject constructor(
private val userWalletsListManager: UserWalletsListManager,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cardSettingsInteractor: CardSettingsInteractor,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) : Model() {
private val params = paramsContainer.require<ResetCardComponent.Params>()
@ -259,16 +261,20 @@ internal class ResetCardModel @Inject constructor(
private fun finishFullReset() {
cardSettingsInteractor.clear()
val newSelectedWallet = userWalletsListManager.selectedUserWalletSync
val newSelectedWallet = getSelectedWalletSyncUseCase.invoke().getOrNull()
if (newSelectedWallet != null) {
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
if (hotWalletFeatureToggles.isHotWalletEnabled) {
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
} else {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync }.isSuccess
if (isLocked && userWalletsListManager.hasUserWallets) {
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
} else {
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
}
}
}
}

View file

@ -24,6 +24,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
import com.tangem.domain.common.LogConfig
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.notifications.GetApplicationIdUseCase
import com.tangem.domain.notifications.SendPushTokenUseCase
import com.tangem.domain.notifications.models.ApplicationId
@ -37,9 +38,9 @@ import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
import com.tangem.domain.staking.FetchStakingTokensUseCase
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase
import com.tangem.domain.wallets.usecase.GetSavedWalletsCountUseCase
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase
import com.tangem.feature.swap.analytics.StoriesEvents
import com.tangem.tap.common.extensions.setContext
@ -69,7 +70,6 @@ internal class MainViewModel @Inject constructor(
deleteDeprecatedLogsUseCase: DeleteDeprecatedLogsUseCase,
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
private val blockchainSDKFactory: BlockchainSDKFactory,
private val userWalletsListManager: UserWalletsListManager,
private val dispatchers: CoroutineDispatcherProvider,
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
@ -90,6 +90,7 @@ internal class MainViewModel @Inject constructor(
private val multiQuoteUpdater: MultiQuoteUpdater,
private val appStateHolder: AppStateHolder,
private val environmentConfigStorage: EnvironmentConfigStorage,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
) : ViewModel() {
@ -185,13 +186,16 @@ internal class MainViewModel @Inject constructor(
}
private fun prepareSelectedWalletFeedback() {
userWalletsListManager.selectedUserWallet
.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet)
getSelectedWalletUseCase.invoke()
.mapLeft { emptyFlow<UserWallet>() }
.onRight {
it.distinctUntilChanged()
.onEach { userWallet ->
Analytics.setContext(userWallet)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
.flowOn(dispatchers.io)
.launchIn(viewModelScope)
}
private suspend fun fetchStakingTokens() {
@ -214,7 +218,7 @@ internal class MainViewModel @Inject constructor(
apiKey = environmentConfig.moonPayApiKey,
secretKey = environmentConfig.moonPayApiSecretKey,
logEnabled = LogConfig.network.moonPayService,
userWalletProvider = { userWalletsListManager.selectedUserWalletSync },
userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() },
)
}

View file

@ -30,7 +30,7 @@ object WalletActivationErrorDialog {
val scanResponse = store.state.globalState.scanResponse
?: error("ScanResponse must be not null")
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
val cardInfo = store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull()
?: error("CardInfo must be not null")
scope.launch {

View file

@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.tap.common.extensions.inject
import com.tangem.tap.domain.model.Currency
import com.tangem.tap.proxy.redux.DaggerGraphState
@ -24,9 +25,7 @@ internal class CryptoCurrencyConverter(
cryptoCurrencyFactory.createCoin(
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = requireNotNull(
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync,
),
userWallet = getSelectedWallet(),
),
)
is Currency.Token -> requireNotNull(
@ -34,9 +33,7 @@ internal class CryptoCurrencyConverter(
sdkToken = value.token,
blockchain = value.blockchain,
extraDerivationPath = value.derivationPath,
userWallet = requireNotNull(
store.inject(DaggerGraphState::generalUserWalletsListManager).selectedUserWalletSync,
),
userWallet = getSelectedWallet(),
),
)
}
@ -63,4 +60,15 @@ internal class CryptoCurrencyConverter(
)
}
}
fun getSelectedWallet(): UserWallet {
val userWalletListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository)
val hotWalletFeatureToggles = store.inject(DaggerGraphState::hotWalletFeatureToggles)
return if (hotWalletFeatureToggles.isHotWalletEnabled) {
requireNotNull(userWalletsListRepository.selectedUserWallet.value)
} else {
requireNotNull(userWalletListManager.selectedUserWalletSync)
}
}
}

View file

@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyAmount
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -15,13 +15,13 @@ import java.math.BigDecimal
class UserWalletManagerImpl(
private val walletManagersFacade: WalletManagersFacade,
private val userWalletsListManager: UserWalletsListManager,
private val getSelectedWalletUseCase: GetSelectedWalletUseCase,
private val dispatchers: CoroutineDispatcherProvider,
) : UserWalletManager {
override fun getWalletId(): String {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
getSelectedWalletUseCase.sync().getOrNull(),
) { "selectedUserWallet shouldn't be null" }
return selectedUserWallet.walletId.stringValue
}
@ -62,7 +62,7 @@ class UserWalletManagerImpl(
@Throws(IllegalArgumentException::class)
private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager {
val selectedUserWallet = requireNotNull(
userWalletsListManager.selectedUserWalletSync,
getSelectedWalletUseCase.sync().getOrNull(),
) { "userWallet or userWalletsListManager is null" }
val walletManager = withContext(dispatchers.io) {
walletManagersFacade.getOrCreateWalletManager(

View file

@ -1,7 +1,7 @@
package com.tangem.tap.proxy.di
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.tap.proxy.AppStateHolder
import com.tangem.tap.proxy.UserWalletManagerImpl
@ -26,12 +26,12 @@ internal object ProxyModule {
@Singleton
fun provideUserWalletManager(
walletManagersFacade: WalletManagersFacade,
userWalletsListManager: UserWalletsListManager,
getSelectedWalletUseCase: GetSelectedWalletUseCase,
dispatchers: CoroutineDispatcherProvider,
): UserWalletManager {
return UserWalletManagerImpl(
walletManagersFacade = walletManagersFacade,
userWalletsListManager = userWalletsListManager,
getSelectedWalletUseCase = getSelectedWalletUseCase,
dispatchers = dispatchers,
)
}

View file

@ -22,7 +22,7 @@ import com.tangem.domain.card.ScanCardUseCase
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onboarding.WasTwinsOnboardingShownUseCase
@ -63,7 +63,7 @@ data class DaggerGraphState(
val settingsRepository: SettingsRepository? = null,
val blockchainSDKFactory: BlockchainSDKFactory? = null,
val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase? = null,
val getCardInfoUseCase: GetCardInfoUseCase? = null,
val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase? = null,
val issuersConfigStorage: IssuersConfigStorage? = null,
val urlOpener: UrlOpener? = null,
val shareManager: ShareManager? = null,

View file

@ -17,6 +17,7 @@ import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.home.api.HomeComponent
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.UpgradeWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.CreateWalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
@ -103,6 +104,7 @@ internal class ChildFactory @Inject constructor(
private val chooseManagedTokensComponentFactory: ChooseManagedTokensComponent.Factory,
private val createWalletSelectionComponentFactory: CreateWalletSelectionComponent.Factory,
private val createMobileWalletComponentFactory: CreateMobileWalletComponent.Factory,
private val upgradeWalletComponentFactory: UpgradeWalletComponent.Factory,
private val addExistingWalletComponentFactory: AddExistingWalletComponent.Factory,
private val walletActivationComponentFactory: WalletActivationComponent.Factory,
private val createWalletBackupComponentFactory: CreateWalletBackupComponent.Factory,
@ -205,6 +207,7 @@ internal class ChildFactory @Inject constructor(
userWalletId = route.userWalletId,
cryptoCurrency = route.currency,
source = route.source,
launchSepa = route.launchSepa,
),
componentFactory = onrampComponentFactory,
)
@ -486,6 +489,15 @@ internal class ChildFactory @Inject constructor(
componentFactory = createMobileWalletComponentFactory,
)
}
is AppRoute.UpgradeWallet -> {
createComponentChild(
context = context,
params = UpgradeWalletComponent.Params(
userWalletId = route.userWalletId,
),
componentFactory = upgradeWalletComponentFactory,
)
}
is AppRoute.AddExistingWallet -> {
createComponentChild(
context = context,

View file

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/splash_logo"
android:insetLeft="48dp"
android:insetRight="48dp"
android:insetTop="48dp"
android:insetBottom="48dp"/>
android:insetLeft="55dp"
android:insetRight="55dp"
android:insetTop="55dp"
android:insetBottom="55dp"/>
<!-- image should fit exactly in square 192x192 dp-->
<!-- (192 - splash_logo_side)/2 = 48dp -->
<!-- (192 - splash_logo_side)/2 = 55dp -->

View file

@ -1,15 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="96dp"
android:height="96dp"
android:viewportWidth="450"
android:viewportHeight="450">
<path
android:fillColor="#ffffff"
android:pathData="M284.96,82H165.04C146.12,82 136.67,82 129.44,85.68C123.08,88.93 117.91,94.08 114.68,100.44C111,107.67 111,117.12 111,136.04V156.1H339V136.04C339,117.12 339,107.67 335.32,100.44C332.07,94.08 326.92,88.91 320.56,85.68C313.33,82 303.88,82 284.96,82Z" />
<path
android:fillColor="#ffffff"
android:pathData="M262.99,218.8H338.99V312.96C338.99,331.88 338.99,341.33 335.31,348.56C332.07,354.92 326.91,360.09 320.56,363.32C313.32,367 303.87,367 284.95,367H263.01L262.99,218.8Z" />
<path
android:fillColor="#ffffff"
android:pathData="M111,218.8H186.99V367H165.04C146.12,367 136.67,367 129.44,363.32C123.08,360.09 117.93,354.92 114.68,348.56C111,341.33 111,331.88 111,312.96V218.8Z" />
</vector>

View file

@ -26,6 +26,4 @@
<color name="text_primary_1">#1E1E1E</color>
<color name="text_secondary">#656565</color>
<color name="background_splash">#000000</color>
</resources>

View file

@ -16,7 +16,7 @@
</style>
<style name="SplashTheme" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/background_splash</item>
<item name="windowSplashScreenBackground">@color/background_primary</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/inset_splash</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>