Updated on 2026-08-14
This commit is contained in:
commit
ff24b00af4
404 changed files with 12634 additions and 3818 deletions
12
.gitignore
vendored
12
.gitignore
vendored
|
|
@ -3,12 +3,24 @@
|
|||
# Built application files
|
||||
/build
|
||||
/buildSrc
|
||||
**/build/.transforms/**
|
||||
**/build/classes/**
|
||||
**/build/generated/**
|
||||
**/build/intermediates/**
|
||||
**/build/kotlin/**
|
||||
**/build/libs/**
|
||||
**/build/outputs/**
|
||||
**/build/tmp/**
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Google services
|
||||
|
||||
|
||||
# Huawei services
|
||||
app/agconnect-services.json
|
||||
|
||||
app/src/debug/google-services.json
|
||||
app/src/internal/google-services.json
|
||||
app/src/external/google-services.json
|
||||
|
|
|
|||
|
|
@ -11,9 +11,14 @@ plugins {
|
|||
alias(deps.plugins.firebase.crashlytics)
|
||||
alias(deps.plugins.firebase.perf)
|
||||
alias(deps.plugins.ksp)
|
||||
id(deps.plugins.agconnect.get().pluginId)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
agcp {
|
||||
manifest = false
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.wallet"
|
||||
testOptions {
|
||||
|
|
@ -53,12 +58,35 @@ android {
|
|||
keyPassword = keystoreProperties["key_password"] as String
|
||||
}
|
||||
}
|
||||
|
||||
flavorDimensions += "services"
|
||||
|
||||
productFlavors {
|
||||
create("google") {
|
||||
dimension = "services"
|
||||
buildConfigField("String", "FLAVOR_NAME", "\"google\"")
|
||||
}
|
||||
create("huawei") {
|
||||
dimension = "services"
|
||||
buildConfigField("String", "FLAVOR_NAME", "\"huawei\"")
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
buildConfigField("String", "BUILD_TYPE", "\"debug\"")
|
||||
}
|
||||
release {
|
||||
buildConfigField("String", "BUILD_TYPE", "\"release\"")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
exclude(group = "org.bouncycastle", module = "bcprov-jdk15to18")
|
||||
exclude(group = "com.github.komputing.kethereum")
|
||||
exclude(group = "com.android.tools.build", module = "gradle")
|
||||
|
||||
resolutionStrategy {
|
||||
dependencySubstitution {
|
||||
|
|
@ -379,4 +407,9 @@ dependencies {
|
|||
// excludes version 9999.0-empty-to-avoid-conflict-with-guava
|
||||
exclude(group = "com.google.guava", module = "listenablefuture")
|
||||
}
|
||||
|
||||
/** Huawei flavor-specific dependencies */
|
||||
"huaweiImplementation"(deps.huawei.push)
|
||||
"huaweiImplementation"(deps.agconnect.agcp)
|
||||
"huaweiImplementation"(deps.agconnect.core)
|
||||
}
|
||||
7
app/proguard-rules.pro
vendored
7
app/proguard-rules.pro
vendored
|
|
@ -7,6 +7,13 @@
|
|||
-keep class com.google.android.gms.internal.** { *; }
|
||||
-keepclasseswithmembers class com.google.firebase.FirebaseException
|
||||
|
||||
# huawei push kit
|
||||
-ignorewarnings
|
||||
-keepattributes SourceFile,LineNumberTable
|
||||
-keep class com.huawei.hianalytics.**{*;}
|
||||
-keep class com.huawei.updatesdk.**{*;}
|
||||
-keep class com.huawei.hms.**{*;}
|
||||
|
||||
# hedera sdk
|
||||
-keep class com.hedera.hashgraph.sdk.** { *; }
|
||||
-keep interface com.hedera.hashgraph.sdk.** { *; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
38
app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt
Normal file
38
app/src/androidTest/kotlin/com/tangem/steps/BaseScenarios.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
181
app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt
Normal file
181
app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt
Normal 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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
17
app/src/huawei/AndroidManifest.xml
Normal file
17
app/src/huawei/AndroidManifest.xml
Normal 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>
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
47
app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt
Normal file
47
app/src/huawei/java/com/tangem/tap/HuaweiPushService.kt
Normal 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
|
||||
}
|
||||
}
|
||||
18
app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt
Normal file
18
app/src/huawei/java/com/tangem/tap/di/HuaweiPushModule.kt
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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()}\" }"
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
@ -26,6 +26,4 @@
|
|||
<color name="text_primary_1">#1E1E1E</color>
|
||||
<color name="text_secondary">#656565</color>
|
||||
|
||||
<color name="background_splash">#000000</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,13 @@ plugins {
|
|||
alias(deps.plugins.ksp) apply false
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath(deps.gradle.android)
|
||||
classpath(deps.agconnect.agcp)
|
||||
}
|
||||
}
|
||||
|
||||
val clean by tasks.registering {
|
||||
delete(rootProject.buildDir)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
package com.tangem.common.routing
|
||||
|
||||
import android.os.Bundle
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.common.routing.bundle.RouteBundleParams
|
||||
import com.tangem.common.routing.bundle.bundle
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.decompose.navigation.Route
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.feedback.models.CardInfo
|
||||
import com.tangem.domain.feedback.models.WalletMetaInfo
|
||||
import com.tangem.domain.markets.TokenMarketParams
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
|
|
@ -83,8 +82,8 @@ sealed class AppRoute(val path: String) : Route {
|
|||
|
||||
@Serializable
|
||||
data class Usedesk(
|
||||
val cardInfo: CardInfo,
|
||||
) : AppRoute(path = "/usedesk/${cardInfo.cardId}")
|
||||
val walletMetaInfo: WalletMetaInfo,
|
||||
) : AppRoute(path = "/usedesk/${walletMetaInfo.userWalletId}")
|
||||
|
||||
@Serializable
|
||||
data class CardSettings(
|
||||
|
|
@ -239,6 +238,7 @@ sealed class AppRoute(val path: String) : Route {
|
|||
val source: OnrampSource,
|
||||
val userWalletId: UserWalletId,
|
||||
val currency: CryptoCurrency,
|
||||
val launchSepa: Boolean = false,
|
||||
) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams {
|
||||
override fun getBundle(): Bundle = bundle(serializer())
|
||||
}
|
||||
|
|
@ -311,6 +311,11 @@ sealed class AppRoute(val path: String) : Route {
|
|||
@Serializable
|
||||
object CreateMobileWallet : AppRoute(path = "/create_mobile_wallet")
|
||||
|
||||
@Serializable
|
||||
data class UpgradeWallet(
|
||||
val userWalletId: UserWalletId,
|
||||
) : AppRoute(path = "/upgrade_wallet/${userWalletId.stringValue}")
|
||||
|
||||
@Serializable
|
||||
object AddExistingWallet : AppRoute(path = "/add_existing_wallet")
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
|
@ -73,6 +74,8 @@ private fun Title(title: TextReference) {
|
|||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +85,8 @@ private fun Subtitle(subtitle: TextReference) {
|
|||
color = TangemTheme.colors.text.tertiary,
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = subtitle.resolveReference(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -139,33 +139,59 @@ private fun NameAndInfo(
|
|||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = " $DOT ",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = balance,
|
||||
label = "Balance content",
|
||||
) { balance ->
|
||||
val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance)
|
||||
BalanceContent(balance)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (balanceValue == null) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = "aaaaa",
|
||||
)
|
||||
} else {
|
||||
@Composable
|
||||
private fun BalanceContent(balance: UserWalletItemUM.Balance, modifier: Modifier = Modifier) {
|
||||
AnimatedContent(
|
||||
modifier = modifier,
|
||||
targetState = balance,
|
||||
label = "Balance content",
|
||||
) { balance ->
|
||||
when (balance) {
|
||||
UserWalletItemUM.Balance.Locked -> {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.padding(start = 4.dp, bottom = 2.dp, top = 2.dp)
|
||||
.size(12.dp),
|
||||
imageVector = ImageVector.vectorResource(R.drawable.ic_lock_24),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
UserWalletItemUM.Balance.NotShowing -> {
|
||||
/** No balance and no dot */
|
||||
}
|
||||
else -> {
|
||||
Row {
|
||||
Text(
|
||||
text = balanceValue,
|
||||
style = TangemTheme.typography.caption2.applyBladeBrush(
|
||||
isEnabled = isFlickering,
|
||||
textColor = TangemTheme.colors.text.tertiary,
|
||||
),
|
||||
text = " $DOT ",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
)
|
||||
|
||||
val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance)
|
||||
|
||||
if (balanceValue == null) {
|
||||
TextShimmer(
|
||||
style = TangemTheme.typography.caption2,
|
||||
text = "aaaaa",
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = balanceValue,
|
||||
style = TangemTheme.typography.caption2.applyBladeBrush(
|
||||
isEnabled = isFlickering,
|
||||
textColor = TangemTheme.colors.text.tertiary,
|
||||
),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -246,9 +272,8 @@ fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<Stri
|
|||
return when (balance) {
|
||||
is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false
|
||||
is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false
|
||||
is UserWalletItemUM.Balance.Loading -> null to false
|
||||
is UserWalletItemUM.Balance.Locked -> stringResourceSafe(R.string.common_locked) to false
|
||||
is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering
|
||||
else -> null to false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -392,6 +417,15 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
|
|||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
UserWalletItemUM(
|
||||
id = UserWalletId("user_wallet_3".encodeToByteArray()),
|
||||
name = stringReference("Multi Card"),
|
||||
information = getInformation(cardCount = 3),
|
||||
balance = UserWalletItemUM.Balance.NotShowing,
|
||||
imageState = UserWalletItemUM.ImageState.MobileWallet,
|
||||
isEnabled = true,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
|
||||
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter
|
|||
|
||||
import com.tangem.common.ui.R
|
||||
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.components.label.entity.LabelStyle
|
||||
import com.tangem.core.ui.components.label.entity.LabelUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -12,7 +12,6 @@ import com.tangem.core.ui.format.bigdecimal.fiat
|
|||
import com.tangem.core.ui.format.bigdecimal.format
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.card.common.util.getCardsCount
|
||||
import com.tangem.domain.models.ArtworkModel
|
||||
import com.tangem.domain.models.StatusSource
|
||||
import com.tangem.domain.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -37,10 +36,10 @@ class UserWalletItemUMConverter(
|
|||
private val isBalanceHidden: Boolean = false,
|
||||
private val authMode: Boolean = false,
|
||||
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
|
||||
private val artwork: ArtworkModel? = null,
|
||||
artwork: UserWalletItemUM.ImageState? = null,
|
||||
) : Converter<UserWallet, UserWalletItemUM> {
|
||||
|
||||
private val artworkUMConverter = ArtworkUMConverter()
|
||||
private val artwork = artwork ?: UserWalletItemUM.ImageState.Loading
|
||||
|
||||
override fun convert(value: UserWallet): UserWalletItemUM {
|
||||
return with(value) {
|
||||
|
|
@ -52,7 +51,7 @@ class UserWalletItemUMConverter(
|
|||
isEnabled = isEnabled(userWallet = this),
|
||||
endIcon = endIcon,
|
||||
onClick = { onClick(value.walletId) },
|
||||
imageState = getImageState(userWallet = value),
|
||||
imageState = artwork,
|
||||
label = getLabelOrNull(userWallet = this),
|
||||
)
|
||||
}
|
||||
|
|
@ -73,14 +72,6 @@ class UserWalletItemUMConverter(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState {
|
||||
return when {
|
||||
userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet
|
||||
artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork))
|
||||
else -> UserWalletItemUM.ImageState.Loading
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
|
||||
val text = when (userWallet) {
|
||||
is UserWallet.Cold -> {
|
||||
|
|
@ -100,8 +91,9 @@ class UserWalletItemUMConverter(
|
|||
|
||||
private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance {
|
||||
return when {
|
||||
isBalanceHidden -> UserWalletItemUM.Balance.Hidden
|
||||
userWallet.isLocked -> UserWalletItemUM.Balance.Locked
|
||||
authMode -> UserWalletItemUM.Balance.NotShowing
|
||||
isBalanceHidden -> UserWalletItemUM.Balance.Hidden
|
||||
balance == null -> UserWalletItemUM.Balance.Loading
|
||||
else -> {
|
||||
when (balance) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ data class UserWalletItemUM(
|
|||
|
||||
data object Hidden : Balance()
|
||||
|
||||
data object NotShowing : Balance()
|
||||
|
||||
data object Locked : Balance()
|
||||
|
||||
data object Failed : Balance()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants
|
||||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import org.json.JSONArray
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
|
|
@ -9,8 +11,24 @@ plugins {
|
|||
id("configuration")
|
||||
}
|
||||
|
||||
buildscript {
|
||||
dependencies {
|
||||
classpath("com.squareup:kotlinpoet:1.15.0")
|
||||
classpath("org.json:json:20231013")
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.configtoggle"
|
||||
sourceSets["main"].java.srcDir("build/generated/source/toggles")
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(generateFeatureToggles, generateExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
@ -32,7 +50,72 @@ dependencies {
|
|||
implementation(projects.core.utils)
|
||||
|
||||
testImplementation(deps.test.coroutine)
|
||||
testImplementation(deps.test.junit)
|
||||
testImplementation(deps.test.junit5)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.mockk)
|
||||
testImplementation(deps.test.truth)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
||||
val generateFeatureToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/feature_toggles_config.json",
|
||||
generatedFileName = "FeatureToggles",
|
||||
)
|
||||
}
|
||||
|
||||
val generateExcludedBlockchainToggles by tasks.registering {
|
||||
generateToggles(
|
||||
inputFilePath = "src/main/assets/configs/excluded_blockchains_config.json",
|
||||
generatedFileName = "ExcludedBlockchainToggles",
|
||||
)
|
||||
}
|
||||
|
||||
fun Task.generateToggles(inputFilePath: String, generatedFileName: String) {
|
||||
val inputFile = file(inputFilePath)
|
||||
val outputDir = file("build/generated/source/toggles")
|
||||
|
||||
inputs.file(inputFile)
|
||||
outputs.dir(outputDir)
|
||||
|
||||
doLast {
|
||||
val jsonText = inputFile.readText()
|
||||
val jsonArray = JSONArray(jsonText)
|
||||
|
||||
val entries = (0 until jsonArray.length()).map { i ->
|
||||
val obj = jsonArray.getJSONObject(i)
|
||||
val name = obj.getString("name")
|
||||
val version = obj.getString("version")
|
||||
CodeBlock.of("%S to %S", name, version)
|
||||
}
|
||||
|
||||
val mapInitializer = CodeBlock.builder()
|
||||
.add("mapOf(\n")
|
||||
.indent()
|
||||
.apply {
|
||||
entries.forEachIndexed { index, entry ->
|
||||
add(entry)
|
||||
if (index != entries.lastIndex) add(",\n") else add("\n")
|
||||
}
|
||||
}
|
||||
.unindent()
|
||||
.add(")")
|
||||
.build()
|
||||
|
||||
val objectBuilder = TypeSpec.objectBuilder(name = generatedFileName)
|
||||
.addKdoc("Generated from $inputFilePath")
|
||||
.addProperty(
|
||||
PropertySpec.builder("values", MAP.parameterizedBy(STRING, STRING))
|
||||
.initializer(mapInitializer)
|
||||
.build()
|
||||
)
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName = "com.tangem.core.configtoggle", fileName = generatedFileName)
|
||||
.addType(objectBuilder.build())
|
||||
.build()
|
||||
|
||||
val outputPackageDir = File(outputDir, "")
|
||||
outputPackageDir.mkdirs()
|
||||
fileSpec.writeTo(outputPackageDir)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,10 +43,6 @@
|
|||
"name": "SEND_REDESIGN_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "WALLET_BALANCE_FETCHER_ENABLED",
|
||||
"version": "5.27.0"
|
||||
},
|
||||
{
|
||||
"name": "HOT_WALLET_ENABLED",
|
||||
"version": "undefined"
|
||||
|
|
@ -62,5 +58,17 @@
|
|||
{
|
||||
"name": "NEW_TOKEN_RECEIVE_ENABLED",
|
||||
"version": "5.28.0"
|
||||
},
|
||||
{
|
||||
"name": "YIELD_LENDING_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "NEW_ONRAMP_MAIN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACCOUNTS_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -3,6 +3,4 @@ package com.tangem.core.configtoggle.blockchain
|
|||
interface ExcludedBlockchainsManager {
|
||||
|
||||
val excludedBlockchainsIds: Set<String>
|
||||
|
||||
suspend fun init()
|
||||
}
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
internal class DefaultExcludedBlockchainsManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private var isInitialized: Boolean = false
|
||||
|
||||
private lateinit var currentExcludedBlockchains: MutableMap<String, Boolean>
|
||||
private lateinit var localExcludedBlockchains: Map<String, Boolean>
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() {
|
||||
if (!isInitialized) error("ExcludedBlockchainsManager is not initialized")
|
||||
|
||||
return currentExcludedBlockchains
|
||||
.filterValues { it }
|
||||
.keys
|
||||
}
|
||||
|
||||
override suspend fun init() {
|
||||
localTogglesStorage.populate(path = "configs/excluded_blockchains_config")
|
||||
|
||||
val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync<Boolean>(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
)
|
||||
|
||||
localExcludedBlockchains = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
.mapValues { (_, isIncluded) -> !isIncluded }
|
||||
|
||||
currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys)
|
||||
.fold(mutableMapOf()) { acc, blockchainId ->
|
||||
val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId]
|
||||
|
||||
requireNotNull(isExcluded) {
|
||||
"Unable to find $blockchainId in local or stored excluded blockchains"
|
||||
}
|
||||
|
||||
acc[blockchainId] = isExcluded
|
||||
acc
|
||||
}
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
currentExcludedBlockchains[mainnetId] = isExcluded
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return currentExcludedBlockchains == localExcludedBlockchains
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
currentExcludedBlockchains = localExcludedBlockchains.toMutableMap()
|
||||
|
||||
storeCurrent()
|
||||
}
|
||||
|
||||
private suspend fun storeCurrent() {
|
||||
appPreferencesStore.storeObjectMap(
|
||||
key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
value = currentExcludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* [MutableExcludedBlockchainsManager] implementation in dev or mocked build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
* @property localTogglesStorage local storage for blockchain toggles
|
||||
*/
|
||||
internal class DevExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
private val localTogglesStorage: LocalTogglesStorage,
|
||||
) : MutableExcludedBlockchainsManager {
|
||||
|
||||
private val fileBlockchainToggles: Map<String, Boolean> = getFileBlockchainToggles()
|
||||
private var blockchainTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override val excludedBlockchainsIds: Set<String>
|
||||
get() = blockchainTogglesMap.filterValues { !it }.keys
|
||||
|
||||
init {
|
||||
val savedExcludedBlockchains = runBlocking { localTogglesStorage.getSyncOrEmpty() }
|
||||
|
||||
blockchainTogglesMap = fileBlockchainToggles
|
||||
.mapValues { (blockchainId, isEnabled) ->
|
||||
savedExcludedBlockchains[blockchainId] ?: isEnabled
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) {
|
||||
blockchainTogglesMap[mainnetId] = isExcluded
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean {
|
||||
return blockchainTogglesMap == fileBlockchainToggles
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
blockchainTogglesMap = fileBlockchainToggles.toMutableMap()
|
||||
|
||||
localTogglesStorage.store(blockchainTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return blockchainTogglesMap
|
||||
.filterKeys { it.isNotEmpty() }
|
||||
.toTableString(tableName = this@DevExcludedBlockchainsManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileBlockchainToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* [ExcludedBlockchainsManager] implementation in PROD build
|
||||
*
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdExcludedBlockchainsManager(
|
||||
private val versionProvider: VersionProvider,
|
||||
) : ExcludedBlockchainsManager {
|
||||
|
||||
override val excludedBlockchainsIds: Set<String> = getBlockchainToggles()
|
||||
|
||||
private fun getBlockchainToggles(): Set<String> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return ExcludedBlockchainToggles.values
|
||||
.defineTogglesAvailability(appVersion = appVersion)
|
||||
.filterValues { !it }
|
||||
.keys
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,10 @@ package com.tangem.core.configtoggle.di
|
|||
import android.content.Context
|
||||
import com.tangem.core.configtoggle.BuildConfig
|
||||
import com.tangem.core.configtoggle.blockchain.ExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.MutableExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DefaultExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.DevExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.blockchain.impl.ProdExcludedBlockchainsManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -24,26 +23,20 @@ internal object ExcludedBlockchainsManagerModule {
|
|||
@Singleton
|
||||
fun provideExcludedBlockchainsManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): ExcludedBlockchainsManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return DefaultExcludedBlockchainsManager(
|
||||
localTogglesStorage,
|
||||
appPreferencesStore,
|
||||
versionProvider,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideMutableExcludedBlockchainsManager(
|
||||
manager: ExcludedBlockchainsManager,
|
||||
): MutableExcludedBlockchainsManager? {
|
||||
if (!BuildConfig.TESTER_MENU_ENABLED) return null
|
||||
|
||||
return manager as MutableExcludedBlockchainsManager
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevExcludedBlockchainsManager(
|
||||
versionProvider = versionProvider,
|
||||
localTogglesStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.EXCLUDED_BLOCKCHAINS_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdExcludedBlockchainsManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,14 +7,12 @@ import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
|||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.DefaultVersionProvider
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -25,29 +23,20 @@ internal object FeatureTogglesManagerModule {
|
|||
@Singleton
|
||||
fun provideFeatureTogglesManager(
|
||||
@ApplicationContext context: Context,
|
||||
assetLoader: AssetLoader,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
): FeatureTogglesManager {
|
||||
val localTogglesStorage = LocalTogglesStorage(assetLoader)
|
||||
val versionProvider = DefaultVersionProvider(context)
|
||||
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
versionProvider = versionProvider,
|
||||
featureTogglesLocalStorage = LocalTogglesStorage(
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
preferencesKey = LocalTogglesStorage.FEATURE_TOGGLES_KEY,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
ProdFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
}.also {
|
||||
// We need to initialize during the hilt graph creation
|
||||
// in order to provide the feature toggles correctly to other dependencies.
|
||||
runBlocking {
|
||||
it.init()
|
||||
}
|
||||
ProdFeatureTogglesManager(versionProvider = versionProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,6 @@ package com.tangem.core.configtoggle.feature
|
|||
*/
|
||||
interface FeatureTogglesManager {
|
||||
|
||||
/** Initialize manager */
|
||||
suspend fun init()
|
||||
|
||||
/** Check feature toggle availability by name [name] */
|
||||
fun isFeatureEnabled(name: String): Boolean
|
||||
}
|
||||
|
|
@ -1,77 +1,68 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.MutableFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.utils.toTableString
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.storeObject
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.properties.Delegates
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in DEV build
|
||||
* Feature toggles manager implementation in dev or mocked build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property appPreferencesStore application local store
|
||||
* @property versionProvider application version provider
|
||||
* @property featureTogglesLocalStorage local storage for feature toggles
|
||||
*/
|
||||
internal class DevFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val versionProvider: VersionProvider,
|
||||
private val featureTogglesLocalStorage: LocalTogglesStorage,
|
||||
) : MutableFeatureTogglesManager {
|
||||
|
||||
private var featureTogglesMap: MutableMap<String, Boolean>? = null
|
||||
private var localFeatureTogglesMap: Map<String, Boolean>? = null
|
||||
private var fileFeatureTogglesMap: Map<String, Boolean> = getFileFeatureToggles()
|
||||
private var featureTogglesMap: MutableMap<String, Boolean> by Delegates.notNull()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureTogglesMap != null && localFeatureTogglesMap != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
init {
|
||||
val savedFeatureToggles = runBlocking { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
val savedFeatureToggles = appPreferencesStore.getObjectSyncOrNull<Map<String, Boolean>>(
|
||||
key = PreferencesKeys.FEATURE_TOGGLES_KEY,
|
||||
) ?: emptyMap<String, Boolean>()
|
||||
|
||||
val localFeatureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get().orEmpty())
|
||||
|
||||
localFeatureTogglesMap = localFeatureToggles
|
||||
|
||||
featureTogglesMap = localFeatureToggles
|
||||
featureTogglesMap = fileFeatureTogglesMap
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureToggles[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
.toMutableMap()
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap!![name] ?: false
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureTogglesMap[name] == true
|
||||
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == localFeatureTogglesMap
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap
|
||||
|
||||
override fun getFeatureToggles(): Map<String, Boolean> = featureTogglesMap!!
|
||||
override fun isMatchLocalConfig(): Boolean = featureTogglesMap == fileFeatureTogglesMap
|
||||
|
||||
override suspend fun changeToggle(name: String, isEnabled: Boolean) {
|
||||
featureTogglesMap!![name] ?: return
|
||||
featureTogglesMap!![name] = isEnabled
|
||||
appPreferencesStore.storeFeatureToggles(value = featureTogglesMap!!)
|
||||
featureTogglesMap[name] ?: return
|
||||
featureTogglesMap[name] = isEnabled
|
||||
featureTogglesLocalStorage.store(value = featureTogglesMap)
|
||||
}
|
||||
|
||||
override suspend fun recoverLocalConfig() {
|
||||
featureTogglesMap = localFeatureTogglesMap!!.toMutableMap()
|
||||
appPreferencesStore.storeFeatureToggles(value = localFeatureTogglesMap!!)
|
||||
featureTogglesMap = fileFeatureTogglesMap.toMutableMap()
|
||||
featureTogglesLocalStorage.store(value = fileFeatureTogglesMap)
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return featureTogglesMap.toTableString(tableName = this@DevFeatureTogglesManager::class.java.simpleName)
|
||||
}
|
||||
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setFeatureToggles(map: MutableMap<String, Boolean>) {
|
||||
featureTogglesMap = map
|
||||
}
|
||||
|
||||
private suspend fun AppPreferencesStore.storeFeatureToggles(value: Map<String, Boolean>) {
|
||||
storeObject(PreferencesKeys.FEATURE_TOGGLES_KEY, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,30 @@
|
|||
package com.tangem.core.configtoggle.feature.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.utils.defineTogglesAvailability
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
|
||||
/**
|
||||
* Feature toggles manager implementation in PROD build
|
||||
*
|
||||
* @property localTogglesStorage local feature toggles storage
|
||||
* @property versionProvider application version provider
|
||||
* @property versionProvider application version provider
|
||||
*/
|
||||
internal class ProdFeatureTogglesManager(
|
||||
private val localTogglesStorage: TogglesStorage,
|
||||
private val versionProvider: VersionProvider,
|
||||
) : FeatureTogglesManager {
|
||||
|
||||
private var featureToggles: Map<String, Boolean>? = null
|
||||
private val featureToggles: Map<String, Boolean> = getFileFeatureToggles()
|
||||
|
||||
override suspend fun init() {
|
||||
if (featureToggles != null) {
|
||||
return // Already initialized
|
||||
}
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles[name] == true
|
||||
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
featureToggles = localTogglesStorage.toggles
|
||||
.associateToggles(currentVersion = versionProvider.get() ?: "")
|
||||
private fun getFileFeatureToggles(): Map<String, Boolean> {
|
||||
val appVersion = versionProvider.get()
|
||||
|
||||
return FeatureToggles.values.defineTogglesAvailability(appVersion = appVersion)
|
||||
}
|
||||
|
||||
override fun isFeatureEnabled(name: String): Boolean = featureToggles!![name] ?: false
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun getProdFeatureToggles() = featureToggles!!
|
||||
|
||||
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
||||
fun setProdFeatureToggles(map: Map<String, Boolean>) {
|
||||
featureToggles = map
|
||||
}
|
||||
fun getProdFeatureToggles() = featureToggles
|
||||
}
|
||||
|
|
@ -1,24 +1,34 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import kotlin.properties.Delegates
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.datasource.local.preferences.utils.storeObjectMap
|
||||
|
||||
/**
|
||||
* Storage implementation for storing local feature toggles.
|
||||
* Feature toggles are declared in file [LOCAL_CONFIG_PATH].
|
||||
* Local storage for toggles
|
||||
*
|
||||
* @property assetLoader asset loader
|
||||
* @property appPreferencesStore app preferences store
|
||||
* @property preferencesKey preferences key
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class LocalTogglesStorage(
|
||||
private val assetLoader: AssetLoader,
|
||||
) : TogglesStorage {
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val preferencesKey: Preferences.Key<String>,
|
||||
) {
|
||||
|
||||
override var toggles: List<ConfigToggle> by Delegates.notNull()
|
||||
private set
|
||||
suspend fun getSyncOrEmpty(): Map<String, Boolean> {
|
||||
return appPreferencesStore.getObjectMapSync<Boolean>(key = preferencesKey)
|
||||
}
|
||||
|
||||
override suspend fun populate(path: String) {
|
||||
toggles = assetLoader.loadList<ConfigToggle>(path)
|
||||
suspend fun store(value: Map<String, Boolean>) {
|
||||
appPreferencesStore.storeObjectMap(key = preferencesKey, value = value)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.version.VersionAvailabilityContract
|
||||
|
||||
internal fun List<ConfigToggle>.associateToggles(currentVersion: String): Map<String, Boolean> {
|
||||
return associate { localToggle ->
|
||||
Pair(
|
||||
first = localToggle.name,
|
||||
second = VersionAvailabilityContract(currentVersion, localToggle.version),
|
||||
)
|
||||
internal fun Map<String, String>.defineTogglesAvailability(appVersion: String?): Map<String, Boolean> {
|
||||
return if (appVersion == null) {
|
||||
mapValues { false }
|
||||
} else {
|
||||
mapValues { (_, version) ->
|
||||
VersionAvailabilityContract(currentVersion = appVersion, localVersion = version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package com.tangem.core.configtoggle.utils
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal fun Map<String, Boolean>.toTableString(tableName: String): String {
|
||||
return buildString {
|
||||
append("$tableName:\n")
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", "name", "isEnabled"))
|
||||
append("|------------------------------------------|-----------|\n")
|
||||
entries.forEachIndexed { index, (name, isEnabled) ->
|
||||
append(String.format(Locale.getDefault(), "| %-40s | %-9s |\n", name, isEnabled))
|
||||
}
|
||||
append("|------------------------------------------|-----------|")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val localTogglesStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedToggles = mapOf("CHAIN_1" to false, "CHAIN_2" to true)
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns savedToggles
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
// Act
|
||||
val actual = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_2")
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevExcludedBlockchainsManager(versionProvider, localTogglesStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
localTogglesStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ExcludeBlockchain {
|
||||
|
||||
@Test
|
||||
fun excludeBlockchain_changesStatusAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).contains("CHAIN_1")
|
||||
coVerify { localTogglesStorage.store(match { it["CHAIN_1"] == false }) }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsTrueIfMatchesFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isMatchLocalConfig_returnsFalseIfDiffersFromFile() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.excludeBlockchain("CHAIN_1", false)
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig_resetsToFileAndSaves() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val toggles = mapOf("CHAIN_1" to "2.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
|
||||
coEvery { localTogglesStorage.getSyncOrEmpty() } returns mapOf("CHAIN_1" to true)
|
||||
|
||||
val manager = DevExcludedBlockchainsManager(versionProvider, localTogglesStorage)
|
||||
manager.recoverLocalConfig()
|
||||
|
||||
// Act
|
||||
val actual = manager.excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).containsExactly("CHAIN_1", "CHAIN_2")
|
||||
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
clearMocks(versionProvider, localTogglesStorage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.tangem.core.configtoggle.blockchain.impl
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.ExcludedBlockchainToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdExcludedBlockchainsManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val toggles = mapOf("CHAIN_1" to "1.0.0", "CHAIN_2" to "2.0.0")
|
||||
mockkObject(ExcludedBlockchainToggles)
|
||||
every { ExcludedBlockchainToggles.values } returns toggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(ExcludedBlockchainToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize excluded blockchains`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `all blockchains excluded if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns ""
|
||||
|
||||
// Act
|
||||
val actual = ProdExcludedBlockchainsManager(versionProvider).excludedBlockchainsIds
|
||||
|
||||
// Assert
|
||||
val expected = setOf("CHAIN_1", "CHAIN_2")
|
||||
Truth.assertThat(actual).containsExactlyElementsIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdExcludedBlockchainsManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,489 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.manager.ProdFeatureTogglesManagerTest.IsFeatureEnabledModel
|
||||
import com.tangem.core.configtoggle.storage.LocalTogglesStorage
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DevFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val featureTogglesLocalStorage = mockk<LocalTogglesStorage>(relaxUnitFun = true)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
val savedFeatureToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
coVerify(inverse = true) { featureTogglesLocalStorage.getSyncOrEmpty() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns empty map`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = emptyMap<String, Boolean>()
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize manager if storage returns unknown toggles`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val savedFeatureToggles = mapOf("TOGGLE_3" to true)
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
// Act
|
||||
val actual = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize manager if storage throws exception`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
val exception = Exception("Test exception")
|
||||
|
||||
every { versionProvider.get() } returns appVersion
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage) }
|
||||
.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: DevFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsMatchLocalConfig {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isMatchLocalConfig(model: IsMatchLocalConfigModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.fileFeatureToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage).apply {
|
||||
setFeatureToggles(model.storedFeatureToggles.toMutableMap())
|
||||
}
|
||||
|
||||
// Act
|
||||
val actual = manager.isMatchLocalConfig()
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0"),
|
||||
storedFeatureToggles = mapOf("TOGGLE_3" to true, "TOGGLE_4" to false),
|
||||
expected = false,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = emptyMap(),
|
||||
expected = true,
|
||||
),
|
||||
IsMatchLocalConfigModel(
|
||||
fileFeatureToggles = emptyMap(),
|
||||
storedFeatureToggles = mapOf("TOGGLE_1" to true),
|
||||
expected = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsMatchLocalConfigModel(
|
||||
val fileFeatureToggles: Map<String, String>,
|
||||
val storedFeatureToggles: Map<String, Boolean>,
|
||||
val expected: Boolean,
|
||||
)
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class GetFeatureToggles {
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = savedFeatureToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class ChangeToggle {
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun changeToggle(model: ChangeToggleModel) = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns model.initialToggles
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns emptyMap()
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.changeToggle(name = model.name, isEnabled = model.isEnabled)
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = model.expectedToggles
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
|
||||
if (model.expectedStoreSaving) {
|
||||
featureTogglesLocalStorage.store(model.expectedToggles)
|
||||
}
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = false,
|
||||
expectedToggles = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_2",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to true),
|
||||
expectedStoreSaving = true,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "undefined"),
|
||||
name = "TOGGLE_3",
|
||||
isEnabled = true,
|
||||
expectedToggles = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
ChangeToggleModel(
|
||||
initialToggles = emptyMap(),
|
||||
name = "TOGGLE_1",
|
||||
isEnabled = true,
|
||||
expectedToggles = emptyMap(),
|
||||
expectedStoreSaving = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class RecoverLocalConfig {
|
||||
|
||||
@Test
|
||||
fun recoverLocalConfig() = runTest {
|
||||
// Arrange
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val fileFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns fileFeatureToggles
|
||||
|
||||
val savedFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
coEvery { featureTogglesLocalStorage.getSyncOrEmpty() } returns savedFeatureToggles
|
||||
|
||||
val manager = DevFeatureTogglesManager(versionProvider, featureTogglesLocalStorage)
|
||||
|
||||
// Act
|
||||
manager.recoverLocalConfig()
|
||||
val actual = manager.getFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder {
|
||||
versionProvider.get()
|
||||
featureTogglesLocalStorage.getSyncOrEmpty()
|
||||
featureTogglesLocalStorage.store(expected)
|
||||
}
|
||||
|
||||
clearMocks(versionProvider, featureTogglesLocalStorage)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
}
|
||||
|
||||
data class ChangeToggleModel(
|
||||
val initialToggles: Map<String, String>,
|
||||
val name: String,
|
||||
val isEnabled: Boolean,
|
||||
val expectedToggles: Map<String, Boolean>,
|
||||
val expectedStoreSaving: Boolean,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.core.configtoggle.feature.impl.DevFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
|
||||
import com.tangem.datasource.local.preferences.utils.getSyncOrNull
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import kotlin.collections.set
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class DevTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val appPreferenceStore = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = mockk(relaxed = true),
|
||||
)
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = DevFeatureTogglesManager(
|
||||
localTogglesStorage = localTogglesStorage,
|
||||
appPreferencesStore = appPreferenceStore,
|
||||
versionProvider = versionProvider,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns savedFeatureTogglesMap
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs kept empty list`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns emptyMap()
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if shared prefs didn't keep feature toggles`() = runTest {
|
||||
val currentVersion = "0.1.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery {
|
||||
appPreferenceStore.getObjectSyncOrNull<Map<String, Boolean>>(PreferencesKeys.FEATURE_TOGGLES_KEY)
|
||||
} returns null
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion)
|
||||
.mapValues(Map.Entry<String, Boolean>::value)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
coEvery { appPreferenceStore.getSyncOrNull(PreferencesKeys.FEATURE_TOGGLES_KEY) } returns savedFeatureToggles
|
||||
coEvery { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
coEvery { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles
|
||||
.associateToggles(currentVersion = "")
|
||||
.mapValues { resultToggle ->
|
||||
savedFeatureTogglesMap[resultToggle.key] ?: resultToggle.value
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getFeatureToggles() {
|
||||
val expected = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(expected)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that contains in map`() = runTest {
|
||||
val changeableToggleName = "INACTIVE_TEST_FEATURE_ENABLED"
|
||||
val resultMap = mutableMapOf(
|
||||
changeableToggleName to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle(changeableToggleName, true)
|
||||
|
||||
resultMap[changeableToggleName] = true
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change toggle that doesn't contains in map`() = runTest {
|
||||
val resultMap = mutableMapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
manager.setFeatureToggles(resultMap)
|
||||
|
||||
manager.changeToggle("FEATURE_TOGGLE", true)
|
||||
|
||||
Truth.assertThat(manager.getFeatureToggles()).containsExactlyEntriesIn(resultMap)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
val savedFeatureToggles = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val savedFeatureTogglesMap = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.utils.ProvideTestModels
|
||||
import com.tangem.core.configtoggle.FeatureToggles
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.*
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class ProdFeatureTogglesManagerTest {
|
||||
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class Initialization {
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
val featureToggles = mapOf("TOGGLE_1" to "1.0.0", "TOGGLE_2" to "2.0.0")
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun tearDownEach() {
|
||||
clearMocks(versionProvider)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = "1.0.0"
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to true, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = null
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns empty string`() = runTest {
|
||||
// Arrange
|
||||
val appVersion = ""
|
||||
every { versionProvider.get() } returns appVersion
|
||||
|
||||
// Act
|
||||
val actual = ProdFeatureTogglesManager(versionProvider).getProdFeatureToggles()
|
||||
|
||||
// Assert
|
||||
val expected = mapOf("TOGGLE_1" to false, "TOGGLE_2" to false)
|
||||
Truth.assertThat(actual).containsExactlyEntriesIn(expected)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
// Arrange
|
||||
val exception = Exception("Test exception")
|
||||
every { versionProvider.get() } throws exception
|
||||
|
||||
// Act
|
||||
val actual = runCatching { ProdFeatureTogglesManager(versionProvider) }.exceptionOrNull()!!
|
||||
|
||||
// Assert
|
||||
Truth.assertThat(actual).isInstanceOf(exception::class.java)
|
||||
Truth.assertThat(actual).hasMessageThat().isEqualTo(exception.message)
|
||||
|
||||
coVerifyOrder { versionProvider.get() }
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
inner class IsFeatureEnabled {
|
||||
|
||||
private lateinit var manager: ProdFeatureTogglesManager
|
||||
|
||||
@BeforeAll
|
||||
fun setupAll() {
|
||||
every { versionProvider.get() } returns "1.0.0"
|
||||
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to "undefined",
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to "1.0.0",
|
||||
)
|
||||
|
||||
mockkObject(FeatureToggles)
|
||||
every { FeatureToggles.values } returns featureToggles
|
||||
|
||||
manager = ProdFeatureTogglesManager(versionProvider)
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun tearDownAll() {
|
||||
clearMocks(versionProvider)
|
||||
unmockkObject(FeatureToggles)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun isFeatureEnabled(model: IsFeatureEnabledModel) {
|
||||
// Act
|
||||
val actual = manager.isFeatureEnabled(name = model.name)
|
||||
|
||||
// Assert
|
||||
val expected = model.expected
|
||||
Truth.assertThat(actual).isEqualTo(expected)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(
|
||||
IsFeatureEnabledModel(name = "ACTIVE2_TEST_FEATURE_ENABLED", expected = true),
|
||||
IsFeatureEnabledModel(name = "INACTIVE_TEST_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "UNKNOWN_FEATURE_ENABLED", expected = false),
|
||||
IsFeatureEnabledModel(name = "", expected = false),
|
||||
)
|
||||
}
|
||||
|
||||
data class IsFeatureEnabledModel(val name: String, val expected: Boolean)
|
||||
}
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
package com.tangem.core.configtoggle.manager
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.core.configtoggle.feature.impl.ProdFeatureTogglesManager
|
||||
import com.tangem.core.configtoggle.storage.ConfigToggle
|
||||
import com.tangem.core.configtoggle.storage.TogglesStorage
|
||||
import com.tangem.core.configtoggle.utils.associateToggles
|
||||
import com.tangem.core.configtoggle.version.VersionProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class ProdTogglesManagerTest {
|
||||
|
||||
private val localTogglesStorage = mockk<TogglesStorage>()
|
||||
private val versionProvider = mockk<VersionProvider>()
|
||||
private val manager = ProdFeatureTogglesManager(localTogglesStorage, versionProvider)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
val currentVersion = "1.0.0"
|
||||
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns currentVersion
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
val expected = localFeatureToggles.associateToggles(currentVersion)
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage if versionProvider returns null`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } returns null
|
||||
|
||||
manager.init()
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
|
||||
Truth.assertThat(manager.getProdFeatureToggles()).containsExactlyEntriesIn(disabledFeatureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if localFeatureTogglesStorage throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } throws IllegalStateException(
|
||||
"Property featureToggles should be initialized before get.",
|
||||
)
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("localFeatureToggles shouldn't be initialized") }
|
||||
.onFailure {
|
||||
Truth
|
||||
.assertThat(it)
|
||||
.hasMessageThat()
|
||||
.contains("Property featureToggles should be initialized before get.")
|
||||
|
||||
Truth.assertThat(it).isInstanceOf(IllegalStateException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) }
|
||||
verifyAll(inverse = true) { versionProvider.get() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if versionProvider throws exception`() = runTest {
|
||||
coEvery { localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH) } just Runs
|
||||
every { localTogglesStorage.toggles } returns localFeatureToggles
|
||||
every { versionProvider.get() } throws PackageManager.NameNotFoundException()
|
||||
|
||||
runCatching { manager.init() }
|
||||
.onSuccess { throw IllegalStateException("versionProvider should throws exception") }
|
||||
.onFailure {
|
||||
Truth.assertThat(it).isInstanceOf(PackageManager.NameNotFoundException::class.java)
|
||||
}
|
||||
|
||||
coVerifyOrder {
|
||||
localTogglesStorage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
versionProvider.get()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to true,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "INACTIVE_TEST_FEATURE_ENABLED")
|
||||
|
||||
Truth.assertThat(actual).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get feature availability if feature toggle doesn't exists`() {
|
||||
val featureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to true,
|
||||
)
|
||||
manager.setProdFeatureToggles(featureToggles)
|
||||
|
||||
val actual = manager.isFeatureEnabled(name = "")
|
||||
|
||||
Truth.assertThat(actual).isFalse()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val localFeatureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
|
||||
val disabledFeatureToggles = mapOf(
|
||||
"INACTIVE_TEST_FEATURE_ENABLED" to false,
|
||||
"ACTIVE2_TEST_FEATURE_ENABLED" to false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
package com.tangem.core.configtoggle.storage
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.google.common.truth.Truth
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import com.tangem.core.configtoggle.feature.impl.FeatureTogglesConstants
|
||||
import com.tangem.datasource.asset.loader.AssetLoader
|
||||
import com.tangem.datasource.asset.reader.AssetReader
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.*
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
internal class LocalTogglesStorageTest {
|
||||
|
||||
private val assetReader = mockk<AssetReader>()
|
||||
private val moshi = mockk<Moshi>()
|
||||
private val jsonAdapter = mockk<JsonAdapter<List<ConfigToggle>>>()
|
||||
|
||||
// Impossible to mockk AssetLoader because it implement inline functions
|
||||
private val assetLoader = AssetLoader(
|
||||
assetReader = assetReader,
|
||||
moshi = moshi,
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
)
|
||||
|
||||
private val storage = LocalTogglesStorage(assetLoader)
|
||||
|
||||
@Test
|
||||
fun `successfully initialize storage`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() returns featureToggles
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(featureToggles)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if assetReader throws exception`() = runTest {
|
||||
everyReadingJson() returns json
|
||||
everyCreatingMoshiAdapter() returns jsonAdapter
|
||||
everyMappingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder {
|
||||
assetReader.read(CONFIG_FILE_NAME)
|
||||
jsonAdapter.fromJson(json)
|
||||
}
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `failure initialize storage if jsonAdapter throws exception`() = runTest {
|
||||
everyReadingJson() throws IOException()
|
||||
|
||||
storage.populate(FeatureTogglesConstants.LOCAL_CONFIG_PATH)
|
||||
|
||||
coVerifyOrder { assetReader.read(CONFIG_FILE_NAME) }
|
||||
verifyAll(inverse = true) { jsonAdapter.fromJson(any<String>()) }
|
||||
|
||||
Truth.assertThat(storage.toggles).containsExactlyElementsIn(emptyList<ConfigToggle>())
|
||||
}
|
||||
|
||||
private fun everyReadingJson() = coEvery { assetReader.read(CONFIG_FILE_NAME) }
|
||||
|
||||
private fun everyCreatingMoshiAdapter() = every {
|
||||
val types = Types.newParameterizedType(List::class.java, ConfigToggle::class.java)
|
||||
moshi.adapter<List<ConfigToggle>>(types)
|
||||
}
|
||||
|
||||
private fun everyMappingJson() = every { jsonAdapter.fromJson(json) }
|
||||
|
||||
private companion object {
|
||||
const val CONFIG_FILE_NAME = "configs/feature_toggles_config.json"
|
||||
|
||||
val json = """
|
||||
[
|
||||
{
|
||||
"name": "INACTIVE_TEST_FEATURE_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "ACTIVE2_TEST_FEATURE_ENABLED",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
|
||||
val featureToggles = listOf(
|
||||
ConfigToggle(name = "INACTIVE_TEST_FEATURE_ENABLED", version = "undefined"),
|
||||
ConfigToggle(name = "ACTIVE2_TEST_FEATURE_ENABLED", version = "1.0.0"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -103,4 +103,7 @@ interface TangemPayApi {
|
|||
|
||||
@GET("v1/customer/kyc")
|
||||
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
|
||||
|
||||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerMeResponse(
|
||||
@Json(name = "result") val result: Result?,
|
||||
@Json(name = "error") val error: String?,
|
||||
) {
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Result(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "state") val state: String,
|
||||
@Json(name = "createdAt") val createdAt: String,
|
||||
@Json(name = "product_instance") val productInstance: ProductInstance,
|
||||
@Json(name = "payment_account") val paymentAccount: PaymentAccount,
|
||||
@Json(name = "kyc") val kyc: Kyc,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ProductInstance(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "cid") val cid: String,
|
||||
@Json(name = "card_id") val cardId: String,
|
||||
@Json(name = "card_wallet_address") val cardWalletAddress: String,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "updated_at") val updatedAt: String,
|
||||
@Json(name = "payment_account_id") val paymentAccountId: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PaymentAccount(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "address") val address: String,
|
||||
@Json(name = "customer_wallet_address") val customerWalletAddress: String,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Kyc(
|
||||
@Json(name = "id") val id: String,
|
||||
@Json(name = "provider") val provider: String,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "risk") val risk: String,
|
||||
@Json(name = "review_answer") val reviewAnswer: String,
|
||||
@Json(name = "created_at") val createdAt: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -154,7 +154,8 @@ interface TangemTechApi {
|
|||
suspend fun saveWalletAccounts(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") ifMatch: String,
|
||||
): ApiResponse<SaveWalletAccountsResponse>
|
||||
@Body body: SaveWalletAccountsResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("/v1/wallets/{walletId}/accounts/archived")
|
||||
suspend fun getWalletArchivedAccounts(
|
||||
|
|
|
|||
|
|
@ -14,11 +14,18 @@ interface RuntimeStateStore<T> {
|
|||
/** Get flow of elements [T] */
|
||||
fun get(): StateFlow<T>
|
||||
|
||||
/** Get element [T] synchronously or null */
|
||||
suspend fun getSyncOrNull(): T?
|
||||
|
||||
/** Store [value] */
|
||||
suspend fun store(value: T)
|
||||
|
||||
/** Update current value by [function] */
|
||||
suspend fun update(function: (T) -> T)
|
||||
|
||||
/** Clear stored value */
|
||||
fun clear()
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +39,8 @@ interface RuntimeStateStore<T> {
|
|||
|
||||
override fun get(): StateFlow<T> = flow
|
||||
|
||||
override suspend fun getSyncOrNull(): T? = flow.value
|
||||
|
||||
override suspend fun store(value: T) {
|
||||
flow.value = value
|
||||
}
|
||||
|
|
@ -39,6 +48,10 @@ interface RuntimeStateStore<T> {
|
|||
override suspend fun update(function: (T) -> T) {
|
||||
flow.update(function)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
flow.value = defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -56,10 +56,6 @@ object PreferencesKeys {
|
|||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
|
||||
val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") }
|
||||
|
||||
val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") }
|
||||
|
||||
val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") }
|
||||
|
||||
val IS_TANGEM_TOS_ACCEPTED_KEY by lazy { booleanPreferencesKey(name = "tangem_tos_accepted") }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
interface TangemPayStorage {
|
||||
|
||||
suspend fun store(authHeader: String)
|
||||
|
||||
suspend fun get(): String?
|
||||
|
||||
suspend fun clear()
|
||||
}
|
||||
|
|
@ -879,7 +879,7 @@
|
|||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_unreachable_error_text">Compruebe su conexión de red</string>
|
||||
<string name="send_fee_unreachable_error_title">Información de tarifa de red no accesible</string>
|
||||
<string name="send_from_title">Tú envías</string>
|
||||
<string name="send_from_title">Envía</string>
|
||||
<string name="send_from_wallet_android">Desde</string>
|
||||
<string name="send_from_wallet_name">De %s</string>
|
||||
<string name="send_gas_limit">Límite de gas</string>
|
||||
|
|
@ -952,10 +952,13 @@
|
|||
<string name="send_with_swap_confirm_title">Intercambiar y enviar</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">¿Continuar con la conversión? Esto borrará sus datos anteriores.</string>
|
||||
<string name="send_with_swap_convert_token_alert_title">Confirmar Conversión</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_message">El envío de cualquier otra moneda supondrá su pérdida irreversible.</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_title">Seleccione la red de destino correcta</string>
|
||||
<string name="send_with_swap_notification_text">Envía cualquier token y lo convertiremos en el camino. Su destinatario obtiene exactamente lo que necesita, sin problemas.</string>
|
||||
<string name="send_with_swap_recipient_amount_success_title">El destinatario recibirá</string>
|
||||
<string name="send_with_swap_recipient_amount_text">Al destinatario</string>
|
||||
<string name="send_with_swap_recipient_amount_title">Cantidad a recibir</string>
|
||||
<string name="send_with_swap_recipient_get_amount">El destinatario recibe %s</string>
|
||||
<string name="send_with_swap_remove_convert_alert_message">¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores.</string>
|
||||
<string name="send_with_swap_title">Enviar con swap</string>
|
||||
<string name="sent_transaction_sent_title">Transacción enviada</string>
|
||||
|
|
|
|||
|
|
@ -1043,13 +1043,13 @@
|
|||
<string name="send_with_swap_change_token_alert_message">受信トークンを変更してもよろしいですか? 変更すると、以前入力したデータがリセットされます。</string>
|
||||
<string name="send_with_swap_change_token_alert_title">トークンの変更</string>
|
||||
<string name="send_with_swap_confirm_title">スワップして送信</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">変換を続行しますか? これにより以前のデータは消去されます。</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">スワップを実行しますか?これにより以前のデータは消去されます。</string>
|
||||
<string name="send_with_swap_convert_token_alert_title">変換を確定</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_message">その他の通貨を送信すると、取り返しのつかない損失が発生します。</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_title">正しい受信者ネットワークを選択してください</string>
|
||||
<string name="send_with_swap_notification_text">受け取るトークンを自由に選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。</string>
|
||||
<string name="send_with_swap_recipient_amount_success_title">受信者は受け取ります</string>
|
||||
<string name="send_with_swap_recipient_amount_text">受取人へ</string>
|
||||
<string name="send_with_swap_notification_text">受け取るトークンを選んでください。受信者は、あなたが選択したトークンをシームレスに受け取ります。</string>
|
||||
<string name="send_with_swap_recipient_amount_success_title">受信者が受け取ります</string>
|
||||
<string name="send_with_swap_recipient_amount_text">受信者へ</string>
|
||||
<string name="send_with_swap_recipient_amount_title">受取金額</string>
|
||||
<string name="send_with_swap_recipient_get_amount">受信者は%sを取得します</string>
|
||||
<string name="send_with_swap_remove_convert_alert_message">変換をキャンセルしてもよろしいですか?以前のデータは消去されます。</string>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="account_details_archive_action">Архив</string>
|
||||
<string name="account_details_archive_description">Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно</string>
|
||||
<string name="account_form_placeholder_edit_account">Аккаунт</string>
|
||||
<string name="account_form_placeholder_new_account">Новый аккаунт</string>
|
||||
<string name="account_unsaved_dialog_action_first">Продолжить редактирование</string>
|
||||
<string name="account_unsaved_dialog_title">Несохраненные изменения</string>
|
||||
<string name="action_buttons_buy_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки.</string>
|
||||
<string name="action_buttons_sell_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи.</string>
|
||||
<string name="action_buttons_sell_navigation_bar_title">Продать</string>
|
||||
|
|
@ -310,6 +316,7 @@
|
|||
<string name="domain_receive_assets_navigation_title">Получить активы</string>
|
||||
<string name="domain_receive_assets_network_name_address">%s адрес</string>
|
||||
<string name="domain_receive_assets_onboarding_description">Отправка средств в другой сети может повлечь потерю средств.</string>
|
||||
<string name="domain_receive_assets_onboarding_network_name">%s сети</string>
|
||||
<string name="domain_receive_assets_onboarding_title">Отправляйте средства, используя только</string>
|
||||
<string name="email_preface_wc_error">Привет, команда поддержки, у меня возникла ошибка с кодом: %s</string>
|
||||
<string name="email_subject_wc_error">Ошибка WalletConnect</string>
|
||||
|
|
@ -411,7 +418,16 @@
|
|||
<string name="home_button_scan">Сканировать</string>
|
||||
<string name="hot_crypto_add_token_subtitle">в %s</string>
|
||||
<string name="hot_crypto_token_network">В сети %s</string>
|
||||
<string name="hw_access_code_create_alert_title">Вы уверены, что хотите прервать процесс создания кода доступа?</string>
|
||||
<string name="hw_backup_hardware_title">Аппаратный кошелёк</string>
|
||||
<string name="hw_create_seed_description">Будьте в курсе новых функций и новостей</string>
|
||||
<string name="hw_upgrade_error_card_already_has_wallet">Это устройство не может быть использовано для апгрейда, оно уже содержит другой кошелек.</string>
|
||||
<string name="hw_upgrade_error_wallet2_card_required">Во время операции произошла ошибка.</string>
|
||||
<string name="hw_upgrade_key_migration_description">Приватные ключи будут перемещены из приложения в вашу Tangem карту или кольцо</string>
|
||||
<string name="hw_upgrade_key_migration_title">Миграция ключей</string>
|
||||
<string name="hw_upgrade_start_action">Начать апгрейд</string>
|
||||
<string name="hw_upgrade_start_title">Tangem кошелек</string>
|
||||
<string name="hw_upgrade_to_cold_banner_title">Сделать апгрейд кошелька до аппаратной версии.</string>
|
||||
<string name="information_generated_with_ai">Эта информация была сгенерирована ИИ.\nНажмите здесь, если обнаружили ошибку.</string>
|
||||
<string name="initial_message_change_access_code_body">Чтобы изменить код доступа, приложите карту или кольцо как показано выше и не убирайте до окончания операции</string>
|
||||
<string name="initial_message_change_passcode_body">Чтобы изменить пароль, приложите карту как показано выше и не убирайте до окончания операции</string>
|
||||
|
|
@ -929,7 +945,7 @@
|
|||
<string name="send_with_swap_change_token_alert_message">Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные.</string>
|
||||
<string name="send_with_swap_change_token_alert_title">Изменение токена</string>
|
||||
<string name="send_with_swap_confirm_title">Обмен и отправка</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">Продолжить с конвертацией? Это действие удалит предыдущие данные</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">Продолжить с обменом? Это действие удалит предыдущие данные</string>
|
||||
<string name="send_with_swap_convert_token_alert_title">Подтвердить конвертацию</string>
|
||||
<string name="send_with_swap_notification_text">Выберите любой токен к получению. Ваш адресат получит ровно то, что вы выбрали — без лишних сложностей.</string>
|
||||
<string name="send_with_swap_recipient_amount_success_title">Будет получено</string>
|
||||
|
|
@ -1101,6 +1117,7 @@
|
|||
<string name="swapping_fee_estimation_error_text">Ошибка расчета комиссии. Пожалуйста, отправьте информацию в поддержку.</string>
|
||||
<string name="swapping_from_title">Вы отправляете</string>
|
||||
<string name="swapping_high_price_impact_description">Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму.</string>
|
||||
<string name="swapping_high_price_impact_title">Высокое влияние на цену</string>
|
||||
<string name="swapping_insufficient_funds">Недостаточно средств</string>
|
||||
<string name="swapping_permission_header">Дать разрешение</string>
|
||||
<string name="swapping_swap_action">Обменять</string>
|
||||
|
|
@ -1229,9 +1246,11 @@
|
|||
<string name="wallet_promo_banner_button_title">Получить с 10% скидкой</string>
|
||||
<string name="wallet_promo_banner_description">Получите доступ к более чем 13 000 криптовалют. Покупайте, продавайте, обменивайте и стейкайте в один клик. Свяжите до трех карт для резервного копирования. </string>
|
||||
<string name="wallet_promo_banner_title">Откройте Tangem Wallet</string>
|
||||
<string name="wallet_settings_change_access_code_title">Изменить Код Доступа</string>
|
||||
<string name="wallet_settings_push_notifications_description">Получайте уведомления о входящих транзакциях в кошельке и обновлениях Tangem.</string>
|
||||
<string name="wallet_settings_push_notifications_huawei_warning">В настоящий момент push-уведомления могут не работать на устройствах Huawei. Мы уже работаем над решением этой проблемы и планируем исправление в ближайших обновлениях. Спасибо за понимание!</string>
|
||||
<string name="wallet_settings_push_notifications_title">Уведомления о транзакциях</string>
|
||||
<string name="wallet_settings_set_access_code_title">Установить Код Доступа</string>
|
||||
<string name="wallet_settings_title">Настройки кошелька</string>
|
||||
<string name="wallet_title">Tangem</string>
|
||||
<string name="warning_access_denied_message">Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку</string>
|
||||
|
|
|
|||
|
|
@ -1368,6 +1368,7 @@
|
|||
<string name="wc_errors_proposal_expired_subtitle">Будь ласка, згенеруйте новий URI та спробуйте ще раз</string>
|
||||
<string name="wc_errors_proposal_expired_title">Термін для з’єднання минув</string>
|
||||
<string name="wc_estimated_wallet_changes">Прогнозовані зміни</string>
|
||||
<string name="wc_fee_suggested">Пропоновано %s</string>
|
||||
<string name="wc_insufficient_warning_subtitle">Поповніть баланс, щоб покрити комісію мережі</string>
|
||||
<string name="wc_insufficient_warning_title">Недостатньо %1$s</string>
|
||||
<string name="wc_missing_required_network_description">Додайте %s мережі до вашого портфелю для цього гаманця</string>
|
||||
|
|
|
|||
|
|
@ -1061,7 +1061,7 @@
|
|||
<string name="send_with_swap_change_token_alert_message">Are you sure you want to change the receiving token? This will reset your previously entered data.</string>
|
||||
<string name="send_with_swap_change_token_alert_title">Changing token</string>
|
||||
<string name="send_with_swap_confirm_title">Swap and send</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">Proceed with conversion? This will clear your previous data.</string>
|
||||
<string name="send_with_swap_convert_token_alert_message">Proceed with swap? This will clear your previous data.</string>
|
||||
<string name="send_with_swap_convert_token_alert_title">Confirm Conversion</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_message">Sending any other currency will result in its irreversible loss.</string>
|
||||
<string name="send_with_swap_correct_recipient_network_notification_title">Select the correct recipient network</string>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ sealed class TopAppBarButtonUM(
|
|||
enabled = enabled,
|
||||
)
|
||||
|
||||
fun Close(enabled: Boolean = true, onCloseClick: () -> Unit) = Icon(
|
||||
iconRes = R.drawable.ic_close_24,
|
||||
onClicked = onCloseClick,
|
||||
enabled = enabled,
|
||||
)
|
||||
|
||||
fun Text(text: TextReference, onTextClicked: () -> Unit, enabled: Boolean = true) = Text(
|
||||
text = text,
|
||||
onClicked = onTextClicked,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
|
|
@ -20,6 +21,7 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.WalletConnectDetailsBottomSheetTestTags
|
||||
|
||||
/**
|
||||
* Title component for [TangemModalBottomSheet] with [TangemIconButton] for buttons.
|
||||
|
|
@ -54,7 +56,9 @@ fun TangemModalBottomSheetTitle(
|
|||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.TITLE),
|
||||
)
|
||||
}
|
||||
if (subtitle != null) {
|
||||
|
|
@ -62,7 +66,9 @@ fun TangemModalBottomSheetTitle(
|
|||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.DATE),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -72,7 +78,8 @@ fun TangemModalBottomSheetTitle(
|
|||
onClick = onEndClick,
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.align(Alignment.CenterEnd),
|
||||
.align(Alignment.CenterEnd)
|
||||
.testTag(WalletConnectDetailsBottomSheetTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.core.ui.components.bottomsheets.modal
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
|
|
@ -49,7 +50,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheetWi
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit = {},
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
val isAlwaysVisible = LocalBottomSheetAlwaysVisible.current
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheetW
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShown) }
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
|
||||
|
|
@ -118,7 +119,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
|
|||
skipPartiallyExpanded: Boolean = true,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable BoxScope.(T) -> Unit,
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
) {
|
||||
BasicModalBottomSheetWithFooter<T>(
|
||||
config = config,
|
||||
|
|
@ -145,7 +146,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
noinline onBack: (() -> Unit)? = null,
|
||||
crossinline title: @Composable BoxScope.(T) -> Unit,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
crossinline footer: @Composable (BoxScope.(T) -> Unit),
|
||||
noinline footer: @Composable (BoxScope.(T) -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val model = config.content as? T ?: return
|
||||
|
|
@ -156,8 +157,13 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
val scrollState = rememberScrollState(initial = initial)
|
||||
|
||||
val isKeyboardOpen by rememberIsKeyboardVisible()
|
||||
val buttonHeight = TangemTheme.dimens.spacing80
|
||||
val contentBottomPadding = TangemTheme.dimens.spacing80
|
||||
val buttonHeight by animateDpAsState(
|
||||
if (footer != null) {
|
||||
80.dp
|
||||
} else {
|
||||
0.dp
|
||||
},
|
||||
)
|
||||
// Offset calculation for keyboard scroll adjustment:
|
||||
// 1) Button height (footer)
|
||||
// 2) Column content bottom padding
|
||||
|
|
@ -202,7 +208,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(state = scrollState)
|
||||
.padding(bottom = contentBottomPadding),
|
||||
.padding(bottom = buttonHeight),
|
||||
) {
|
||||
content(model)
|
||||
}
|
||||
|
|
@ -218,7 +224,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
|
|||
.height(buttonHeight)
|
||||
.align(Alignment.BottomCenter),
|
||||
) {
|
||||
footer(model)
|
||||
if (footer != null) {
|
||||
footer(model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
|||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.utils.StringsSigns.PASSWORD_VISUAL_CHAR
|
||||
|
|
@ -159,14 +158,17 @@ private fun CellDecoration(
|
|||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
Box(
|
||||
modifier = Modifier.sizeIn(minWidth = minWidth, minHeight = minHeight),
|
||||
text = text,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 48.sp,
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = text,
|
||||
style = TangemTheme.typography.h3,
|
||||
color = color,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
package com.tangem.core.ui.components.grid
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* EnumeratedTwoColumnGrid component
|
||||
*
|
||||
* @param items component items
|
||||
* @param modifier composable modifier
|
||||
*
|
||||
*/
|
||||
@Composable
|
||||
fun EnumeratedTwoColumnGrid(items: ImmutableList<EnumeratedTwoColumnGridItem>, modifier: Modifier = Modifier) {
|
||||
VerticalGrid(
|
||||
modifier = modifier,
|
||||
items = items,
|
||||
) { item ->
|
||||
Row(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = item.mnemonic,
|
||||
style = TangemTheme.typography.button,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.width(TangemTheme.dimens.size40),
|
||||
text = "${item.index}.",
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private inline fun <T> VerticalGrid(
|
||||
items: ImmutableList<T>,
|
||||
modifier: Modifier = Modifier,
|
||||
crossinline content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val columnLength = items.size / 2
|
||||
Row(
|
||||
modifier = modifier,
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
) {
|
||||
repeat(2) { index ->
|
||||
Column {
|
||||
for (i in 0 until columnLength) {
|
||||
val item = items[index * columnLength + i]
|
||||
content(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(widthDp = 360, heightDp = 640, showBackground = true)
|
||||
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview() {
|
||||
TangemThemePreview {
|
||||
EnumeratedTwoColumnGrid(
|
||||
items = List(24) {
|
||||
EnumeratedTwoColumnGridItem(
|
||||
index = it + 1,
|
||||
mnemonic = "word${it + 1}",
|
||||
)
|
||||
}.toImmutableList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.ui.components.grid.entity
|
||||
|
||||
data class EnumeratedTwoColumnGridItem(
|
||||
val index: Int,
|
||||
val mnemonic: String,
|
||||
)
|
||||
|
|
@ -27,7 +27,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
|
||||
|
|
@ -63,8 +62,8 @@ fun Notification(
|
|||
NotificationConfig.IconTint.Unspecified -> null
|
||||
NotificationConfig.IconTint.Accent -> TangemTheme.colors.icon.accent
|
||||
NotificationConfig.IconTint.Attention -> TangemTheme.colors.icon.attention
|
||||
NotificationConfig.IconTint.Warning -> TangemTheme.colors.icon.warning
|
||||
},
|
||||
iconSize: Dp = 20.dp,
|
||||
isEnabled: Boolean = true,
|
||||
) {
|
||||
NotificationBaseContainer(
|
||||
|
|
@ -78,7 +77,7 @@ fun Notification(
|
|||
MainContent(
|
||||
iconResId = config.iconResId,
|
||||
iconTint = iconTint,
|
||||
iconSize = iconSize,
|
||||
iconSize = config.iconSize,
|
||||
title = config.title,
|
||||
titleColor = titleColor,
|
||||
subtitle = config.subtitle,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
/**
|
||||
|
|
@ -26,6 +28,7 @@ data class NotificationConfig(
|
|||
val onCloseClick: (() -> Unit)? = null,
|
||||
val showArrowIcon: Boolean = onClick != null,
|
||||
val iconTint: IconTint = IconTint.Unspecified,
|
||||
val iconSize: Dp = 20.dp,
|
||||
) {
|
||||
|
||||
sealed class ButtonsState {
|
||||
|
|
@ -62,5 +65,6 @@ data class NotificationConfig(
|
|||
Unspecified,
|
||||
Accent,
|
||||
Attention,
|
||||
Warning,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.ui.test
|
||||
|
||||
object WalletConnectBottomSheetTestTags {
|
||||
const val TITLE = "WALLET_CONNECT_BOTTOM_SHEET_TITLE"
|
||||
const val APP_ICON = "WALLET_CONNECT_BOTTOM_SHEET_APP_ICON"
|
||||
const val APP_NAME = "WALLET_CONNECT_BOTTOM_SHEET_APP_NAME"
|
||||
const val APPROVE_ICON = "WALLET_CONNECT_BOTTOM_SHEET_APPROVE_ICON"
|
||||
const val APP_URL = "WALLET_CONNECT_BOTTOM_SHEET_APP_URL"
|
||||
|
||||
const val CONNECTION_REQUEST_ICON = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_ICON"
|
||||
const val CONNECTION_REQUEST_TEXT = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_TEXT"
|
||||
const val CONNECTION_REQUEST_CHEVRON = "WALLET_CONNECT_BOTTOM_SHEET_CONNECTION_REQUEST_CHEVRON"
|
||||
|
||||
const val WALLET_ICON = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_ICON"
|
||||
const val WALLET_NAME_TITLE = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_NAME_TITLE"
|
||||
const val WALLET_NAME = "WALLET_CONNECT_BOTTOM_SHEET_WALLET_NAME"
|
||||
|
||||
const val NETWORKS_ICON = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_ICON"
|
||||
const val NETWORKS_TITLE = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_TITLE"
|
||||
const val NETWORKS_SELECTOR_ICON = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_SELECTOR_ICON"
|
||||
const val NETWORKS_ICONS = "WALLET_CONNECT_BOTTOM_SHEET_NETWORKS_ICONS"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue