Updated on 2026-08-14
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.common.extensions
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
|
||||
fun BaseTestCase.swipeToCloseApp() {
|
||||
|
||||
device.uiDevice.swipe(
|
||||
device.uiDevice.displayWidth / 2,
|
||||
device.uiDevice.displayHeight / 2,
|
||||
device.uiDevice.displayWidth / 2,
|
||||
device.uiDevice.displayHeight / 30,
|
||||
15
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.common.utils
|
||||
|
||||
import okhttp3.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* Method uses to set WireMock scenario state
|
||||
*/
|
||||
fun setWireMockScenarioState(
|
||||
scenarioName: String,
|
||||
state: String,
|
||||
baseUrl: String = "[REDACTED_ENV_URL]"
|
||||
): Boolean {
|
||||
val client = OkHttpClient()
|
||||
val json = """{"state": "$state"}"""
|
||||
val mediaType = "application/json".toMediaType()
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/__admin/scenarios/$scenarioName/state")
|
||||
.put(json.toRequestBody(mediaType))
|
||||
.build()
|
||||
|
||||
return try {
|
||||
client.newCall(request).execute().use { response ->
|
||||
val body = response.body?.string() ?: ""
|
||||
Timber.d("WireMock scenario request URL: ${request.url}")
|
||||
Timber.d("WireMock scenario request body: $json")
|
||||
Timber.d("WireMock scenario response: ${response.code} - ${response.message}")
|
||||
Timber.d("WireMock scenario response body: $body")
|
||||
response.isSuccessful
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "WireMock scenario error")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checks accessibility of WireMock
|
||||
*/
|
||||
fun checkWireMockStatus(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
||||
val client = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url("$baseUrl/__admin/scenarios")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
client.newCall(request).execute().use { response ->
|
||||
val body = response.body?.string() ?: ""
|
||||
Timber.d("WireMock status check: ${response.code}")
|
||||
Timber.d("Available scenarios: $body")
|
||||
response.isSuccessful
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "WireMock not accessible")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to reset all WireMock scenarios
|
||||
*/
|
||||
fun resetWireMockScenarios(baseUrl: String = "[REDACTED_ENV_URL]"): Boolean {
|
||||
Timber.i("=== WireMock Scenarios Reset ===")
|
||||
Timber.i("Base URL: $baseUrl")
|
||||
|
||||
val client = OkHttpClient()
|
||||
val url = "$baseUrl/__admin/scenarios/reset"
|
||||
Timber.i("Request URL: $url")
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post("".toRequestBody())
|
||||
.build()
|
||||
|
||||
return try {
|
||||
Timber.d("Sending reset request...")
|
||||
client.newCall(request).execute().use { response ->
|
||||
Timber.d("Response code: ${response.code}")
|
||||
Timber.d("Response message: ${response.message}")
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
Timber.d("Response body: $responseBody")
|
||||
|
||||
val isSuccessful = response.isSuccessful
|
||||
Timber.d("Is successful: $isSuccessful")
|
||||
isSuccessful
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e, "Exception during reset")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
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.BuyTokenDetailsScreenTestTags
|
||||
import com.tangem.core.ui.test.NotificationTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
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 androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
|
||||
class BuyTokenDetailsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<BuyTokenDetailsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val topBarMoreButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.MORE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val topBarCloseButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val errorNotificationTitle: KNode = child {
|
||||
hasTestTag(NotificationTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.common_error))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val errorNotificationText: KNode = child {
|
||||
hasTestTag(NotificationTestTags.TEXT)
|
||||
hasText(getResourceString(R.string.common_unknown_error))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val refreshButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.warning_button_refresh))
|
||||
}
|
||||
|
||||
val fiatCurrencyIcon: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val expandFiatListButton: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val fiatAmountTextField: KNode = child {
|
||||
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val tokenAmountField: KNode = child {
|
||||
hasParent(withTestTag(BuyTokenDetailsScreenTestTags.TOKEN_AMOUNT))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val providerLoadingTitle: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TITLE)
|
||||
}
|
||||
|
||||
val providerLoadingText: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_LOADING_TEXT)
|
||||
}
|
||||
|
||||
val providerTitle: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TITLE)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val providerText: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.PROVIDER_TEXT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val buyButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_buy))
|
||||
}
|
||||
|
||||
val toSBlock: KNode = child {
|
||||
hasTestTag(BuyTokenDetailsScreenTestTags.TOS_BLOCK)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onBuyTokenDetailsScreen(function: BuyTokenDetailsPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.test.BuyTokenFiatListTestTags
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
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.compose.node.element.lazylist.KLazyListNode
|
||||
|
||||
class BuyTokenFiatListPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<BuyTokenFiatListPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
|
||||
private val lazyList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(BuyTokenFiatListTestTags.LAZY_LIST) },
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position ->
|
||||
SemanticsMatcher.expectValue(
|
||||
LazyListItemPositionSemantics,
|
||||
position
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
fun fiatListItemWithTitle(title: String): KNode {
|
||||
return lazyList.child<KNode> {
|
||||
hasText(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onBuyTokenFiatListBottomSheet(function: BuyTokenFiatListPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.ExperimentalTestApi
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.test.BuyTokenScreenTestTags
|
||||
import com.tangem.core.ui.test.TokenElementsTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
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.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
|
||||
class BuyTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<BuyTokenPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topAppBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.common_buy))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
private val lazyList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(BuyTokenScreenTestTags.LAZY_LIST) },
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position ->
|
||||
SemanticsMatcher.expectValue(
|
||||
LazyListItemPositionSemantics,
|
||||
position
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalTestApi::class)
|
||||
fun tokenWithTitleAndFiatAmount(tokenTitle: String): KNode {
|
||||
return lazyList.childWith<LazyListItemNode> {
|
||||
hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM)
|
||||
hasText(tokenTitle)
|
||||
useUnmergedTree = true
|
||||
}.child<KNode> {
|
||||
hasTestTag(TokenElementsTestTags.TOKEN_FIAT_AMOUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onBuyTokenScreen(function: BuyTokenPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -3,6 +3,7 @@ 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.DialogTestTags
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen
|
||||
import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen
|
||||
|
|
@ -17,14 +18,19 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
|||
}
|
||||
|
||||
val cancelButton: KNode = child {
|
||||
hasTestTag(DialogTestTags.BUTTON)
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_cancel))
|
||||
}
|
||||
|
||||
val hideButton: KNode = child {
|
||||
hasTestTag(DialogTestTags.BUTTON)
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.token_details_hide_alert_hide))
|
||||
}
|
||||
|
||||
val confirmButton: KNode = child {
|
||||
hasTestTag(BaseButtonTestTags.BUTTON)
|
||||
hasText(getResourceString(R.string.common_confirm))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) =
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package com.tangem.screens
|
|||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.DisclaimerScreenTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.features.disclaimer.impl.R
|
||||
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 DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<DisclaimerPageObject>(
|
||||
|
|
@ -13,6 +16,15 @@ class DisclaimerPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
viewBuilderAction = { hasTestTag(DisclaimerScreenTestTags.SCREEN_CONTAINER) }
|
||||
) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.disclaimer_title))
|
||||
}
|
||||
|
||||
val webView: KNode = child {
|
||||
hasTestTag(DisclaimerScreenTestTags.WEB_VIEW)
|
||||
}
|
||||
|
||||
val acceptButton: KNode = child {
|
||||
hasTestTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,11 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider)
|
|||
hasText(getResourceString(R.string.common_generate_addresses))
|
||||
}
|
||||
|
||||
val buyButton: KNode = child {
|
||||
hasTestTag(MainScreenTestTags.MULTI_CURRENCY_ACTION_BUTTON)
|
||||
hasText(getResourceString(R.string.common_buy))
|
||||
}
|
||||
|
||||
/**
|
||||
* Find token list item with title and address
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
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.ResidenceSettingsScreenTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
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.onramp.impl.R as OnrampImplR
|
||||
|
||||
class ResidenceSettingsPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<ResidenceSettingsPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val topBarTitle: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.onramp_settings_title))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val topBarCloseButton: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.CLOSE_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val residenceButton: KNode = child {
|
||||
hasText(getResourceString(OnrampImplR.string.onramp_settings_residence))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val countryName: KNode = child {
|
||||
hasTestTag(ResidenceSettingsScreenTestTags.COUNTRY_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val residenceSettingsDescription: KNode = child {
|
||||
hasText(getResourceString(OnrampImplR.string.onramp_settings_residence_description))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onResidenceSettingsScreen(function: ResidenceSettingsPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.test.SelectCountryBottomSheetTestTags
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
import com.tangem.features.onramp.impl.R
|
||||
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.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
|
||||
|
||||
class SelectCountryPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SelectCountryPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
|
||||
private val lazyList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(SelectCountryBottomSheetTestTags.LAZY_LIST) },
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position ->
|
||||
SemanticsMatcher.expectValue(
|
||||
LazyListItemPositionSemantics,
|
||||
position
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val searchBar: KNode = child {
|
||||
hasTestTag(SelectCountryBottomSheetTestTags.SEARCH_BAR)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun countryWithNameAndIcon(name: String): KNode {
|
||||
return lazyList.child<KNode> {
|
||||
hasText(name)
|
||||
hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.COUNTRY_ICON))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
fun unavailableCountryWithNameAndIcon(name: String): KNode {
|
||||
return lazyList.child<KNode> {
|
||||
hasText(name)
|
||||
hasAnySibling(withText(getResourceString(R.string.onramp_country_unavailable)))
|
||||
hasAnySibling(withTestTag(SelectCountryBottomSheetTestTags.UNAVAILABLE_COUNTRY_ICON))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSelectCountryBottomSheet(function: SelectCountryPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.utils.LazyListItemNode
|
||||
import com.tangem.core.ui.test.SelectPaymentMethodBottomSheetTestTags
|
||||
import com.tangem.core.ui.test.TopAppBarTestTags
|
||||
import com.tangem.core.ui.utils.LazyListItemPositionSemantics
|
||||
import com.tangem.features.onramp.impl.R
|
||||
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.compose.node.element.lazylist.KLazyListNode
|
||||
import io.github.kakaocup.kakao.common.utilities.getResourceString
|
||||
import androidx.compose.ui.test.hasTestTag as withTestTag
|
||||
import androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class SelectPaymentMethodPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SelectPaymentMethodPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
|
||||
private val lazyList = KLazyListNode(
|
||||
semanticsProvider = semanticsProvider,
|
||||
viewBuilderAction = { hasTestTag(SelectPaymentMethodBottomSheetTestTags.LAZY_LIST) },
|
||||
itemTypeBuilder = { itemType(::LazyListItemNode) },
|
||||
positionMatcher = { position ->
|
||||
SemanticsMatcher.expectValue(
|
||||
LazyListItemPositionSemantics,
|
||||
position
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val title: KNode = child {
|
||||
hasTestTag(TopAppBarTestTags.TITLE)
|
||||
hasText(getResourceString(R.string.onramp_pay_with))
|
||||
}
|
||||
|
||||
fun paymentMethodWithNameAndIcon(name: String): KNode {
|
||||
return lazyList.child<KNode> {
|
||||
hasAnyDescendant(withText(name))
|
||||
hasAnyDescendant(withTestTag(SelectPaymentMethodBottomSheetTestTags.PAYMENT_METHOD_ICON))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSelectPaymentMethodBottomSheet(function: SelectPaymentMethodPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.tangem.screens
|
||||
|
||||
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.core.ui.test.SelectProviderBottomSheetTestTags
|
||||
import com.tangem.features.onramp.impl.R
|
||||
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 androidx.compose.ui.test.hasText as withText
|
||||
|
||||
class SelectProviderPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) :
|
||||
ComposeScreen<SelectProviderPageObject>(semanticsProvider = semanticsProvider) {
|
||||
|
||||
val title: KNode = child {
|
||||
hasText(getResourceString(R.string.onramp_choose_provider_title_hint))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val paymentMethodIcon: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val paymentMethodTitle: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val paymentMethodName: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val paymentMethodExpandButton: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.PAYMENT_METHOD_EXPAND_BUTTON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val availableProviderItem: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val availableProviderName: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val tokenAmount: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.TOKEN_AMOUNT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val unavailableProviderItem: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_ITEM)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val unavailableProviderName: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.UNAVAILABLE_PROVIDER_NAME)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val moreProvidersIcon: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_ICON)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val moreProvidersText: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.MORE_PROVIDERS_TEXT)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
val bestRateLabel: KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.BEST_RATE_LABEL)
|
||||
useUnmergedTree = true
|
||||
}
|
||||
|
||||
fun availableProviderWithName(name: String, tokenAmount: String, rate: String): KNode = child {
|
||||
hasTestTag(SelectProviderBottomSheetTestTags.AVAILABLE_PROVIDER_ITEM)
|
||||
hasAnyChild(withText(name))
|
||||
hasAnyChild(withText(tokenAmount))
|
||||
hasAnyChild(withText(rate))
|
||||
useUnmergedTree = true
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BaseTestCase.onSelectProviderBottomSheet(function: SelectProviderPageObject.() -> Unit) =
|
||||
onComposeScreen(composeTestRule, function)
|
||||
477
app/src/androidTest/kotlin/com/tangem/tests/BuyTokenTest.kt
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
package com.tangem.tests
|
||||
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.utils.resetWireMockScenarios
|
||||
import com.tangem.common.utils.setWireMockScenarioState
|
||||
import com.tangem.scenarios.OpenMainScreenScenario
|
||||
import com.tangem.screens.*
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class BuyTokenTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3478")
|
||||
@DisplayName("Onramp: error in providers loading")
|
||||
@Test
|
||||
fun errorInProvidersLoadingTest() {
|
||||
setupHooks(
|
||||
additionalAfterSection = {
|
||||
resetWireMockScenarios()
|
||||
}
|
||||
).run {
|
||||
val tokenTitle = "Bitcoin"
|
||||
val balance = "$184.85"
|
||||
|
||||
resetWireMockScenarios()
|
||||
|
||||
step("Setup WireMock scenario for 'Error' state") {
|
||||
setWireMockScenarioState("payment_methods", "Error")
|
||||
}
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = $balance") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Assert error notification title is displayed") {
|
||||
onBuyTokenDetailsScreen { errorNotificationTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert error notification text is displayed") {
|
||||
onBuyTokenDetailsScreen { errorNotificationText.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Refresh' button is displayed and clickable") {
|
||||
onBuyTokenDetailsScreen { refreshButton.clickWithAssertion() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("2565")
|
||||
@DisplayName("Onramp: validate currency selector")
|
||||
@Test
|
||||
fun validateCurrencySelectorTest() {
|
||||
setupHooks().run {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
val popularFiatsTitle = "Popular Fiats"
|
||||
val otherCurrenciesTitle = "Other currencies"
|
||||
val australianDollar = "AUD"
|
||||
val fiatAmount = "1"
|
||||
val tokenAmount = "POL 488.24938338"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Write fiat amount = '$fiatAmount'") {
|
||||
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
|
||||
}
|
||||
step("Assert 'Provider loading block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerLoadingTitle.assertIsDisplayed()
|
||||
providerLoadingText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert 'Provider block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerTitle.assertIsDisplayed()
|
||||
providerText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert token amount = '$tokenAmount'") {
|
||||
onBuyTokenDetailsScreen {
|
||||
tokenAmountField.assertTextContains(tokenAmount)
|
||||
}
|
||||
}
|
||||
step("Fiat currency icon is displayed") {
|
||||
onBuyTokenDetailsScreen { fiatCurrencyIcon.assertIsDisplayed() }
|
||||
}
|
||||
step("Click on 'Expand fiat list' button") {
|
||||
onBuyTokenDetailsScreen { expandFiatListButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert '$popularFiatsTitle' is displayed") {
|
||||
onBuyTokenFiatListBottomSheet {
|
||||
fiatListItemWithTitle(popularFiatsTitle).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert '$otherCurrenciesTitle' is displayed") {
|
||||
onBuyTokenFiatListBottomSheet {
|
||||
fiatListItemWithTitle(otherCurrenciesTitle).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Click on fiat with title: '$australianDollar'") {
|
||||
onBuyTokenFiatListBottomSheet {
|
||||
fiatListItemWithTitle(australianDollar).performClick()
|
||||
}
|
||||
}
|
||||
step("Assert new fiat currency: '$australianDollar' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
fiatAmountTextField.assertTextContains(australianDollar + fiatAmount)
|
||||
}
|
||||
}
|
||||
step("Assert token amount = '$tokenAmount'") {
|
||||
onBuyTokenDetailsScreen {
|
||||
tokenAmountField.assertTextContains(tokenAmount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("2566")
|
||||
@DisplayName("Onramp: validate 'Buy token' screen")
|
||||
@Test
|
||||
fun validateBuyTokenScreenTest() {
|
||||
setupHooks().run {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
val euro = "EUR"
|
||||
val fiatAmount = "1"
|
||||
val tokenAmount = "POL 488.24938338"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Buy Token' title is displayed") {
|
||||
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
|
||||
}
|
||||
step("Assert 'More button' in top bar is displayed") {
|
||||
onBuyTokenDetailsScreen { topBarMoreButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Write fiat amount = '$fiatAmount'") {
|
||||
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
|
||||
}
|
||||
step("Assert fiat amount = '$fiatAmount'") {
|
||||
onBuyTokenDetailsScreen { fiatAmountTextField.assertTextContains(euro + fiatAmount) }
|
||||
}
|
||||
step("Assert 'Provider loading block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerLoadingTitle.assertIsDisplayed()
|
||||
providerLoadingText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert 'Provider block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerTitle.assertIsDisplayed()
|
||||
providerText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert token amount = '$tokenAmount'") {
|
||||
onBuyTokenDetailsScreen { tokenAmountField.assertTextContains(tokenAmount) }
|
||||
}
|
||||
step("Assert 'ToS' block is displayed") {
|
||||
onBuyTokenDetailsScreen { toSBlock.assertIsDisplayed()}
|
||||
}
|
||||
step("Assert 'Buy' button is displayed") {
|
||||
onBuyTokenDetailsScreen { buyButton.assertIsDisplayed()}
|
||||
}
|
||||
step("Assert 'Close' button in top bar is displayed") {
|
||||
onBuyTokenDetailsScreen { topBarCloseButton.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("2563")
|
||||
@DisplayName("Onramp: validate 'Residence' settings screen")
|
||||
@Test
|
||||
fun validateResidenceSettingsScreenTest() {
|
||||
setupHooks().run {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
val country = "Albania"
|
||||
val unavailableCountry = "Lebanon"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Buy $tokenTitle' title is displayed") {
|
||||
onBuyTokenDetailsScreen { topBarTitle.assertTextContains("Buy $tokenTitle") }
|
||||
}
|
||||
step("Click 'More' button in tab bar") {
|
||||
onBuyTokenDetailsScreen { topBarMoreButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Residence Settings' screen top bar title is displayed") {
|
||||
onResidenceSettingsScreen { topBarTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Residence Settings' screen top bar 'Close' button is displayed") {
|
||||
onResidenceSettingsScreen { topBarCloseButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Residence' button is displayed on 'Residence Settings' screen") {
|
||||
onResidenceSettingsScreen { residenceButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert country name is displayed on 'Residence Settings' screen") {
|
||||
onResidenceSettingsScreen { countryName.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert residence settings description is displayed on 'Residence Settings' screen") {
|
||||
onResidenceSettingsScreen { residenceSettingsDescription.assertIsDisplayed() }
|
||||
}
|
||||
step("Click 'Residence button'") {
|
||||
onResidenceSettingsScreen { residenceButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Search bar' is displayed") {
|
||||
onSelectCountryBottomSheet { searchBar.assertIsDisplayed() }
|
||||
}
|
||||
step("Type unavailable country name: '$unavailableCountry' in 'Search bar'") {
|
||||
onSelectCountryBottomSheet { searchBar.performTextReplacement(unavailableCountry) }
|
||||
}
|
||||
step("Unavailable country: '$unavailableCountry' is displayed") {
|
||||
onSelectCountryBottomSheet { unavailableCountryWithNameAndIcon(unavailableCountry).assertIsDisplayed() }
|
||||
}
|
||||
step("Type country name: '$country' in 'Search bar'") {
|
||||
onSelectCountryBottomSheet { searchBar.performTextReplacement(country) }
|
||||
}
|
||||
step("Available country: '$country' is displayed") {
|
||||
onSelectCountryBottomSheet { countryWithNameAndIcon(country).assertIsDisplayed() }
|
||||
}
|
||||
step("Click on country: '$country'") {
|
||||
onSelectCountryBottomSheet { countryWithNameAndIcon(country).clickWithAssertion() }
|
||||
}
|
||||
step("Assert country: '$country' is displayed on 'Residence Settings' screen") {
|
||||
onResidenceSettingsScreen { countryName.assertTextContains(country) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("2570")
|
||||
@DisplayName("Onramp: validate 'Select provider' bottom sheet")
|
||||
@Test
|
||||
fun validateProvidersScreenTest() {
|
||||
setupHooks().run {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
val paymentMethod = "Card"
|
||||
val fiatAmount = "1"
|
||||
val providerNameMercuryo = "Mercuryo"
|
||||
val providerNameSimplex = "Simplex"
|
||||
val tokenAmount = "POL 488.24938338"
|
||||
val bestRate = "Best rate"
|
||||
val rate = "-0.00%"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Write fiat amount = '$fiatAmount'") {
|
||||
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
|
||||
}
|
||||
step("Assert 'Provider block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerTitle.assertIsDisplayed()
|
||||
providerText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Open 'Select Provider' bottom sheet") {
|
||||
onBuyTokenDetailsScreen { providerTitle.performClick() }
|
||||
}
|
||||
step("Assert available provider name is displayed") {
|
||||
onSelectProviderBottomSheet {
|
||||
flakySafely(timeoutMs = 20_000) {
|
||||
availableProviderItem.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Assert unavailable provider name is displayed") {
|
||||
onSelectProviderBottomSheet {
|
||||
flakySafely(timeoutMs = 20_000) {
|
||||
unavailableProviderItem.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
}
|
||||
step("Click on 'Expand payment methods' button") {
|
||||
onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on payment method: '$paymentMethod'") {
|
||||
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(paymentMethod).clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Select Provider' bottom sheet title is displayed") {
|
||||
onSelectProviderBottomSheet { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method icon is displayed") {
|
||||
onSelectProviderBottomSheet { paymentMethodIcon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method title is displayed") {
|
||||
onSelectProviderBottomSheet { paymentMethodTitle.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method name is displayed") {
|
||||
onSelectProviderBottomSheet { paymentMethodName.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert provider with name: '$providerNameMercuryo' and rate: '$bestRate' is displayed") {
|
||||
onSelectProviderBottomSheet {
|
||||
availableProviderWithName(providerNameMercuryo, tokenAmount, bestRate).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert provider with name: '$providerNameSimplex' and rate: '$rate' is displayed") {
|
||||
onSelectProviderBottomSheet {
|
||||
availableProviderWithName(providerNameSimplex, tokenAmount, rate).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Assert 'More providers' icon is displayed") {
|
||||
onSelectProviderBottomSheet { moreProvidersIcon.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'More providers' text is displayed") {
|
||||
onSelectProviderBottomSheet { moreProvidersText.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert 'Best rate' label is displayed") {
|
||||
onSelectProviderBottomSheet { bestRateLabel.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3479")
|
||||
@DisplayName("Onramp: validate 'Select payment method' bottom sheet")
|
||||
@Test
|
||||
fun validatePaymentMethodScreenTest() {
|
||||
setupHooks().run {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
val card = "Card"
|
||||
val googlePay = "Google Pay"
|
||||
val invoiceRevolutPay = "Invoice Revolut Pay"
|
||||
val sepa = "Sepa"
|
||||
val fiatAmount = "1"
|
||||
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button") {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Click on 'Buy' button") {
|
||||
onMainScreen { buyButton.clickWithAssertion() }
|
||||
}
|
||||
step("Click on token with name: '$tokenTitle'") {
|
||||
onBuyTokenScreen {
|
||||
topAppBarTitle.assertIsDisplayed()
|
||||
tokenWithTitleAndFiatAmount(tokenTitle).clickWithAssertion()
|
||||
}
|
||||
}
|
||||
step("Click on 'Confirm' button in 'Dialog'") {
|
||||
onDialog { confirmButton.clickWithAssertion() }
|
||||
}
|
||||
step("Write fiat amount = '$fiatAmount'") {
|
||||
onBuyTokenDetailsScreen { fiatAmountTextField.performTextInput(fiatAmount) }
|
||||
}
|
||||
step("Assert 'Provider block' is displayed") {
|
||||
onBuyTokenDetailsScreen {
|
||||
providerTitle.assertIsDisplayed()
|
||||
providerText.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
step("Open 'Select Provider' bottom sheet") {
|
||||
onBuyTokenDetailsScreen { providerTitle.performClick() }
|
||||
}
|
||||
step("Click on 'Expand payment methods' button") {
|
||||
onSelectProviderBottomSheet { paymentMethodExpandButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Select Payment Method' bottom sheet title is displayed") {
|
||||
onSelectPaymentMethodBottomSheet { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method: '$card' is displayed") {
|
||||
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(card).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method: '$googlePay' is displayed") {
|
||||
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(googlePay).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method: '$invoiceRevolutPay' is displayed") {
|
||||
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(invoiceRevolutPay).assertIsDisplayed() }
|
||||
}
|
||||
step("Assert payment method: '$sepa' is displayed") {
|
||||
onSelectPaymentMethodBottomSheet { paymentMethodWithNameAndIcon(sepa).assertIsDisplayed() }
|
||||
}
|
||||
step("Press 'Back' button") {
|
||||
onSelectPaymentMethodBottomSheet { device.uiDevice.pressBack() }
|
||||
}
|
||||
step("Assert 'Select Provider' bottom sheet title is displayed") {
|
||||
onSelectProviderBottomSheet { title.assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -61,7 +61,7 @@ class DetailsTest : BaseTestCase() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
// @Test
|
||||
fun wallet2DetailsTest() =
|
||||
setupHooks().run {
|
||||
scenario(OpenMainScreenScenario(composeTestRule, ProductType.Wallet2))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class HideTokenTest : BaseTestCase() {
|
|||
@Test
|
||||
fun hideWalletTokenByHideButtonTest() {
|
||||
val tokenTitle = "Polygon"
|
||||
val balance = "<$0.01"
|
||||
val balance = "$184.85"
|
||||
setupHooks().run {
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
|
|
|
|||
|
|
@ -79,12 +79,16 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
setupHooks().run {
|
||||
val ethereumTitle = "Ethereum"
|
||||
val bitcoinTitle = "Bitcoin"
|
||||
val balance = "$184.85"
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button" ) {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Check positions of tokens on 'Main Screen'") {
|
||||
onMainScreen {
|
||||
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
|
||||
|
|
@ -154,12 +158,16 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
val ethereumTitle = "Ethereum"
|
||||
val bitcoinTitle = "Bitcoin"
|
||||
val polygonTitle = "Polygon"
|
||||
val balance = "$184.85"
|
||||
step("Open 'Main Screen'") {
|
||||
scenario(OpenMainScreenScenario(composeTestRule))
|
||||
}
|
||||
step("Click on 'Synchronize addresses' button" ) {
|
||||
onMainScreen { synchronizeAddressesButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert wallet balance = '$balance'") {
|
||||
onMainScreen { walletBalance().assertTextContains(balance) }
|
||||
}
|
||||
step("Check positions of tokens on 'Main Screen'") {
|
||||
onMainScreen {
|
||||
tokenWithTitleAndPosition(bitcoinTitle, 0).assertIsDisplayed()
|
||||
|
|
@ -184,8 +192,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
}
|
||||
step("Check positions of tokens by balance on 'Organize tokens' screen") {
|
||||
onOrganizeTokensScreen {
|
||||
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(ethereumTitle, 2).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(bitcoinTitle, 3).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
@ -194,8 +202,8 @@ class OrganizeTokensTest : BaseTestCase() {
|
|||
}
|
||||
step("Check positions of tokens by balance on 'Organize tokens' screen") {
|
||||
onMainScreen {
|
||||
tokenWithTitleAndPosition(ethereumTitle, 0).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(polygonTitle, 1).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(polygonTitle, 0).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed()
|
||||
tokenWithTitleAndPosition(bitcoinTitle, 2).assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase
|
|||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.screens.onDisclaimerScreen
|
||||
import com.tangem.screens.onStoriesScreen
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.github.kakaocup.kakao.intent.KIntent
|
||||
import org.junit.Test
|
||||
|
|
@ -16,6 +15,7 @@ class StoriesTest : BaseTestCase() {
|
|||
@Test
|
||||
fun clickOnOrderButtonTest() =
|
||||
setupHooks().run {
|
||||
val buyWalletUrl = "https://buy.tangem.com/?utm_source=tangem-app&utm_medium=app"
|
||||
onDisclaimerScreen {
|
||||
step("Click on 'Accept' button") {
|
||||
acceptButton.clickWithAssertion()
|
||||
|
|
@ -28,7 +28,7 @@ class StoriesTest : BaseTestCase() {
|
|||
step("Assert: browser opened") {
|
||||
val expectedIntent = KIntent {
|
||||
hasAction(ACTION_VIEW)
|
||||
hasData { toString().startsWith(NEW_BUY_WALLET_URL) }
|
||||
hasData { toString().startsWith(buyWalletUrl) }
|
||||
}
|
||||
expectedIntent.intended()
|
||||
device.uiDevice.pressBack()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,106 @@
|
|||
package com.tangem.tests
|
||||
|
||||
import androidx.test.InstrumentationRegistry.getTargetContext
|
||||
import com.tangem.common.BaseTestCase
|
||||
import com.tangem.common.extensions.clickWithAssertion
|
||||
import com.tangem.common.extensions.swipeToCloseApp
|
||||
import com.tangem.screens.onDisclaimerScreen
|
||||
import com.tangem.screens.onStoriesScreen
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import io.qameta.allure.kotlin.AllureId
|
||||
import io.qameta.allure.kotlin.junit4.DisplayName
|
||||
import org.junit.Test
|
||||
|
||||
@HiltAndroidTest
|
||||
class TermsOfServiceTest : BaseTestCase() {
|
||||
|
||||
@AllureId("3573")
|
||||
@DisplayName("ToS: success acceptance")
|
||||
@Test
|
||||
fun validateTermsOfServiceScreenTest() {
|
||||
setupHooks().run {
|
||||
val tosUrl = "https://tangem.com/tangem_tos.html"
|
||||
step("Assert title of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert title of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { webView.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify WebView loads correct URL") {
|
||||
onDisclaimerScreen {
|
||||
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
|
||||
}
|
||||
}
|
||||
step("Click on 'Accept' button") {
|
||||
onDisclaimerScreen { acceptButton.clickWithAssertion() }
|
||||
}
|
||||
step("Assert 'Stories' screen is opened") {
|
||||
onStoriesScreen {
|
||||
scanButton.assertIsDisplayed()
|
||||
orderButton.assertIsDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AllureId("3574")
|
||||
@DisplayName("ToS: accept after app restart")
|
||||
@Test
|
||||
fun acceptTermsOfServiceAfterAppRestart() {
|
||||
val packageName = getTargetContext().packageName
|
||||
setupHooks().run {
|
||||
val tosUrl = "https://tangem.com/tangem_tos.html"
|
||||
step("Assert title of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert WebView of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { webView.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify WebView loads correct URL") {
|
||||
onDisclaimerScreen {
|
||||
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
|
||||
}
|
||||
}
|
||||
step("'Accept' button is displayed") {
|
||||
onDisclaimerScreen { acceptButton.assertIsDisplayed() }
|
||||
}
|
||||
step("Open recent apps") {
|
||||
device.uiDevice.pressRecentApps()
|
||||
}
|
||||
step("Stop app by swipe") {
|
||||
swipeToCloseApp()
|
||||
}
|
||||
step("Launch app") {
|
||||
device.apps.launch(packageName)
|
||||
}
|
||||
step("Assert title of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { title.assertIsDisplayed() }
|
||||
}
|
||||
step("Assert WebView of 'Disclaimer screen' is displayed") {
|
||||
onDisclaimerScreen { webView.assertIsDisplayed() }
|
||||
}
|
||||
step("Verify WebView loads correct URL") {
|
||||
onDisclaimerScreen {
|
||||
webView.assertContentDescriptionContains(value = tosUrl, substring = true)
|
||||
}
|
||||
}
|
||||
step("Click on 'Accept' button") {
|
||||
onDisclaimerScreen { acceptButton.clickWithAssertion() }
|
||||
}
|
||||
step("Open recent apps") {
|
||||
device.uiDevice.pressRecentApps()
|
||||
}
|
||||
step("Stop app by swipe") {
|
||||
swipeToCloseApp()
|
||||
}
|
||||
step("Launch app") {
|
||||
device.apps.launch(packageName)
|
||||
}
|
||||
step("Assert 'Stories' screen is opened") {
|
||||
onStoriesScreen {
|
||||
scanButton.assertIsDisplayed()
|
||||
orderButton.assertIsDisplayed()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo
|
|||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.SystemBarStyle
|
||||
|
|
@ -175,13 +176,22 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
@Inject
|
||||
internal lateinit var testerMenuLauncher: TesterMenuLauncher
|
||||
|
||||
@Inject
|
||||
internal lateinit var intentProcessor: IntentProcessor
|
||||
|
||||
@Inject
|
||||
internal lateinit var walletConnectLinkIntentHandler: WalletConnectLinkIntentHandler
|
||||
|
||||
@Inject
|
||||
internal lateinit var onPushClickedIntentHandler: OnPushClickedIntentHandler
|
||||
|
||||
@Inject
|
||||
internal lateinit var backgroundScanIntentHandler: BackgroundScanIntentHandler
|
||||
|
||||
internal val viewModel: MainViewModel by viewModels()
|
||||
|
||||
private lateinit var appThemeModeFlow: SharedFlow<AppThemeMode>
|
||||
|
||||
// TODO: fixme: inject through DI
|
||||
private val intentProcessor: IntentProcessor = IntentProcessor()
|
||||
|
||||
private val dialogManager = DialogManager()
|
||||
|
||||
private val onActivityResultCallbacks = mutableListOf<OnActivityResultCallback>()
|
||||
|
|
@ -231,7 +241,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
lifecycle.addObserver(defaultDeviceFlipDetector)
|
||||
|
||||
if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
lifecycle.addObserver(testerMenuLauncher.launchOnShakeObserver)
|
||||
lifecycle.addObserver(testerMenuLauncher.launchOnKeyEventObserver)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -343,12 +353,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
private fun initIntentHandlers() {
|
||||
val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets }
|
||||
intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler))
|
||||
intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope))
|
||||
intentProcessor.addHandler(onPushClickedIntentHandler)
|
||||
|
||||
if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) {
|
||||
intentProcessor.addHandler(WalletConnectLinkIntentHandler())
|
||||
intentProcessor.addHandler(walletConnectLinkIntentHandler)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -409,10 +417,17 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||
val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler)
|
||||
|
||||
return if (result) super.dispatchTouchEvent(event) else false
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
return if (BuildConfig.TESTER_MENU_ENABLED) {
|
||||
testerMenuLauncher.launchOnKeyEventObserver.dispatchKeyEvent(event) || super.dispatchKeyEvent(event)
|
||||
} else {
|
||||
super.dispatchKeyEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) {
|
||||
val backStack = appRouterConfig.stack ?: emptyList()
|
||||
// TODO move inital navigation to navigation component ([REDACTED_JIRA])
|
||||
|
|
@ -434,9 +449,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
private fun navigateToInitialScreen(intentWhichStartedActivity: Intent?) {
|
||||
val launchMode = backgroundScanIntentHandler.getInitScreenLaunchMode(intentWhichStartedActivity)
|
||||
if (userWalletsListManager.isLockable && userWalletsListManager.hasUserWallets) {
|
||||
store.dispatchNavigationAction {
|
||||
replaceAll(AppRoute.Welcome(intentWhichStartedActivity?.let(::SerializableIntent)))
|
||||
replaceAll(
|
||||
AppRoute.Welcome(
|
||||
launchMode = launchMode,
|
||||
intent = intentWhichStartedActivity?.let(::SerializableIntent),
|
||||
),
|
||||
)
|
||||
}
|
||||
intentProcessor.handleIntent(
|
||||
intent = intentWhichStartedActivity,
|
||||
|
|
@ -450,7 +471,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
val route = if (shouldShowTos) {
|
||||
AppRoute.Disclaimer(isTosAccepted = false)
|
||||
} else {
|
||||
AppRoute.Home
|
||||
AppRoute.Home(launchMode = launchMode)
|
||||
}
|
||||
|
||||
store.dispatchNavigationAction { replaceAll(route) }
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class IntroductionProcess(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Introduction Process", event, params) {
|
||||
|
||||
class ScreenOpened : IntroductionProcess("Introduction Process Screen Opened")
|
||||
class ButtonTokensList : IntroductionProcess("Button - Tokens List")
|
||||
class ButtonBuyCards : IntroductionProcess("Button - Buy Cards")
|
||||
class ButtonScanCard : IntroductionProcess("Button - Scan Card")
|
||||
class ButtonRequestSupport : IntroductionProcess("Button - Request Support")
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.tap.common.extensions.filterNotNull
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed class Shop(
|
||||
event: String,
|
||||
params: Map<String, String> = mapOf(),
|
||||
) : AnalyticsEvent("Shop", event, params) {
|
||||
|
||||
class ScreenOpened : Shop("Shop Screen Opened")
|
||||
|
||||
class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
|
||||
event = "Purchased",
|
||||
params = mapOf(
|
||||
"SKU" to sku,
|
||||
"Count" to count,
|
||||
"Amount" to amount,
|
||||
"Coupon Code" to couponCode,
|
||||
).filterNotNull(),
|
||||
)
|
||||
|
||||
class Redirected(partnerName: String?) : Shop(
|
||||
event = "Redirected",
|
||||
params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(),
|
||||
)
|
||||
}
|
||||
|
|
@ -6,9 +6,9 @@ import com.tangem.domain.card.common.util.cardTypesResolver
|
|||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.features.home.impl.analytics.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.Easing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
typealias AnimatedValue = Pair<Float, Float>
|
||||
|
||||
@Composable
|
||||
fun AnimatedValue.toAnimatable(
|
||||
isPaused: Boolean,
|
||||
duration: Int,
|
||||
easing: Easing = LinearEasing,
|
||||
): Animatable<Float, AnimationVector1D> {
|
||||
return animatable(
|
||||
values = this,
|
||||
isPaused = isPaused,
|
||||
duration = duration,
|
||||
easing = easing,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun animatable(
|
||||
values: AnimatedValue,
|
||||
duration: Int,
|
||||
isPaused: Boolean = false,
|
||||
easing: Easing = LinearEasing,
|
||||
): Animatable<Float, AnimationVector1D> {
|
||||
val animatable = remember { Animatable(values.first) }
|
||||
|
||||
LaunchedEffect(isPaused) {
|
||||
if (isPaused) {
|
||||
animatable.stop()
|
||||
} else {
|
||||
animatable.animateTo(
|
||||
targetValue = values.second,
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = easing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return animatable
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun Dp.toPx(): Float {
|
||||
val currentDp = this
|
||||
return with(LocalDensity.current) { currentDp.toPx() }
|
||||
}
|
||||
|
||||
fun DpSize.halfHeight(): Dp = this.height / 2
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
|
||||
val drawable = requireNotNull(AppCompatResources.getDrawable(LocalContext.current, drawableId)) {
|
||||
"drawable is null"
|
||||
}
|
||||
return drawable.toBitmap().asImageBitmap()
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.common.compose.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.sdk.extensions.pxToDp
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun Painter.dpSize(): DpSize = DpSize(
|
||||
intrinsicSize.width.pxToDp().dp,
|
||||
intrinsicSize.height.pxToDp().dp,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)
|
||||
|
|
@ -24,7 +24,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) {
|
|||
|
||||
fun Analytics.setContext(userWallet: UserWallet) {
|
||||
setUserId(userWallet.walletId.stringValue)
|
||||
// TODO add product type for hot ([REDACTED_TASK_KEY])
|
||||
// TODO add product type for hot ([REDACTED_TASK_KEY] [Hot Wallet] Analytics)
|
||||
|
||||
if (userWallet is UserWallet.Cold) {
|
||||
addParamsInterceptor(LinkedCardContextInterceptor(userWallet.scanResponse))
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
package com.tangem.tap.common.extensions
|
||||
|
||||
fun Int.isEven() = this and 1 == 0
|
||||
|
|
@ -3,7 +3,6 @@ package com.tangem.tap.common.redux
|
|||
import com.tangem.tap.common.redux.global.globalReducer
|
||||
import com.tangem.tap.features.details.redux.DetailsReducer
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
|
||||
import com.tangem.tap.features.home.redux.HomeReducer
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeReducer
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphReducer
|
||||
import org.rekotlin.Action
|
||||
|
|
@ -14,7 +13,6 @@ fun appReducer(action: Action, state: AppState?): AppState {
|
|||
|
||||
return AppState(
|
||||
globalState = globalReducer(action, state),
|
||||
homeState = HomeReducer.reduce(action, state),
|
||||
detailsState = DetailsReducer.reduce(action, state),
|
||||
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
|
||||
welcomeState = WelcomeReducer.reduce(action, state),
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@ import com.tangem.tap.features.details.redux.DetailsMiddleware
|
|||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddleware
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware
|
||||
import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeMiddleware
|
||||
|
|
@ -20,7 +18,6 @@ import org.rekotlin.StateType
|
|||
|
||||
data class AppState(
|
||||
val globalState: GlobalState = GlobalState(),
|
||||
val homeState: HomeState = HomeState(),
|
||||
val detailsState: DetailsState = DetailsState(),
|
||||
val walletConnectState: WalletConnectState = WalletConnectState(),
|
||||
val welcomeState: WelcomeState = WelcomeState(),
|
||||
|
|
@ -32,7 +29,6 @@ data class AppState(
|
|||
return listOf(
|
||||
logMiddleware,
|
||||
GlobalMiddleware.handler,
|
||||
HomeMiddleware.handler,
|
||||
DetailsMiddleware().detailsMiddleware,
|
||||
WalletConnectMiddleware().walletConnectMiddleware,
|
||||
BackupMiddleware().backupMiddleware,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.core.view.isVisible
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
|
|
@ -14,8 +15,6 @@ import com.tangem.tap.common.analytics.events.ScanFailsDialogAnalytics
|
|||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
|
|
@ -29,6 +28,7 @@ internal object ScanFailsDialog {
|
|||
|
||||
private const val HOW_TO_SCAN_RU_LINK = "https://tangem.com/ru/blog/post/scan-tangem-card/"
|
||||
private const val HOW_TO_SCAN_LINK = "https://tangem.com/en/blog/post/scan-tangem-card/"
|
||||
private const val RUSSIA_LOCALE = "ru"
|
||||
|
||||
fun create(context: Context, source: StateDialog.ScanFailsSource, onTryAgain: (() -> Unit)? = null): AlertDialog {
|
||||
return AlertDialog.Builder(context, R.style.CustomMaterialDialog).apply {
|
||||
|
|
@ -62,8 +62,8 @@ internal object ScanFailsDialog {
|
|||
source = sourceAnalytics,
|
||||
),
|
||||
)
|
||||
val locale = LocaleRegionProvider().getRegion()
|
||||
val link = if (locale.lowercase() == RUSSIA_COUNTRY_CODE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
|
||||
val locale = Locale.current.region
|
||||
val link = if (locale.lowercase() == RUSSIA_LOCALE) HOW_TO_SCAN_RU_LINK else HOW_TO_SCAN_LINK
|
||||
store.dispatchOpenUrl(link)
|
||||
}
|
||||
customView.findViewById<TextView>(R.id.request_support_button)?.setOnClickListener {
|
||||
|
|
|
|||
34
app/src/main/java/com/tangem/tap/di/IntentHandlingModule.kt
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package com.tangem.tap.di
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.tap.features.intentHandler.IntentProcessor
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object IntentHandlingModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBackgroundScanIntentHandler(): BackgroundScanIntentHandler = BackgroundScanIntentHandler()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletConnectLinkIntentHandler(): WalletConnectLinkIntentHandler = WalletConnectLinkIntentHandler()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOnPushClickedIntentHandler(analyticsEventHandler: AnalyticsEventHandler): OnPushClickedIntentHandler =
|
||||
OnPushClickedIntentHandler(analyticsEventHandler)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIntentProcessor(): IntentProcessor = IntentProcessor()
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.tap.di.data
|
||||
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.card.DefaultDerivationsRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CardDataModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun providesDerivationsRepository(
|
||||
tangemSdkManager: TangemSdkManager,
|
||||
userWalletsStore: UserWalletsStore,
|
||||
networkFactory: NetworkFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): DerivationsRepository {
|
||||
return DefaultDerivationsRepository(
|
||||
tangemSdkManager = tangemSdkManager,
|
||||
userWalletsStore = userWalletsStore,
|
||||
networkFactory = networkFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,11 +2,14 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.card.*
|
||||
import com.tangem.domain.card.repository.CardRepository
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase
|
||||
import com.tangem.domain.wallets.usecase.HasMissedDerivationsUseCase
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.managetokens.*
|
||||
import com.tangem.domain.managetokens.repository.CustomTokensRepository
|
||||
import com.tangem.domain.managetokens.repository.ManageTokensRepository
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
|
|
@ -72,6 +73,7 @@ internal object ManageTokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): SaveManagedTokensUseCase {
|
||||
return SaveManagedTokensUseCase(
|
||||
customTokensRepository = customTokensRepository,
|
||||
|
|
@ -81,6 +83,7 @@ internal object ManageTokensDomainModule {
|
|||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.wallets.derivations.DerivationsRepository
|
||||
import com.tangem.domain.markets.*
|
||||
import com.tangem.domain.markets.repositories.MarketsTokenRepository
|
||||
import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher
|
||||
|
|
@ -9,6 +9,7 @@ import com.tangem.domain.promo.PromoRepository
|
|||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
|
|
@ -63,6 +64,7 @@ object MarketsDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): SaveMarketTokensUseCase {
|
||||
return SaveMarketTokensUseCase(
|
||||
derivationsRepository = derivationsRepository,
|
||||
|
|
@ -71,6 +73,7 @@ object MarketsDomainModule {
|
|||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.tap.di.domain
|
|||
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.domain.notifications.*
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.notifications.repository.PushNotificationsRepository
|
||||
import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles
|
||||
import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles
|
||||
import com.tangem.utils.notifications.PushNotificationsTokenProvider
|
||||
|
|
@ -18,20 +19,22 @@ internal object NotificationsDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase {
|
||||
fun providesGetApplicationIdUseCase(
|
||||
pushNotificationsRepository: PushNotificationsRepository,
|
||||
): GetApplicationIdUseCase {
|
||||
return GetApplicationIdUseCase(
|
||||
notificationsRepository = notificationsRepository,
|
||||
pushNotificationsRepository = pushNotificationsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesSendPushTokenUseCase(
|
||||
notificationsRepository: NotificationsRepository,
|
||||
pushNotificationsRepository: PushNotificationsRepository,
|
||||
pushNotificationsTokenProvider: PushNotificationsTokenProvider,
|
||||
): SendPushTokenUseCase {
|
||||
return SendPushTokenUseCase(
|
||||
notificationsRepository = notificationsRepository,
|
||||
pushNotificationsRepository = pushNotificationsRepository,
|
||||
pushNotificationsTokenProvider = pushNotificationsTokenProvider,
|
||||
)
|
||||
}
|
||||
|
|
@ -56,6 +59,26 @@ internal object NotificationsDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesShouldShowNotificationUseCase(
|
||||
notificationsRepository: NotificationsRepository,
|
||||
): ShouldShowNotificationUseCase {
|
||||
return ShouldShowNotificationUseCase(
|
||||
notificationsRepository = notificationsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesSetShouldShowNotificationUseCase(
|
||||
notificationsRepository: NotificationsRepository,
|
||||
): SetShouldShowNotificationUseCase {
|
||||
return SetShouldShowNotificationUseCase(
|
||||
notificationsRepository = notificationsRepository,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles {
|
||||
|
|
@ -65,8 +88,8 @@ internal object NotificationsDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideGetNetworksAvailableForNotifications(
|
||||
notificationsRepository: NotificationsRepository,
|
||||
pushNotificationsRepository: PushNotificationsRepository,
|
||||
): GetNetworksAvailableForNotificationsUseCase {
|
||||
return GetNetworksAvailableForNotificationsUseCase(notificationsRepository = notificationsRepository)
|
||||
return GetNetworksAvailableForNotificationsUseCase(pushNotificationsRepository = pushNotificationsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -94,12 +94,12 @@ internal object StakingDomainModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchStakingYieldBalanceUseCase(
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchStakingYieldBalanceUseCase {
|
||||
return FetchStakingYieldBalanceUseCase(
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -173,18 +173,6 @@ internal object StakingDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideIsApproveNeededUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
stakingErrorResolver: StakingErrorResolver,
|
||||
): IsApproveNeededUseCase {
|
||||
return IsApproveNeededUseCase(
|
||||
stakingRepository = stakingRepository,
|
||||
stakingErrorResolver = stakingErrorResolver,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetConstructedStakingTransactionUseCase(
|
||||
|
|
@ -209,12 +197,6 @@ internal object StakingDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetStakingIntegrationIdUseCase(stakingRepository: StakingRepository): GetStakingIntegrationIdUseCase {
|
||||
return GetStakingIntegrationIdUseCase(stakingRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCheckAccountInitializedUseCase(
|
||||
|
|
@ -225,9 +207,13 @@ internal object StakingDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetActionRequirementAmountUseCase(
|
||||
stakingRepository: StakingRepository,
|
||||
): GetActionRequirementAmountUseCase {
|
||||
return GetActionRequirementAmountUseCase(stakingRepository)
|
||||
fun provideGetActionRequirementAmountUseCase(): GetActionRequirementAmountUseCase {
|
||||
return GetActionRequirementAmountUseCase()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory {
|
||||
return StakingIdFactory(walletManagersFacade = walletManagersFacade)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,9 @@ import com.tangem.domain.promo.PromoRepository
|
|||
import com.tangem.domain.quotes.QuotesRepository
|
||||
import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher
|
||||
import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier
|
||||
import com.tangem.domain.staking.StakingIdFactory
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier
|
||||
import com.tangem.domain.staking.repositories.StakingRepository
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceFetcher
|
||||
import com.tangem.domain.staking.single.SingleYieldBalanceSupplier
|
||||
|
|
@ -46,6 +48,7 @@ internal object TokensDomainModule {
|
|||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): AddCryptoCurrenciesUseCase {
|
||||
return AddCryptoCurrenciesUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -54,6 +57,7 @@ internal object TokensDomainModule {
|
|||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -64,12 +68,14 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchTokenListUseCase {
|
||||
return FetchTokenListUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -172,6 +178,7 @@ internal object TokensDomainModule {
|
|||
singleYieldBalanceFetcher: SingleYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchCurrencyStatusUseCase {
|
||||
return FetchCurrencyStatusUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
|
|
@ -180,6 +187,7 @@ internal object TokensDomainModule {
|
|||
singleYieldBalanceFetcher = singleYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -190,12 +198,14 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): FetchCardTokenListUseCase {
|
||||
return FetchCardTokenListUseCase(
|
||||
currenciesRepository = currenciesRepository,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +405,6 @@ internal object TokensDomainModule {
|
|||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
|
|
@ -403,13 +412,14 @@ internal object TokensDomainModule {
|
|||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
|
||||
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): BaseCurrenciesStatusesOperations {
|
||||
return CachedCurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
|
|
@ -417,9 +427,11 @@ internal object TokensDomainModule {
|
|||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
|
||||
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -429,7 +441,6 @@ internal object TokensDomainModule {
|
|||
tokensFeatureToggles: TokensFeatureToggles,
|
||||
currenciesRepository: CurrenciesRepository,
|
||||
quotesRepository: QuotesRepository,
|
||||
stakingRepository: StakingRepository,
|
||||
singleNetworkStatusSupplier: SingleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier: MultiNetworkStatusSupplier,
|
||||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
|
|
@ -437,13 +448,14 @@ internal object TokensDomainModule {
|
|||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
singleQuoteStatusSupplier: SingleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier: SingleYieldBalanceSupplier,
|
||||
multiYieldBalanceSupplier: MultiYieldBalanceSupplier,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
): BaseCurrencyStatusOperations {
|
||||
return CachedCurrenciesStatusesOperations(
|
||||
currenciesRepository = currenciesRepository,
|
||||
quotesRepository = quotesRepository,
|
||||
stakingRepository = stakingRepository,
|
||||
singleNetworkStatusSupplier = singleNetworkStatusSupplier,
|
||||
multiNetworkStatusSupplier = multiNetworkStatusSupplier,
|
||||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
|
|
@ -451,9 +463,11 @@ internal object TokensDomainModule {
|
|||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
singleQuoteStatusSupplier = singleQuoteStatusSupplier,
|
||||
singleYieldBalanceSupplier = singleYieldBalanceSupplier,
|
||||
multiYieldBalanceSupplier = multiYieldBalanceSupplier,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
|
||||
tokensFeatureToggles = tokensFeatureToggles,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -472,6 +486,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher: MultiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher: MultiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher: MultiYieldBalanceFetcher,
|
||||
stakingIdFactory: StakingIdFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): WalletBalanceFetcher {
|
||||
return WalletBalanceFetcher(
|
||||
|
|
@ -481,6 +496,7 @@ internal object TokensDomainModule {
|
|||
multiNetworkStatusFetcher = multiNetworkStatusFetcher,
|
||||
multiQuoteStatusFetcher = multiQuoteStatusFetcher,
|
||||
multiYieldBalanceFetcher = multiYieldBalanceFetcher,
|
||||
stakingIdFactory = stakingIdFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.data.wallets.hot.TangemHotWalletSigner
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.demo.models.DemoConfig
|
||||
import com.tangem.domain.networks.single.SingleNetworkStatusFetcher
|
||||
|
|
@ -11,7 +12,6 @@ import com.tangem.domain.transaction.FeeRepository
|
|||
import com.tangem.domain.transaction.TransactionRepository
|
||||
import com.tangem.domain.transaction.usecase.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.tap.domain.hot.TangemHotWalletSigner
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -180,8 +180,13 @@ internal object TransactionDomainModule {
|
|||
fun providePrepareForSendUseCase(
|
||||
transactionRepository: TransactionRepository,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
): PrepareForSendUseCase {
|
||||
return PrepareForSendUseCase(transactionRepository, cardSdkConfigRepository)
|
||||
return PrepareForSendUseCase(
|
||||
transactionRepository = transactionRepository,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
@ -189,8 +194,13 @@ internal object TransactionDomainModule {
|
|||
fun provideSignUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory,
|
||||
): SignUseCase {
|
||||
return SignUseCase(cardSdkConfigRepository, walletManagersFacade)
|
||||
return SignUseCase(
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
getHotTransactionSigner = { tangemHotWalletSignerFactory.create(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
package com.tangem.tap.di.hot
|
||||
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
|
||||
import com.tangem.tap.features.hot.DefaultHotWalletPasswordRequester
|
||||
import com.tangem.tap.features.hot.TangemHotSDKProxy
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
|
@ -17,8 +15,4 @@ internal interface TangemHotSdkModule {
|
|||
@Binds
|
||||
@Singleton
|
||||
fun bindTangemHotSdk(proxy: TangemHotSDKProxy): TangemHotSdk
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindHotWalletPasswordRequester(impl: DefaultHotWalletPasswordRequester): HotWalletPasswordRequester
|
||||
}
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.domain.card.BackendId
|
||||
import com.tangem.domain.card.repository.DerivationsRepository
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.sdk.api.TangemSdkManager
|
||||
import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
internal typealias Derivations = Map<ByteArrayKey, List<DerivationPath>>
|
||||
private typealias DerivedKeys = Map<ByteArrayKey, ExtendedPublicKeysMap>
|
||||
|
||||
internal class DefaultDerivationsRepository(
|
||||
private val tangemSdkManager: TangemSdkManager,
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) : DerivationsRepository {
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, currencies: List<CryptoCurrency>) {
|
||||
derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network))
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List<Network.RawID>) {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
derivePublicKeysByNetworks(
|
||||
userWalletId = userWalletId,
|
||||
networks = networkIds.mapNotNull {
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(it.value) ?: return@mapNotNull null,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeysByNetworks(userWalletId: UserWalletId, networks: List<Network>) {
|
||||
val userWallet = withContext(dispatchers.io) {
|
||||
userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
}
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return
|
||||
}
|
||||
|
||||
userWallet.requireColdWallet()
|
||||
|
||||
if (!userWallet.scanResponse.card.settings.isHDWalletAllowed) {
|
||||
Timber.d("Nothing to derive")
|
||||
return
|
||||
}
|
||||
|
||||
val derivations = MissedDerivationsFinder(scanResponse = userWallet.scanResponse)
|
||||
.findByNetworks(networks)
|
||||
.ifEmpty {
|
||||
Timber.d("Nothing to derive")
|
||||
return
|
||||
}
|
||||
|
||||
derivePublicKeys(userWalletId = userWalletId, derivations = derivations)
|
||||
}
|
||||
|
||||
override suspend fun hasMissedDerivations(
|
||||
userWalletId: UserWalletId,
|
||||
networksWithDerivationPath: Map<BackendId, String?>,
|
||||
): Boolean {
|
||||
val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found")
|
||||
|
||||
if (userWallet is UserWallet.Hot) {
|
||||
return false
|
||||
}
|
||||
|
||||
val derivations =
|
||||
MissedDerivationsFinder(scanResponse = userWallet.requireColdWallet().scanResponse)
|
||||
.findByNetworks(
|
||||
networksWithDerivationPath.mapNotNull { (backendId, extraDerivationPath) ->
|
||||
networkFactory.create(
|
||||
blockchain = Blockchain.fromNetworkId(backendId) ?: return@mapNotNull null,
|
||||
extraDerivationPath = extraDerivationPath,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return derivations.isNotEmpty()
|
||||
}
|
||||
|
||||
override suspend fun derivePublicKeys(userWalletId: UserWalletId, derivations: Derivations): DerivedKeys {
|
||||
// todo replace it in task [REDACTED_JIRA]
|
||||
val preflightReadFilter = UserWalletIdPreflightReadFilter(userWalletId)
|
||||
tangemSdkManager.derivePublicKeys(
|
||||
cardId = null,
|
||||
derivations = derivations,
|
||||
preflightReadFilter = preflightReadFilter,
|
||||
).doOnSuccess { response ->
|
||||
updatePublicKeys(userWalletId = userWalletId, keys = response.entries)
|
||||
.doOnSuccess {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
validateDerivations(scanResponse = it.requireColdWallet().scanResponse, derivations = derivations)
|
||||
return response.entries
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
}
|
||||
.doOnFailure { throw it }
|
||||
|
||||
error("This code should never be reached")
|
||||
}
|
||||
|
||||
/**
|
||||
* It throws an exception if any of the provided derivations are invalid
|
||||
* Validation for NonHardened moved to application layer, to avoid fails when derive multiple paths
|
||||
* It needs to be called after success [derivePublicKeys] or in same flows
|
||||
*/
|
||||
private fun validateDerivations(scanResponse: ScanResponse, derivations: Derivations) {
|
||||
derivations.entries.forEach { derivationForKey ->
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.publicKey.toMapKey() == derivationForKey.key }
|
||||
if (wallet == null) return@forEach
|
||||
val hasHardenedNodes = derivationForKey.value.any { path -> path.nodes.any { node -> !node.isHardened } }
|
||||
if (wallet.curve == EllipticCurve.Ed25519Slip0010 && hasHardenedNodes) {
|
||||
throw TangemSdkError.NonHardenedDerivationNotSupported()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updatePublicKeys(userWalletId: UserWalletId, keys: DerivedKeys): CompletionResult<UserWallet> {
|
||||
return withContext(dispatchers.io) {
|
||||
userWalletsStore.update(
|
||||
userWalletId = userWalletId,
|
||||
update = { userWallet -> userWallet.requireColdWallet().updateDerivedKeys(keys) }, // TODO [REDACTED_TASK_KEY]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.Cold.updateDerivedKeys(keys: DerivedKeys): UserWallet {
|
||||
return copy(
|
||||
scanResponse = scanResponse.copy(
|
||||
derivedKeys = getUpdatedDerivedKeys(oldKeys = scanResponse.derivedKeys, newKeys = keys),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getUpdatedDerivedKeys(oldKeys: DerivedKeys, newKeys: DerivedKeys): DerivedKeys {
|
||||
return (oldKeys.keys + newKeys.keys).toSet()
|
||||
.associateWith { walletKey ->
|
||||
val oldDerivations = ExtendedPublicKeysMap(oldKeys[walletKey] ?: emptyMap())
|
||||
val newDerivations = newKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
|
||||
ExtendedPublicKeysMap(oldDerivations + newDerivations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package com.tangem.tap.domain.card
|
||||
|
||||
import com.tangem.blockchain.blockchains.cardano.CardanoUtils
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toBlockchain
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.common.extensions.ByteArrayKey
|
||||
import com.tangem.common.extensions.toMapKey
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.scan.KeyWalletPublicKey
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
|
||||
private typealias DerivationData = Pair<ByteArrayKey, List<DerivationPath>>
|
||||
|
||||
/**
|
||||
* Finder of missed derivations
|
||||
*
|
||||
* @property scanResponse scanning response
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) {
|
||||
|
||||
/** Find missed derivations for given currencies [currencies] */
|
||||
fun find(currencies: List<CryptoCurrency>): Derivations {
|
||||
return currencies.map { it.network }.let(::findByNetworks)
|
||||
}
|
||||
|
||||
fun findByNetworks(networks: List<Network>): Derivations {
|
||||
return buildMap<ByteArrayKey, MutableList<DerivationPath>> {
|
||||
networks
|
||||
.mapToNewDerivations()
|
||||
.forEach { data ->
|
||||
val current = this[data.first]
|
||||
if (current != null) {
|
||||
current.addAll(data.second)
|
||||
current.distinct()
|
||||
} else {
|
||||
this[data.first] = data.second.toMutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Network>.mapToNewDerivations(): List<DerivationData> {
|
||||
val config = CardConfig.createConfig(scanResponse.card)
|
||||
return mapNotNull { network ->
|
||||
val blockchain = network.toBlockchain()
|
||||
val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null
|
||||
|
||||
findNewDerivations(curve = curve, scanResponse = scanResponse, network = network)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findNewDerivations(
|
||||
curve: EllipticCurve,
|
||||
scanResponse: ScanResponse,
|
||||
network: Network,
|
||||
): DerivationData? {
|
||||
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
|
||||
val publicKey = wallet.publicKey.toMapKey()
|
||||
|
||||
val derivationCandidates = network
|
||||
.getDerivationCandidates(curve)
|
||||
.ifEmpty { return null }
|
||||
.filterAlreadyDerivedKeys(publicKey)
|
||||
.ifEmpty { return null }
|
||||
|
||||
return publicKey to derivationCandidates
|
||||
}
|
||||
|
||||
private fun Network.getDerivationCandidates(curve: EllipticCurve): List<DerivationPath> {
|
||||
val blockchain = this.toBlockchain()
|
||||
|
||||
return buildList {
|
||||
add(blockchain.getDerivationPath(curve = curve))
|
||||
add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates))
|
||||
add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates))
|
||||
}
|
||||
.filterNotNull()
|
||||
.distinct()
|
||||
}
|
||||
|
||||
private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
derivationPath(style = scanResponse.derivationStyleProvider.getDerivationStyle())
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? {
|
||||
return if (getSupportedCurves().contains(curve)) {
|
||||
network.derivationPath.value?.let(::DerivationPath)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? {
|
||||
return if (this == Blockchain.Cardano) {
|
||||
network.derivationPath.value?.let {
|
||||
CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<DerivationPath>.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey)
|
||||
return filterNot(alreadyDerivedPaths::contains)
|
||||
}
|
||||
|
||||
private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List<DerivationPath> {
|
||||
val extendedPublicKeysMap = scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap())
|
||||
return extendedPublicKeysMap.keys.toList()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.features.hotwallet.HotWalletPasswordRequester
|
||||
import com.tangem.hot.sdk.TangemHotSdk
|
||||
import com.tangem.hot.sdk.exception.WrongPasswordException
|
||||
import com.tangem.hot.sdk.model.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class HotWalletAccessor @Inject constructor(
|
||||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotWalletPasswordRequester: HotWalletPasswordRequester,
|
||||
) {
|
||||
|
||||
suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List<DataToSign>): List<SignedData> {
|
||||
val auth = when (hotWalletId.authType) {
|
||||
HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth
|
||||
HotWalletId.AuthType.Password -> requestPassword(false)
|
||||
HotWalletId.AuthType.Biometry -> HotAuth.Biometry
|
||||
}
|
||||
|
||||
return runCatchingSdkErrors(hotWalletId, auth) {
|
||||
tangemHotSdk.signHashes(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = it,
|
||||
),
|
||||
dataToSign = dataToSign,
|
||||
).also {
|
||||
hotWalletPasswordRequester.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingSdkErrors(
|
||||
hotWalletId: HotWalletId,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T {
|
||||
return runCatchingWrongPassInternal(
|
||||
originalAuth = auth,
|
||||
auth = auth,
|
||||
block = { blockAuth ->
|
||||
block(blockAuth).also {
|
||||
// TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method
|
||||
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
|
||||
tangemHotSdk.changeAuth(
|
||||
unlockHotWallet = UnlockHotWallet(
|
||||
walletId = hotWalletId,
|
||||
auth = blockAuth,
|
||||
),
|
||||
auth = HotAuth.Biometry,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> runCatchingWrongPassInternal(
|
||||
originalAuth: HotAuth,
|
||||
auth: HotAuth,
|
||||
block: suspend (auth: HotAuth) -> T,
|
||||
): T = runCatching {
|
||||
block(auth)
|
||||
}.getOrElse { exception ->
|
||||
if (auth is HotAuth.Biometry && exception.isBiometryError()) {
|
||||
// fallback to password if biometry fails
|
||||
val passAuth = requestPassword(true)
|
||||
|
||||
return@getOrElse runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passAuth,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
if (exception !is WrongPasswordException) {
|
||||
throw exception
|
||||
}
|
||||
|
||||
// If the exception is a wrong password, we need to request the password again
|
||||
|
||||
hotWalletPasswordRequester.wrongPassword()
|
||||
val passResult = requestPassword(originalAuth is HotAuth.Biometry)
|
||||
|
||||
runCatchingWrongPassInternal(
|
||||
originalAuth = originalAuth,
|
||||
auth = passResult,
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun requestPassword(hasBiometry: Boolean): HotAuth {
|
||||
return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled()
|
||||
}
|
||||
|
||||
private fun Throwable.isBiometryError(): Boolean {
|
||||
return this is TangemSdkError.AuthenticationFailed ||
|
||||
this is TangemSdkError.AuthenticationCanceled ||
|
||||
this is TangemSdkError.AuthenticationLockout ||
|
||||
this is TangemSdkError.AuthenticationUnavailable ||
|
||||
this is TangemSdkError.AuthenticationAlreadyInProgress ||
|
||||
this is TangemSdkError.AuthenticationNotInitialized ||
|
||||
this is TangemSdkError.AuthenticationPermanentLockout
|
||||
}
|
||||
|
||||
private fun HotWalletPasswordRequester.Result.toAuth() = when (this) {
|
||||
HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry
|
||||
HotWalletPasswordRequester.Result.Dismiss -> null
|
||||
is HotWalletPasswordRequester.Result.EnteredPassword -> this.password
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
|
||||
interface HotWalletPasswordRequester {
|
||||
|
||||
suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
||||
class TangemHotSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == publicKey.seedKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey == signData.publicKey }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
package com.tangem.tap.domain.hot
|
||||
|
||||
import com.tangem.blockchain.common.TransactionSigner
|
||||
import com.tangem.blockchain.common.Wallet
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.map
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.hot.sdk.model.DataToSign
|
||||
import com.tangem.operations.sign.SignData
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import timber.log.Timber
|
||||
|
||||
class TangemHotWalletSigner @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet.Hot,
|
||||
private val hotWalletAccessor: HotWalletAccessor,
|
||||
) : TransactionSigner {
|
||||
|
||||
override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult<ByteArray> {
|
||||
return sign(listOf(hash), publicKey).map { it.first() }
|
||||
}
|
||||
|
||||
override suspend fun sign(
|
||||
hashes: List<ByteArray>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<List<ByteArray>> {
|
||||
val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = listOf(
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = hashes,
|
||||
derivationPath = publicKey.derivationPath,
|
||||
),
|
||||
),
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(result.map { it.signatures }.flatten())
|
||||
}
|
||||
|
||||
override suspend fun multiSign(
|
||||
dataToSign: List<SignData>,
|
||||
publicKey: Wallet.PublicKey,
|
||||
): CompletionResult<Map<ByteArray, ByteArray>> {
|
||||
val result = runCatching {
|
||||
hotWalletAccessor.signHashes(
|
||||
hotWalletId = userWallet.hotWalletId,
|
||||
dataToSign = dataToSign.map { signData ->
|
||||
val wallet =
|
||||
userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) }
|
||||
?: return CompletionResult.Failure(
|
||||
TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")),
|
||||
)
|
||||
|
||||
DataToSign(
|
||||
curve = wallet.curve,
|
||||
hashes = listOf(signData.hash),
|
||||
derivationPath = signData.derivationPath,
|
||||
)
|
||||
},
|
||||
)
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
return if (it is TangemSdkError) {
|
||||
CompletionResult.Failure(it)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.ExceptionError(it))
|
||||
}
|
||||
}
|
||||
|
||||
return CompletionResult.Success(
|
||||
result.mapIndexed { index, data ->
|
||||
dataToSign[index].publicKey to data.signatures.first()
|
||||
}.toMap(),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import com.tangem.crypto.hdWallet.DerivationPath
|
|||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import com.tangem.common.map
|
|||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.CardTypesResolver
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.tangem.blockchain.common.derivation.DerivationStyle
|
|||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.datasource.local.token.UserTokensResponseStore
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.useOldStyleDerivation
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
|
|
|
|||
|
|
@ -14,14 +14,14 @@ import com.tangem.common.tlv.Tlv
|
|||
import com.tangem.common.tlv.TlvDecoder
|
||||
import com.tangem.crypto.CryptoUtils
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.domain.card.DerivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isExcluded
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins
|
||||
import com.tangem.domain.card.common.TapWorkarounds.isVisa
|
||||
import com.tangem.domain.common.TwinsHelper
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.configs.CardConfig
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import com.tangem.common.extensions.toHexString
|
|||
import com.tangem.core.error.ext.tangemError
|
||||
import com.tangem.crypto.hdWallet.DerivationPath
|
||||
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
|
||||
import com.tangem.domain.card.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.wallets.derivations.derivationStyleProvider
|
||||
import com.tangem.domain.card.common.visa.VisaUtilities
|
||||
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility
|
||||
import com.tangem.domain.card.common.visa.VisaWalletPublicKeyUtility.findKeyWithoutDerivation
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ class DetailsMiddleware {
|
|||
deleteSavedAccessCodes()
|
||||
store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = false)
|
||||
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
||||
|
||||
return CompletionResult.Success(Unit)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,9 +87,10 @@ internal class CardSettingsModel @Inject constructor(
|
|||
|
||||
val userWallet = getUserWalletUseCase(userWalletId)
|
||||
.getOrElse { error("User wallet $userWalletId not found") }
|
||||
.requireColdWallet()
|
||||
|
||||
cardSdkConfigRepository.isBiometricsRequestPolicy =
|
||||
userWallet.requireColdWallet().scanResponse.card.isAccessCodeSet && // TODO [REDACTED_TASK_KEY]
|
||||
userWallet.scanResponse.card.isAccessCodeSet &&
|
||||
settingsRepository.shouldSaveAccessCodes()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ internal class ResetCardModel @Inject constructor(
|
|||
if (isLocked && userWalletsListManager.hasUserWallets) {
|
||||
store.dispatchNavigationAction { popTo<AppRoute.Welcome>() }
|
||||
} else {
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home) }
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Home()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,90 +0,0 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.arkivanov.essenty.lifecycle.subscribe
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.components.SystemBarsIconsDisposable
|
||||
import com.tangem.core.ui.utils.ChangeRootBackgroundColorEffect
|
||||
import com.tangem.core.ui.utils.findActivity
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.home.api.HomeComponent
|
||||
import com.tangem.tap.features.home.compose.StoriesScreen
|
||||
import com.tangem.tap.features.home.compose.StoriesScreenV2
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.store
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
internal class DefaultHomeComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
@Assisted params: Unit,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : HomeComponent, AppComponentContext by appComponentContext, StoreSubscriber<HomeState> {
|
||||
|
||||
private val model: HomeModel = getOrCreateModel()
|
||||
|
||||
private var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
|
||||
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
onCreate = {
|
||||
store.dispatch(HomeAction.OnCreate)
|
||||
},
|
||||
onStart = {
|
||||
store.subscribe(subscriber = this) { state ->
|
||||
state
|
||||
.skipRepeats { oldState, newState -> oldState.homeState == newState.homeState }
|
||||
.select(AppState::homeState)
|
||||
}
|
||||
},
|
||||
onStop = {
|
||||
store.unsubscribe(this)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content(modifier: Modifier) {
|
||||
val activity = LocalContext.current.findActivity()
|
||||
BackHandler(onBack = activity::finish)
|
||||
SystemBarsIconsDisposable(darkIcons = false)
|
||||
if (hotWalletFeatureToggles.isHotWalletEnabled) {
|
||||
StoriesScreenV2(
|
||||
homeState = homeState,
|
||||
onCreateNewWalletButtonClick = model::onCreateNewWalletScreen,
|
||||
onAddExistingWalletButtonClick = model::onAddExistingWalletScreen,
|
||||
onScanButtonClick = model::onScanClick,
|
||||
)
|
||||
} else {
|
||||
StoriesScreen(
|
||||
homeState = homeState,
|
||||
onScanButtonClick = model::onScanClick,
|
||||
onShopButtonClick = model::onShopClick,
|
||||
onSearchTokensClick = model::onSearchClick,
|
||||
)
|
||||
}
|
||||
|
||||
ChangeRootBackgroundColorEffect(Color(color = 0xFF010101))
|
||||
}
|
||||
|
||||
override fun newState(state: HomeState) {
|
||||
homeState.value = state
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : HomeComponent.Factory {
|
||||
override fun create(context: AppComponentContext, params: Unit): DefaultHomeComponent
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.google.firebase.analytics.ktx.analytics
|
||||
import com.google.firebase.ktx.Firebase
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRoute.ManageTokens.Source
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.navigation.Router
|
||||
import com.tangem.core.navigation.url.UrlOpener
|
||||
import com.tangem.domain.card.ScanCardProcessor
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.settings.usercountry.GetUserCountryUseCase
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.analytics.events.Shop
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.features.home.redux.HIDE_PROGRESS_DELAY
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.home.redux.HomeMiddleware.NEW_BUY_WALLET_URL
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Stable
|
||||
@ModelScoped
|
||||
internal class HomeModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val scanCardProcessor: ScanCardProcessor,
|
||||
private val saveWalletUseCase: SaveWalletUseCase,
|
||||
private val cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val urlOpener: UrlOpener,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
|
||||
private val router: Router,
|
||||
getUserCountryUseCase: GetUserCountryUseCase,
|
||||
) : Model() {
|
||||
|
||||
private val tangemErrorHandler = TangemTangemErrorsHandler(store)
|
||||
|
||||
init {
|
||||
getUserCountryUseCase.invoke()
|
||||
.distinctUntilChanged()
|
||||
.filterNotNull()
|
||||
.onEach {
|
||||
val userCountry = it.getOrNull() ?: UserCountry.Other(Locale.getDefault().country)
|
||||
store.dispatchOnMain(HomeAction.UserCountryLoaded(userCountry))
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(modelScope)
|
||||
}
|
||||
|
||||
fun onCreateNewWalletScreen() {
|
||||
router.push(AppRoute.CreateWalletSelection)
|
||||
}
|
||||
|
||||
fun onAddExistingWalletScreen() {
|
||||
router.push(AppRoute.AddExistingWallet)
|
||||
}
|
||||
|
||||
fun onScanClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonScanCard())
|
||||
scanCard()
|
||||
}
|
||||
|
||||
fun onShopClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonBuyCards())
|
||||
analyticsEventHandler.send(Shop.ScreenOpened())
|
||||
|
||||
Firebase.analytics.appInstanceId
|
||||
.addOnSuccessListener { urlOpener.openUrl(url = "$NEW_BUY_WALLET_URL&app_instance_id=$it") }
|
||||
.addOnFailureListener { urlOpener.openUrl(url = NEW_BUY_WALLET_URL) }
|
||||
}
|
||||
|
||||
fun onSearchClick() {
|
||||
analyticsEventHandler.send(IntroductionProcess.ButtonTokensList())
|
||||
|
||||
store.dispatch(TokensAction.SetArgs.ReadAccess)
|
||||
store.dispatchNavigationAction { push(AppRoute.ManageTokens(Source.STORIES)) }
|
||||
}
|
||||
|
||||
private fun scanCard() {
|
||||
modelScope.launch {
|
||||
cardSdkConfigRepository.isBiometricsRequestPolicy = settingsRepository.shouldSaveAccessCodes()
|
||||
|
||||
scanCardProcessor.scan(
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Intro,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
|
||||
} else {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
tangemErrorHandler.onErrorReceived(error = it)
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
},
|
||||
onSuccess = ::proceedWithScanResponse,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse) {
|
||||
val userWallet = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build()
|
||||
|
||||
if (userWallet == null) {
|
||||
Timber.e("User wallet not created")
|
||||
return
|
||||
}
|
||||
|
||||
saveWalletUseCase(userWallet).fold(
|
||||
ifLeft = { Timber.e(it.toString(), "Unable to save user wallet") },
|
||||
ifRight = {
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
coroutineScope { store.onUserWalletSelected(userWallet = userWallet) }
|
||||
},
|
||||
)
|
||||
|
||||
store.dispatchWithMain(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(value = scanResponse.cardTypesResolver)
|
||||
|
||||
if (currency != null) {
|
||||
Analytics.send(
|
||||
event = Basic.SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = Basic.SignedIn.SignInType.Card,
|
||||
walletsCount = "1",
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface RegionProvider {
|
||||
fun getRegion(): String?
|
||||
}
|
||||
|
||||
class LocaleRegionProvider : RegionProvider {
|
||||
override fun getRegion(): String = Locale.current.region
|
||||
}
|
||||
|
||||
const val RUSSIA_COUNTRY_CODE = "ru"
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.tap.features.home
|
||||
|
||||
import com.tangem.blockchain.common.BlockchainError
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.home.errors.TangemSdkErrorHandler
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
class TangemTangemErrorsHandler(val store: Store<AppState>) : TangemSdkErrorHandler {
|
||||
|
||||
override fun onErrorReceived(error: TangemError) {
|
||||
when (error) {
|
||||
is TangemSdkError -> {
|
||||
handleCardSdkError(error)
|
||||
}
|
||||
is BlockchainError -> {
|
||||
handleBlockchainSdkError(error)
|
||||
}
|
||||
else -> {
|
||||
Timber.e("Error happened", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCardSdkError(error: TangemSdkError) {
|
||||
when (error) {
|
||||
is TangemSdkError.NfcFeatureIsUnavailable -> {
|
||||
store.dispatchOnMain(GlobalAction.ShowDialog(StateDialog.NfcFeatureIsUnavailable))
|
||||
}
|
||||
else -> {
|
||||
Timber.e(error, "Unable to scan card")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleBlockchainSdkError(error: TangemError) {
|
||||
Timber.e("Sdk error happened", error)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.tap.features.home.api
|
||||
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
||||
interface HomeComponent : ComposableContentComponent {
|
||||
|
||||
interface Factory : ComponentFactory<Unit, HomeComponent>
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.tap.common.compose.extensions.AnimatedValue
|
||||
import com.tangem.tap.common.compose.extensions.toAnimatable
|
||||
|
||||
private const val SCALE_SWITCH_BARRIER = 1.15f
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun HorizontalSlidingImage(
|
||||
painter: Painter,
|
||||
paused: Boolean,
|
||||
duration: Int,
|
||||
itemSize: DpSize,
|
||||
startOffset: Float,
|
||||
targetOffset: Float,
|
||||
contentDescription: String,
|
||||
) {
|
||||
val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f)
|
||||
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.requiredWidth(itemSize.width)
|
||||
.requiredHeight(itemSize.height)
|
||||
.graphicsLayer(
|
||||
translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value,
|
||||
),
|
||||
alignment = Alignment.TopStart,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
painter = painter,
|
||||
contentDescription = contentDescription,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesTextAnimation(
|
||||
slideInDuration: Int = 500,
|
||||
slideInDelay: Int = 200,
|
||||
slideDistance: Dp = 60.dp,
|
||||
label: String = "",
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
val isLaunched = remember { mutableStateOf(false) }
|
||||
val transition = updateTransition(targetState = isLaunched.value, label = label)
|
||||
|
||||
val offsetY = transition.animateDp(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = slideInDuration,
|
||||
delayMillis = slideInDelay,
|
||||
easing = FastOutSlowInEasing,
|
||||
)
|
||||
},
|
||||
label = "Slide in",
|
||||
) { value -> if (value) 0.dp else slideDistance }
|
||||
|
||||
val alpha = transition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = slideInDuration * 2,
|
||||
delayMillis = slideInDelay,
|
||||
easing = FastOutSlowInEasing,
|
||||
)
|
||||
},
|
||||
label = "Visibility",
|
||||
) { value -> if (value) 1f else 0f }
|
||||
|
||||
content(
|
||||
Modifier
|
||||
.absoluteOffset(y = offsetY.value)
|
||||
.alpha(alpha.value),
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) { isLaunched.value = true }
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesBottomImageAnimation(
|
||||
initialScale: Float = 2.5f,
|
||||
secondStageScale: Float = SCALE_SWITCH_BARRIER,
|
||||
targetScale: Float = 1.0f,
|
||||
firstStepDuration: Int,
|
||||
totalDuration: Int,
|
||||
content: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
val secondStepDuration = totalDuration - firstStepDuration
|
||||
|
||||
val isFirstStepLaunched = remember { mutableStateOf(false) }
|
||||
val isSecondStepLaunched = remember { mutableStateOf(false) }
|
||||
|
||||
val firstTransition = updateTransition(
|
||||
targetState = isFirstStepLaunched.value,
|
||||
label = "Image appearing",
|
||||
)
|
||||
val firstScaleStep = firstTransition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = firstStepDuration,
|
||||
easing = FastOutLinearInEasing,
|
||||
)
|
||||
},
|
||||
label = "Appearing scale",
|
||||
) { value -> if (value) secondStageScale else initialScale }
|
||||
|
||||
val secondTransition = updateTransition(
|
||||
targetState = isSecondStepLaunched.value,
|
||||
label = "Image slow outgoing",
|
||||
)
|
||||
val secondScaleStep = secondTransition.animateFloat(
|
||||
transitionSpec = {
|
||||
tween(
|
||||
durationMillis = secondStepDuration,
|
||||
easing = LinearEasing,
|
||||
)
|
||||
},
|
||||
label = "Outgoing scale",
|
||||
) { value -> if (value) targetScale else secondStageScale }
|
||||
|
||||
val fadeIn = firstTransition.animateFloat(
|
||||
transitionSpec = { tween(durationMillis = 400) },
|
||||
label = "Fade in on start",
|
||||
) { value -> if (value) 1f else 0f }
|
||||
|
||||
if (firstScaleStep.value == secondStageScale) {
|
||||
isSecondStepLaunched.value = true
|
||||
}
|
||||
|
||||
val modifier = if (!isSecondStepLaunched.value) {
|
||||
Modifier.scale(firstScaleStep.value)
|
||||
} else {
|
||||
Modifier.scale(secondScaleStep.value)
|
||||
}.alpha(fadeIn.value)
|
||||
|
||||
content(modifier)
|
||||
|
||||
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
|
||||
}
|
||||
|
|
@ -1,266 +0,0 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.tap.features.home.compose
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.tap.features.home.compose.content.*
|
||||
import com.tangem.tap.features.home.compose.views.HomeButtons
|
||||
import com.tangem.tap.features.home.compose.views.SearchCurrenciesButton
|
||||
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.home.redux.Stories
|
||||
import com.tangem.wallet.R
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal fun StoriesScreen(
|
||||
homeState: MutableState<HomeState>,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
onSearchTokensClick: () -> Unit,
|
||||
) {
|
||||
val state = homeState.value
|
||||
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
|
||||
|
||||
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
|
||||
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
|
||||
}
|
||||
val goToNextStory = remember(currentStory, currentStoryIndex) {
|
||||
{
|
||||
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
|
||||
state.stories[currentStoryIndex + 1]
|
||||
} else {
|
||||
state.firstStory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
StoriesScreenContent(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
|
||||
config = StoriesScreenContentConfig(
|
||||
storiesSize = state.stories.lastIndex,
|
||||
currentStoryIndex = currentStoryIndex,
|
||||
currentStory = currentStory,
|
||||
isScanInProgress = homeState.value.scanInProgress,
|
||||
onGoToPreviousStory = goToPreviousStory,
|
||||
onGoToNextStory = goToNextStory,
|
||||
onSearchTokensClick = onSearchTokensClick,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
onShopButtonClick = onShopButtonClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use StoriesContainer from core/ui")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: Modifier = Modifier) {
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
|
||||
val isPaused = isPressed || config.isScanInProgress
|
||||
val currentStoryDuration = config.currentStory.duration
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color(0xFF010101)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToPreviousStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToNextStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
StoriesProgressBar(
|
||||
steps = config.storiesSize,
|
||||
currentStep = config.currentStoryIndex,
|
||||
stepDuration = currentStoryDuration,
|
||||
paused = isPaused,
|
||||
onStepFinish = config.onGoToNextStory,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_tangem_logo),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.height(TangemTheme.dimens.size18)
|
||||
.align(Alignment.Start),
|
||||
)
|
||||
when (config.currentStory) {
|
||||
Stories.TangemIntro -> FirstStoriesContent(
|
||||
isPaused = isPaused,
|
||||
duration = currentStoryDuration,
|
||||
)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = currentStoryDuration,
|
||||
)
|
||||
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
|
||||
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
|
||||
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = config.currentStory == Stories.Currencies,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
SearchCurrenciesButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = config.onSearchTokensClick,
|
||||
)
|
||||
}
|
||||
|
||||
HomeButtons(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
btnScanStateInProgress = config.isScanInProgress,
|
||||
onScanButtonClick = config.onScanButtonClick,
|
||||
onShopButtonClick = config.onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class StoriesScreenContentConfig(
|
||||
val storiesSize: Int,
|
||||
val currentStoryIndex: Int,
|
||||
val currentStory: Stories,
|
||||
val isScanInProgress: Boolean,
|
||||
val onGoToPreviousStory: () -> Unit = {},
|
||||
val onGoToNextStory: () -> Unit = {},
|
||||
val onSearchTokensClick: () -> Unit = {},
|
||||
val onScanButtonClick: () -> Unit = {},
|
||||
val onShopButtonClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun StoriesScreenContentPreview(
|
||||
@PreviewParameter(StoriesScreenContentConfigProvider::class) config: StoriesScreenContentConfig,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StoriesScreenContent(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class StoriesScreenContentConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentConfig>(
|
||||
collection = listOf(
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 0,
|
||||
currentStory = Stories.TangemIntro,
|
||||
isScanInProgress = true,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 1,
|
||||
currentStory = Stories.RevolutionaryWallet,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 2,
|
||||
currentStory = Stories.UltraSecureBackup,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 3,
|
||||
currentStory = Stories.Currencies,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 4,
|
||||
currentStory = Stories.Web3,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentConfig(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 5,
|
||||
currentStory = Stories.WalletForEveryone,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.tap.features.home.compose
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.gestures.detectTapGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.tap.features.home.compose.content.*
|
||||
import com.tangem.tap.features.home.compose.views.HomeButtonsV2
|
||||
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
|
||||
import com.tangem.tap.features.home.redux.HomeState
|
||||
import com.tangem.tap.features.home.redux.Stories
|
||||
import com.tangem.wallet.R
|
||||
import kotlin.math.max
|
||||
|
||||
@Composable
|
||||
internal fun StoriesScreenV2(
|
||||
homeState: MutableState<HomeState>,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
onScanButtonClick: () -> Unit,
|
||||
) {
|
||||
val state = homeState.value
|
||||
|
||||
var currentStory by remember { mutableStateOf(state.firstStory) }
|
||||
val currentStoryIndex by rememberUpdatedState(newValue = state.stepOf(currentStory))
|
||||
|
||||
val goToPreviousStory = remember(currentStory, currentStoryIndex) {
|
||||
{ currentStory = state.stories[max(0, currentStoryIndex - 1)] }
|
||||
}
|
||||
val goToNextStory = remember(currentStory, currentStoryIndex) {
|
||||
{
|
||||
currentStory = if (currentStoryIndex < state.stories.lastIndex) {
|
||||
state.stories[currentStoryIndex + 1]
|
||||
} else {
|
||||
state.firstStory
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
StoriesScreenContentV2(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag(StoriesScreenTestTags.SCREEN_CONTAINER),
|
||||
config = StoriesScreenContentV2Config(
|
||||
storiesSize = state.stories.lastIndex,
|
||||
currentStoryIndex = currentStoryIndex,
|
||||
currentStory = currentStory,
|
||||
isScanInProgress = homeState.value.scanInProgress,
|
||||
onGoToPreviousStory = goToPreviousStory,
|
||||
onGoToNextStory = goToNextStory,
|
||||
onCreateNewWalletButtonClick = onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = onAddExistingWalletButtonClick,
|
||||
onScanButtonClick = onScanButtonClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Deprecated("Use StoriesContainer from core/ui")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StoriesScreenContentV2(config: StoriesScreenContentV2Config, modifier: Modifier = Modifier) {
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
|
||||
val isPaused = isPressed || config.isScanInProgress
|
||||
val currentStoryDuration = config.currentStory.duration
|
||||
|
||||
Box(
|
||||
modifier = modifier.background(Color(0xFF010101)),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToPreviousStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.pointerInput(Unit) {
|
||||
detectTapGestures(
|
||||
onPress = {
|
||||
val pressStartTime = System.currentTimeMillis()
|
||||
isPressed = true
|
||||
this.tryAwaitRelease()
|
||||
val pressEndTime = System.currentTimeMillis()
|
||||
val totalPressTime = pressEndTime - pressStartTime
|
||||
if (totalPressTime < 200) config.onGoToNextStory()
|
||||
isPressed = false
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
StoriesProgressBar(
|
||||
steps = config.storiesSize,
|
||||
currentStep = config.currentStoryIndex,
|
||||
stepDuration = currentStoryDuration,
|
||||
paused = isPaused,
|
||||
onStepFinish = config.onGoToNextStory,
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_tangem_logo),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
)
|
||||
.height(TangemTheme.dimens.size18)
|
||||
.align(Alignment.Start),
|
||||
)
|
||||
when (config.currentStory) {
|
||||
Stories.TangemIntro -> FirstStoriesContent(
|
||||
isPaused = isPaused,
|
||||
duration = currentStoryDuration,
|
||||
)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = currentStoryDuration,
|
||||
)
|
||||
Stories.Currencies -> StoriesCurrencies(isPaused, currentStoryDuration)
|
||||
Stories.Web3 -> StoriesWeb3(isPaused, currentStoryDuration)
|
||||
Stories.WalletForEveryone -> StoriesWalletForEveryone(currentStoryDuration)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = TangemTheme.dimens.spacing16)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
btnScanStateInProgress = config.isScanInProgress,
|
||||
onScanButtonClick = config.onScanButtonClick,
|
||||
onCreateNewWalletButtonClick = config.onCreateNewWalletButtonClick,
|
||||
onAddExistingWalletButtonClick = config.onAddExistingWalletButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class StoriesScreenContentV2Config(
|
||||
val storiesSize: Int,
|
||||
val currentStoryIndex: Int,
|
||||
val currentStory: Stories,
|
||||
val isScanInProgress: Boolean,
|
||||
val onGoToPreviousStory: () -> Unit = {},
|
||||
val onGoToNextStory: () -> Unit = {},
|
||||
val onCreateNewWalletButtonClick: () -> Unit = {},
|
||||
val onAddExistingWalletButtonClick: () -> Unit = {},
|
||||
val onScanButtonClick: () -> Unit = {},
|
||||
)
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun StoriesScreenContentV2Preview(
|
||||
@PreviewParameter(StoriesScreenContentV2ConfigProvider::class) config: StoriesScreenContentV2Config,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
StoriesScreenContentV2(config = config)
|
||||
}
|
||||
}
|
||||
|
||||
private class StoriesScreenContentV2ConfigProvider : CollectionPreviewParameterProvider<StoriesScreenContentV2Config>(
|
||||
collection = listOf(
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 0,
|
||||
currentStory = Stories.TangemIntro,
|
||||
isScanInProgress = true,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 1,
|
||||
currentStory = Stories.RevolutionaryWallet,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 2,
|
||||
currentStory = Stories.UltraSecureBackup,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 3,
|
||||
currentStory = Stories.Currencies,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 4,
|
||||
currentStory = Stories.Web3,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
StoriesScreenContentV2Config(
|
||||
storiesSize = 6,
|
||||
currentStoryIndex = 5,
|
||||
currentStory = Stories.WalletForEveryone,
|
||||
isScanInProgress = false,
|
||||
),
|
||||
),
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.content
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerH16
|
||||
import com.tangem.core.ui.components.SpacerH32
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
|
||||
import com.tangem.tap.features.home.compose.StoriesTextAnimation
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun StoriesRevolutionaryWallet() {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_awe_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_awe_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
StoriesImage(
|
||||
modifier = Modifier,
|
||||
drawableResId = R.drawable.img_revolutionary_wallet,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_backup_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_backup_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
FloatingCardsContent(
|
||||
isPaused = isPaused,
|
||||
stepDuration = stepDuration,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_currencies_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_currencies_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_web3_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_web3_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH(TangemTheme.dimens.spacing70)
|
||||
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StoriesWalletForEveryone(stepDuration: Int) {
|
||||
SplitContent(
|
||||
topContent = {
|
||||
TopContent(
|
||||
titleText = stringResourceSafe(id = R.string.story_finish_title),
|
||||
subtitleText = stringResourceSafe(id = R.string.story_finish_description),
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
SpacerH32()
|
||||
BoxWithGradient {
|
||||
StoriesBottomImageAnimation(
|
||||
initialScale = 2.6f,
|
||||
secondStageScale = 1.2f,
|
||||
targetScale = 1.1f,
|
||||
totalDuration = stepDuration,
|
||||
firstStepDuration = 500,
|
||||
) { modifier ->
|
||||
StoriesImage(
|
||||
modifier = modifier,
|
||||
drawableResId = R.drawable.img_tangem_for_everyone,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SplitContent(topContent: @Composable () -> Unit, bottomContent: @Composable () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Top,
|
||||
) {
|
||||
topContent()
|
||||
bottomContent()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TopContent(titleText: String, subtitleText: String) {
|
||||
SpacerH(TangemTheme.dimens.spacing36)
|
||||
StoriesTitleText(
|
||||
text = titleText,
|
||||
)
|
||||
SpacerH16()
|
||||
StoriesSubtitleText(
|
||||
subtitleText = subtitleText,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun StoriesTitleText(text: String) {
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 150,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(start = 40.dp, end = 40.dp),
|
||||
text = text,
|
||||
style = TangemTheme.typography.head,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
private fun StoriesSubtitleText(subtitleText: String) {
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 400,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier
|
||||
.padding(start = 40.dp, end = 40.dp),
|
||||
text = subtitleText,
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemColorPalette.Dark1,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StoriesImage(@DrawableRes drawableResId: Int, modifier: Modifier = Modifier) {
|
||||
Image(
|
||||
painter = painterResource(id = drawableResId),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Inside,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RevolutionaryWalletPreview() {
|
||||
StoriesRevolutionaryWallet()
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun UltraSecureBackupPreview() {
|
||||
StoriesUltraSecureBackup(
|
||||
false,
|
||||
6000,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun CurrenciesPreview() {
|
||||
StoriesCurrencies(false, 6000)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Web3Preview() {
|
||||
StoriesWeb3(false, 6000)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun WalletForEveryonePreview() {
|
||||
StoriesWalletForEveryone(6000)
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.content
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.SpacerH12
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.common.compose.extensions.dpSize
|
||||
import com.tangem.tap.common.compose.extensions.halfHeight
|
||||
import com.tangem.tap.common.compose.extensions.toPx
|
||||
import com.tangem.tap.common.extensions.isEven
|
||||
import com.tangem.tap.features.home.compose.HorizontalSlidingImage
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
fun StoriesCurrenciesContent(paused: Boolean, duration: Int) {
|
||||
val currencyDrawableList = remember {
|
||||
listOf(
|
||||
R.drawable.currency0,
|
||||
R.drawable.currency1,
|
||||
R.drawable.currency2,
|
||||
R.drawable.currency3,
|
||||
R.drawable.currency4,
|
||||
)
|
||||
}
|
||||
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val decreaseRate = remember { 1f / currencyDrawableList.size }
|
||||
val designItemHeight = remember { 82.dp }
|
||||
|
||||
BoxWithGradient {
|
||||
Column(modifier = Modifier.graphicsLayer(clip = false)) {
|
||||
currencyDrawableList.forEachIndexed { index, drawableResId ->
|
||||
val painter = painterResource(id = drawableResId)
|
||||
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
|
||||
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
|
||||
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
|
||||
|
||||
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
|
||||
val animateFrom = chessOffset - moveItemToStartOfScreen
|
||||
val animateTo = 50.dp - 50.dp * index * decreaseRate
|
||||
|
||||
HorizontalSlidingImage(
|
||||
paused = paused,
|
||||
duration = duration,
|
||||
painter = painter,
|
||||
itemSize = scaledItemSize,
|
||||
startOffset = animateFrom.toPx(),
|
||||
targetOffset = animateTo.toPx(),
|
||||
contentDescription = "Currency row",
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
fun StoriesWeb3Content(paused: Boolean, duration: Int) {
|
||||
val dappsItemList = remember {
|
||||
listOf(
|
||||
R.drawable.dapps1,
|
||||
R.drawable.dapps1,
|
||||
R.drawable.dapps2,
|
||||
R.drawable.dapps3,
|
||||
R.drawable.dapps4,
|
||||
R.drawable.dapps5,
|
||||
)
|
||||
}
|
||||
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||
val decreaseRate = remember { 1f / dappsItemList.size }
|
||||
val designItemHeight = 75.dp
|
||||
|
||||
BoxWithGradient {
|
||||
Column(modifier = Modifier.graphicsLayer(clip = false)) {
|
||||
dappsItemList.forEachIndexed { index, drawableResId ->
|
||||
val painter = painterResource(id = drawableResId)
|
||||
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
|
||||
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
|
||||
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
|
||||
|
||||
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
|
||||
val animateFrom = chessOffset - moveItemToStartOfScreen
|
||||
val animateTo = 70.dp - 70.dp * index * decreaseRate
|
||||
|
||||
HorizontalSlidingImage(
|
||||
paused = paused,
|
||||
duration = duration,
|
||||
painter = painter,
|
||||
itemSize = scaledItemSize,
|
||||
startOffset = animateFrom.toPx(),
|
||||
targetOffset = animateTo.toPx(),
|
||||
contentDescription = "Web3 row",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun BoxWithGradient(content: @Composable () -> Unit) {
|
||||
val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
content()
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size164 + bottomInsetsPx.dp)
|
||||
.background(BottomGradient),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
|
||||
val scaleRate = itemSize.height / designItemHeight
|
||||
return itemSize / scaleRate
|
||||
}
|
||||
|
||||
private val BottomGradient: Brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
TangemColorPalette.Black.copy(alpha = 0f),
|
||||
TangemColorPalette.Black.copy(alpha = 0.75f),
|
||||
TangemColorPalette.Black.copy(alpha = 0.95f),
|
||||
TangemColorPalette.Black,
|
||||
),
|
||||
)
|
||||
|
|
@ -1,93 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.content
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.home.compose.StoriesTextAnimation
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Suppress("LongMethod", "ComplexMethod", "MagicNumber")
|
||||
@Composable
|
||||
fun FirstStoriesContent(isPaused: Boolean, duration: Int) {
|
||||
val progress = remember { Animatable(0f) }
|
||||
|
||||
LaunchedEffect(isPaused) {
|
||||
if (isPaused) {
|
||||
progress.stop()
|
||||
} else {
|
||||
progress.animateTo(
|
||||
targetValue = 2f,
|
||||
animationSpec = tween(
|
||||
durationMillis = duration,
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val style = TextStyle(
|
||||
fontSize = 46.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
SpacerH(TangemTheme.dimens.spacing94)
|
||||
StoriesTextAnimation(
|
||||
slideInDuration = 500,
|
||||
slideInDelay = 150,
|
||||
) { modifier ->
|
||||
Text(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(R.string.story_meet_title),
|
||||
style = style,
|
||||
color = TangemColorPalette.White,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
SpacerH(TangemTheme.dimens.spacing46)
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
painter = painterResource(R.drawable.img_meet_tangem),
|
||||
contentScale = ContentScale.Inside,
|
||||
contentDescription = "Tangem Wallet card",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun FirstStoriesPreview() {
|
||||
FirstStoriesContent(
|
||||
false,
|
||||
8000,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.content
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import com.tangem.tap.common.compose.extensions.AnimatedValue
|
||||
import com.tangem.tap.common.compose.extensions.asImageBitmap
|
||||
import com.tangem.tap.common.compose.extensions.toAnimatable
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
fun FloatingCardsContent(isPaused: Boolean, stepDuration: Int) {
|
||||
val imageBitmap = asImageBitmap(R.drawable.img_card_placeholder_wallet_2)
|
||||
val cards = listOf(
|
||||
FloatingCard.first(),
|
||||
FloatingCard.second(),
|
||||
FloatingCard.third(),
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
cards.forEach { floatingCard ->
|
||||
FloatingCard.Item(
|
||||
isPaused = isPaused,
|
||||
imageBitmap = imageBitmap,
|
||||
cardValues = floatingCard,
|
||||
stepDuration = stepDuration,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class CardValues(
|
||||
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
|
||||
val scale: AnimatedValue = AnimatedValue(1f, 1f),
|
||||
)
|
||||
|
||||
private object FloatingCard {
|
||||
|
||||
@Suppress("TopLevelComposableFunctions")
|
||||
@Composable
|
||||
fun Item(isPaused: Boolean, stepDuration: Int, imageBitmap: ImageBitmap, cardValues: CardValues) {
|
||||
Image(
|
||||
bitmap = imageBitmap,
|
||||
contentDescription = "Floating Tangem card",
|
||||
modifier = Modifier
|
||||
.graphicsLayer(
|
||||
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
|
||||
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
|
||||
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
|
||||
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
|
||||
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun first(): CardValues = CardValues(
|
||||
translateX = -400f to -350f,
|
||||
translateY = 30f to 32f,
|
||||
rotationX = 10f to 15f,
|
||||
rotationY = 15f to 15f,
|
||||
rotationZ = 40f to 27f,
|
||||
scale = 0.6f to 0.6f,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun second(): CardValues = CardValues(
|
||||
translateX = 350f to 300f,
|
||||
translateY = -70f to 0f,
|
||||
rotationX = 30f to 48f,
|
||||
rotationY = 0f to 5f,
|
||||
rotationZ = -34f to -42f,
|
||||
scale = 0.47f to 0.35f,
|
||||
)
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun third(): CardValues = CardValues(
|
||||
translateX = 320f to 250f,
|
||||
translateY = 500f to 500f,
|
||||
rotationX = 0f to 3f,
|
||||
rotationY = 10f to 10f,
|
||||
rotationZ = -45f to -30f,
|
||||
scale = 0.6f to 0.75f,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerW12
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun HomeButtons(
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onShopButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
modifier = modifier,
|
||||
) {
|
||||
ScanCardButton(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
|
||||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
)
|
||||
SpacerW12()
|
||||
OrderCardButton(
|
||||
modifier = Modifier
|
||||
.weight(weight = 1f)
|
||||
.testTag(StoriesScreenTestTags.ORDER_BUTTON),
|
||||
onClick = onShopButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = false,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrderCardButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_order),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun HomeButtonsPreview(@PreviewParameter(HomeButtonsParameterProvider::class) state: HomeButtonsState) {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtons(
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onScanButtonClick = {},
|
||||
onShopButtonClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HomeButtonsParameterProvider : CollectionPreviewParameterProvider<HomeButtonsState>(
|
||||
collection = listOf(
|
||||
HomeButtonsState(
|
||||
btnScanStateInProgress = false,
|
||||
),
|
||||
HomeButtonsState(
|
||||
btnScanStateInProgress = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private data class HomeButtonsState(
|
||||
val btnScanStateInProgress: Boolean,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.test.StoriesScreenTestTags
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun HomeButtonsV2(
|
||||
btnScanStateInProgress: Boolean,
|
||||
onScanButtonClick: () -> Unit,
|
||||
onCreateNewWalletButtonClick: () -> Unit,
|
||||
onAddExistingWalletButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CreateNewWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.CREATE_NEW_WALLET_BUTTON),
|
||||
onClick = onCreateNewWalletButtonClick,
|
||||
)
|
||||
AddExistingWalletButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.ADD_EXISTING_WALLET_BUTTON),
|
||||
onClick = onAddExistingWalletButtonClick,
|
||||
)
|
||||
ScanCardButton(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.testTag(StoriesScreenTestTags.SCAN_BUTTON),
|
||||
showProgress = btnScanStateInProgress,
|
||||
onClick = onScanButtonClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CreateNewWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_create_new_wallet),
|
||||
useDarkerColors = false,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddExistingWalletButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_add_existing_wallet),
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScanCardButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.home_button_scan),
|
||||
useDarkerColors = true,
|
||||
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_tangem_24),
|
||||
onClick = onClick,
|
||||
showProgress = showProgress,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun HomeButtonsV2Preview(@PreviewParameter(HomeButtonsV2ParameterProvider::class) state: HomeButtonsV2State) {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier.background(Color.Black),
|
||||
) {
|
||||
HomeButtonsV2(
|
||||
btnScanStateInProgress = state.btnScanStateInProgress,
|
||||
onCreateNewWalletButtonClick = {},
|
||||
onAddExistingWalletButtonClick = {},
|
||||
onScanButtonClick = {},
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class HomeButtonsV2ParameterProvider : CollectionPreviewParameterProvider<HomeButtonsV2State>(
|
||||
collection = listOf(
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = false,
|
||||
),
|
||||
HomeButtonsV2State(
|
||||
btnScanStateInProgress = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
private data class HomeButtonsV2State(
|
||||
val btnScanStateInProgress: Boolean,
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.extensions.stringResourceSafe
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.wallet.R
|
||||
|
||||
@Composable
|
||||
internal fun SearchCurrenciesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
StoriesButton(
|
||||
modifier = modifier,
|
||||
text = stringResourceSafe(id = R.string.common_search_tokens),
|
||||
icon = TangemButtonIconPosition.Start(R.drawable.ic_search_24),
|
||||
showProgress = false,
|
||||
useDarkerColors = true,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Composable
|
||||
private fun SearchCurrenciesButtonPreview() {
|
||||
TangemThemePreview {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(color = Color.Black)
|
||||
.padding(all = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
SearchCurrenciesButton(modifier = Modifier.fillMaxWidth(), onClick = {})
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion Preview
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import androidx.compose.material3.ButtonColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButton
|
||||
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
internal fun StoriesButton(
|
||||
text: String,
|
||||
useDarkerColors: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
icon: TangemButtonIconPosition = TangemButtonIconPosition.None,
|
||||
showProgress: Boolean = false,
|
||||
) {
|
||||
TangemButton(
|
||||
modifier = modifier,
|
||||
text = text,
|
||||
icon = icon,
|
||||
colors = if (useDarkerColors) DarkerButtonColors else LighterButtonColors,
|
||||
showProgress = showProgress,
|
||||
enabled = true,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
textStyle = TangemTheme.typography.subtitle1,
|
||||
iconPadding = when (icon) {
|
||||
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing4
|
||||
is TangemButtonIconPosition.End,
|
||||
is TangemButtonIconPosition.None,
|
||||
-> TangemTheme.dimens.spacing8
|
||||
},
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
private val LighterButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Light4,
|
||||
contentColor = TangemColorPalette.Dark6,
|
||||
disabledContainerColor = TangemColorPalette.Dark5,
|
||||
disabledContentColor = TangemColorPalette.Dark6,
|
||||
)
|
||||
|
||||
private val DarkerButtonColors: ButtonColors = ButtonColors(
|
||||
containerColor = TangemColorPalette.Dark4,
|
||||
contentColor = TangemColorPalette.White,
|
||||
disabledContainerColor = TangemColorPalette.Dark4,
|
||||
disabledContentColor = TangemColorPalette.White,
|
||||
)
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
package com.tangem.tap.features.home.compose.views
|
||||
|
||||
import android.provider.Settings
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.components.SpacerW4
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
private const val STORIES_ANIMATION_SPEED_ZERO_DURATION = 3000L
|
||||
|
||||
@Composable
|
||||
fun StoriesProgressBar(
|
||||
steps: Int,
|
||||
currentStep: Int,
|
||||
paused: Boolean = false,
|
||||
stepDuration: Int = 8_000,
|
||||
onStepFinish: () -> Unit = {},
|
||||
) {
|
||||
val progress = remember(currentStep) { Animatable(initialValue = 0f) }
|
||||
|
||||
val context = LocalContext.current
|
||||
val animatorSpeed = Settings.Global.getFloat(
|
||||
context.contentResolver,
|
||||
Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f,
|
||||
)
|
||||
|
||||
LaunchedEffect(paused, currentStep, animatorSpeed) {
|
||||
if (paused) {
|
||||
progress.stop()
|
||||
} else {
|
||||
if (animatorSpeed == 0f) {
|
||||
progress.snapTo(1f)
|
||||
delay(STORIES_ANIMATION_SPEED_ZERO_DURATION)
|
||||
} else {
|
||||
progress.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
|
||||
easing = LinearEasing,
|
||||
),
|
||||
)
|
||||
progress.snapTo(0f)
|
||||
}
|
||||
onStepFinish()
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing16,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
top = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
for (index in 0..steps) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(TangemTheme.dimens.size2)
|
||||
.weight(1f)
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(TangemColorPalette.White.copy(alpha = .2f)),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(TangemTheme.dimens.radius2))
|
||||
.background(TangemColorPalette.White)
|
||||
.fillMaxHeight()
|
||||
.let {
|
||||
when (index) {
|
||||
currentStep -> it.fillMaxWidth(progress.value)
|
||||
in 0..currentStep -> it.fillMaxWidth(fraction = 1f)
|
||||
else -> it
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
if (index != steps) {
|
||||
SpacerW4()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun StoriesProgressBarPreview() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.wrapContentSize()
|
||||
.background(TangemColorPalette.Black)
|
||||
.padding(vertical = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
StoriesProgressBar(steps = 5, currentStep = 3, paused = false)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.tap.features.home.di
|
||||
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.tap.features.home.DefaultHomeComponent
|
||||
import com.tangem.tap.features.home.HomeModel
|
||||
import com.tangem.tap.features.home.api.HomeComponent
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.ClassKey
|
||||
import dagger.multibindings.IntoMap
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal interface HomeFeatureModule {
|
||||
|
||||
@Binds
|
||||
fun bindFactory(impl: DefaultHomeComponent.Factory): HomeComponent.Factory
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ClassKey(HomeModel::class)
|
||||
fun bindModel(model: HomeModel): Model
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package com.tangem.tap.features.home.errors
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
|
||||
interface TangemSdkErrorHandler {
|
||||
|
||||
fun onErrorReceived(error: TangemError)
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.domain.settings.usercountry.models.UserCountry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class HomeAction : Action {
|
||||
|
||||
data object OnCreate : HomeAction()
|
||||
|
||||
/**
|
||||
* Action for scanning card
|
||||
*
|
||||
* @property scope lifecycle scope. It will be canceled when lifecycle-aware component is destroyed
|
||||
*/
|
||||
data class ReadCard(val scope: CoroutineScope) : HomeAction()
|
||||
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
|
||||
data class UserCountryLoaded(val userCountry: UserCountry) : HomeAction()
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import android.content.res.Resources
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnResult
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.card.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.IntroductionProcess
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.eraseContext
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
import java.util.Locale
|
||||
|
||||
internal const val HIDE_PROGRESS_DELAY = 400L
|
||||
|
||||
object HomeMiddleware {
|
||||
val handler = homeMiddleware
|
||||
|
||||
private val SYSTEM_LANGUAGE =
|
||||
runCatching { Resources.getSystem().configuration.locales[0].language }.getOrElse { "" }
|
||||
private val APP_LANGUAGE = Locale.getDefault().language
|
||||
private val UTM_MARKS = "utm_source=tangem-app" +
|
||||
"&utm_medium=app" +
|
||||
"&utm_campaign=prospect-$SYSTEM_LANGUAGE" +
|
||||
"&utm_content=devicelang-$APP_LANGUAGE"
|
||||
|
||||
val NEW_BUY_WALLET_URL = "https://buy.tangem.com/?$UTM_MARKS"
|
||||
}
|
||||
|
||||
private val homeMiddleware: Middleware<AppState> = { _, _ ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
handleHomeAction(action)
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleHomeAction(action: Action) {
|
||||
when (action) {
|
||||
is HomeAction.OnCreate -> {
|
||||
Analytics.eraseContext()
|
||||
Analytics.send(IntroductionProcess.ScreenOpened())
|
||||
|
||||
store.dispatch(GlobalAction.RestoreAppCurrency)
|
||||
}
|
||||
is HomeAction.ReadCard -> {
|
||||
action.scope.launch {
|
||||
readCard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readCard() {
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
||||
isBiometricsRequestPolicy = shouldSaveAccessCodes,
|
||||
)
|
||||
|
||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsSource = AnalyticsParam.ScreensSources.Intro,
|
||||
onProgressStateChange = { showProgress ->
|
||||
if (showProgress) {
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = true))
|
||||
} else {
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
}
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Unable to scan card")
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
proceedWithScanResponse(scanResponse)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun proceedWithScanResponse(scanResponse: ScanResponse) = scope.launch {
|
||||
val userWalletBuilder = store.inject(DaggerGraphState::coldUserWalletBuilderFactory).create(scanResponse)
|
||||
|
||||
val userWallet = userWalletBuilder.build().guard {
|
||||
Timber.e("User wallet not created")
|
||||
return@launch
|
||||
}
|
||||
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
}
|
||||
.doOnSuccess {
|
||||
sendSignedInCardAnalyticsEvent(scanResponse)
|
||||
store.onUserWalletSelected(userWallet = userWallet)
|
||||
}
|
||||
.doOnResult {
|
||||
navigateTo(AppRoute.Wallet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSignedInCardAnalyticsEvent(scanResponse: ScanResponse) {
|
||||
val currency = ParamCardCurrencyConverter().convert(
|
||||
value = scanResponse.cardTypesResolver,
|
||||
)
|
||||
|
||||
if (currency != null) {
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
|
||||
Analytics.send(
|
||||
event = Basic.SignedIn(
|
||||
currency = currency,
|
||||
batch = scanResponse.card.batchId,
|
||||
signInType = Basic.SignedIn.SignInType.Card,
|
||||
walletsCount = userWalletsListManager.walletsCount.toString(),
|
||||
hasBackup = scanResponse.card.backupStatus?.isActive,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun navigateTo(route: AppRoute) {
|
||||
store.dispatchNavigationAction { push(route) }
|
||||
delay(HIDE_PROGRESS_DELAY)
|
||||
store.dispatch(HomeAction.ScanInProgress(scanInProgress = false))
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.Action
|
||||
|
||||
object HomeReducer {
|
||||
fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
private fun internalReduce(action: Action, appState: AppState): HomeState {
|
||||
if (action !is HomeAction) return appState.homeState
|
||||
|
||||
return when (action) {
|
||||
is HomeAction.ScanInProgress -> {
|
||||
appState.homeState.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
is HomeAction.UserCountryLoaded -> {
|
||||
val stories = if (action.userCountry.needApplyFCARestrictions()) {
|
||||
getRestrictedStories()
|
||||
} else {
|
||||
Stories.entries
|
||||
}
|
||||
appState.homeState.copy(
|
||||
stories = stories.toImmutableList(),
|
||||
)
|
||||
}
|
||||
else -> appState.homeState
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.StateType
|
||||
|
||||
// todo refactor [REDACTED_TASK_KEY]
|
||||
data class HomeState(
|
||||
val scanInProgress: Boolean = false,
|
||||
val stories: ImmutableList<Stories> = getRestrictedStories().toImmutableList(),
|
||||
) : StateType {
|
||||
|
||||
val firstStory: Stories get() = stories[0]
|
||||
|
||||
fun stepOf(story: Stories): Int = stories.indexOf(story)
|
||||
}
|
||||
|
||||
enum class Stories(val duration: Int = 6000) {
|
||||
TangemIntro,
|
||||
RevolutionaryWallet,
|
||||
UltraSecureBackup,
|
||||
Currencies,
|
||||
Web3,
|
||||
WalletForEveryone,
|
||||
}
|
||||
|
||||
/**
|
||||
* For FCA restriction stories
|
||||
*/
|
||||
fun getRestrictedStories(): List<Stories> {
|
||||
return Stories.entries.filterNot { it == Stories.Currencies }
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
package com.tangem.tap.features.hot
|
||||
|
||||
import com.tangem.hot.sdk.model.HotAuth
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.tap.domain.hot.HotWalletPasswordRequester
|
||||
import javax.inject.Inject
|
||||
|
||||
class DefaultHotWalletPasswordRequester @Inject constructor() : HotWalletPasswordRequester {
|
||||
|
||||
override suspend fun requestPassword(hotWalletId: HotWalletId): HotAuth.Password {
|
||||
return HotAuth.Password("TODO [REDACTED_TASK_KEY]".toCharArray()) // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
}
|
||||
|
|
@ -4,21 +4,12 @@ import android.content.Intent
|
|||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.os.Build
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.features.home.redux.HomeAction
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.features.intentHandler.AffectsNavigation
|
||||
import com.tangem.tap.features.welcome.redux.WelcomeAction
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class BackgroundScanIntentHandler(
|
||||
private val hasSavedUserWalletsProvider: () -> Boolean,
|
||||
private val scope: CoroutineScope,
|
||||
) : IntentHandler, AffectsNavigation {
|
||||
class BackgroundScanIntentHandler {
|
||||
|
||||
private val nfcActions = arrayOf(
|
||||
NfcAdapter.ACTION_NDEF_DISCOVERED,
|
||||
|
|
@ -26,8 +17,15 @@ class BackgroundScanIntentHandler(
|
|||
NfcAdapter.ACTION_TAG_DISCOVERED,
|
||||
)
|
||||
|
||||
override fun handleIntent(intent: Intent?, isFromForeground: Boolean): Boolean {
|
||||
if (isFromForeground) return true
|
||||
fun getInitScreenLaunchMode(intent: Intent?): InitScreenLaunchMode {
|
||||
return if (shouldOpenScanCard(intent)) {
|
||||
InitScreenLaunchMode.WithCardScan
|
||||
} else {
|
||||
InitScreenLaunchMode.Standard
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldOpenScanCard(intent: Intent?): Boolean {
|
||||
if (intent == null || intent.action !in nfcActions) return false
|
||||
|
||||
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
|
|
@ -36,15 +34,9 @@ class BackgroundScanIntentHandler(
|
|||
@Suppress("DEPRECATION")
|
||||
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
|
||||
}
|
||||
if (tag == null) return false
|
||||
|
||||
intent.action = null
|
||||
if (hasSavedUserWalletsProvider.invoke()) {
|
||||
store.dispatchOnMain(WelcomeAction.ProceedWithCard)
|
||||
} else {
|
||||
store.dispatchOnMain(HomeAction.ReadCard(scope = scope))
|
||||
}
|
||||
|
||||
return true
|
||||
return tag != null
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,8 @@ import android.content.Intent
|
|||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.removePrefixOrNull
|
||||
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.features.intentHandler.AffectsNavigation
|
||||
import com.tangem.tap.features.intentHandler.IntentHandler
|
||||
import com.tangem.tap.store
|
||||
import timber.log.Timber
|
||||
import java.net.URLDecoder
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ 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.scan.ScanResponse
|
||||
import com.tangem.domain.models.wallet.requireColdWallet
|
||||
import com.tangem.domain.notifications.GetApplicationIdUseCase
|
||||
import com.tangem.domain.notifications.SendPushTokenUseCase
|
||||
import com.tangem.domain.notifications.models.ApplicationId
|
||||
|
|
@ -212,15 +210,11 @@ internal class MainViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun makeSellExchangeService(environmentConfig: EnvironmentConfig): ExchangeService {
|
||||
val cardProvider: () -> ScanResponse? = {
|
||||
userWalletsListManager.selectedUserWalletSync?.requireColdWallet()?.scanResponse // TODO [REDACTED_TASK_KEY]
|
||||
}
|
||||
|
||||
return MoonPayService(
|
||||
apiKey = environmentConfig.moonPayApiKey,
|
||||
secretKey = environmentConfig.moonPayApiSecretKey,
|
||||
logEnabled = LogConfig.network.moonPayService,
|
||||
cardProvider = { cardProvider.invoke()?.card },
|
||||
userWalletProvider = { userWalletsListManager.selectedUserWalletSync },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.features.welcome.component
|
||||
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.common.routing.entity.SerializableIntent
|
||||
import com.tangem.core.decompose.factory.ComponentFactory
|
||||
import com.tangem.core.ui.decompose.ComposableContentComponent
|
||||
|
|
@ -7,6 +8,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
|
|||
interface WelcomeComponent : ComposableContentComponent {
|
||||
|
||||
data class Params(
|
||||
val launchMode: InitScreenLaunchMode,
|
||||
val intent: SerializableIntent?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.tap.features.welcome.model
|
||||
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.routing.entity.InitScreenLaunchMode
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
|
|
@ -44,10 +45,9 @@ internal class WelcomeModel @Inject constructor(
|
|||
subscribeToStoreChanges()
|
||||
initGlobalState()
|
||||
|
||||
val welcomeAction = if (params.intent != null) {
|
||||
WelcomeAction.ProceedWithIntent(params.intent.toIntent())
|
||||
} else {
|
||||
WelcomeAction.ProceedWithBiometrics()
|
||||
val welcomeAction = when (params.launchMode) {
|
||||
is InitScreenLaunchMode.WithCardScan -> WelcomeAction.ProceedWithCard
|
||||
is InitScreenLaunchMode.Standard -> WelcomeAction.ProceedWithBiometrics(params.intent?.toIntent())
|
||||
}
|
||||
|
||||
store.dispatch(welcomeAction)
|
||||
|
|
|
|||
|
|
@ -20,10 +20,8 @@ import com.tangem.tap.*
|
|||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
|
||||
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
|
|
@ -44,7 +42,7 @@ internal class WelcomeMiddleware {
|
|||
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
|
||||
mainScope.launch {
|
||||
when (action) {
|
||||
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent, scope = this)
|
||||
is WelcomeAction.ProceedWithIntent -> proceedWithIntent(action.intent)
|
||||
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(
|
||||
afterUnlockIntent = action.afterUnlockIntent ?: state.intent,
|
||||
)
|
||||
|
|
@ -55,7 +53,7 @@ internal class WelcomeMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithIntent(initialIntent: Intent, scope: CoroutineScope) {
|
||||
private suspend fun proceedWithIntent(initialIntent: Intent) {
|
||||
Timber.d(
|
||||
"""
|
||||
Proceeding with intent
|
||||
|
|
@ -63,15 +61,12 @@ internal class WelcomeMiddleware {
|
|||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val handler = BackgroundScanIntentHandler(
|
||||
scope = scope,
|
||||
hasSavedUserWalletsProvider = { true },
|
||||
)
|
||||
val isBackgroundScanHandled = handler.handleIntent(initialIntent, isFromForeground = false)
|
||||
val hasUncompletedBackup = backupService.hasIncompletedBackup
|
||||
|
||||
if (!isBackgroundScanHandled && !hasUncompletedBackup) {
|
||||
if (!hasUncompletedBackup) {
|
||||
store.dispatchWithMain(WelcomeAction.ProceedWithBiometrics(initialIntent))
|
||||
} else {
|
||||
store.dispatchWithMain(WelcomeAction.ProceedWithCard)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -139,7 +134,7 @@ internal class WelcomeMiddleware {
|
|||
}
|
||||
|
||||
private fun sendSignedInAnalyticsEvent(userWallet: UserWallet, signInType: Basic.SignedIn.SignInType) {
|
||||
// TODO [REDACTED_TASK_KEY]
|
||||
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Analytics
|
||||
|
||||
if (userWallet !is UserWallet.Cold) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ import com.tangem.domain.core.lce.Lce
|
|||
import com.tangem.domain.exchange.ExpressAvailabilityState
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.utils.Provider
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import com.tangem.domain.core.utils.lceContent
|
|||
import com.tangem.domain.core.utils.lceError
|
||||
import com.tangem.domain.core.utils.lceLoading
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeService
|
||||
import com.tangem.tap.network.exchangeServices.ExchangeServiceInitializationStatus
|
||||
|
|
@ -27,7 +27,7 @@ class MoonPayService(
|
|||
private val apiKey: String,
|
||||
private val secretKey: String,
|
||||
private val logEnabled: Boolean,
|
||||
private val cardProvider: () -> CardDTO?,
|
||||
private val userWalletProvider: () -> UserWallet?,
|
||||
) : ExchangeService {
|
||||
|
||||
override val initializationStatus: StateFlow<ExchangeServiceInitializationStatus>
|
||||
|
|
@ -103,8 +103,8 @@ class MoonPayService(
|
|||
}
|
||||
|
||||
override fun availableForSell(currency: Currency): Boolean {
|
||||
val card = cardProvider() ?: return false
|
||||
val checkCardExchange = !card.isStart2Coin
|
||||
val userWallet = userWalletProvider() ?: return false
|
||||
val checkCardExchange = userWallet !is UserWallet.Cold || !userWallet.scanResponse.card.isStart2Coin
|
||||
|
||||
if (!checkCardExchange) return false
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import com.tangem.tap.features.details.ui.cardsettings.coderecovery.api.AccessCo
|
|||
import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent
|
||||
import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent
|
||||
import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent
|
||||
import com.tangem.tap.features.home.api.HomeComponent
|
||||
import com.tangem.features.home.api.HomeComponent
|
||||
import com.tangem.tap.features.welcome.component.WelcomeComponent
|
||||
import com.tangem.tap.routing.component.RoutingComponent.Child
|
||||
import dagger.hilt.android.scopes.ActivityScoped
|
||||
|
|
@ -133,6 +133,7 @@ internal class ChildFactory @Inject constructor(
|
|||
createComponentChild(
|
||||
context = context,
|
||||
params = WelcomeComponent.Params(
|
||||
launchMode = route.launchMode,
|
||||
intent = route.intent,
|
||||
),
|
||||
componentFactory = welcomeComponentFactory,
|
||||
|
|
@ -290,7 +291,7 @@ internal class ChildFactory @Inject constructor(
|
|||
is AppRoute.Home -> {
|
||||
createComponentChild(
|
||||
context = context,
|
||||
params = Unit,
|
||||
params = HomeComponent.Params(route.launchMode),
|
||||
componentFactory = homeComponentFactory,
|
||||
)
|
||||
}
|
||||
|
|
@ -437,6 +438,7 @@ internal class ChildFactory @Inject constructor(
|
|||
initialCurrency = route.initialCurrency,
|
||||
selectedCurrency = route.selectedCurrency,
|
||||
source = ChooseManagedTokensComponent.Source.valueOf(route.source.name),
|
||||
showSendViaSwapNotification = route.showSendViaSwapNotification,
|
||||
),
|
||||
componentFactory = chooseManagedTokensComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 234 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 509 KiB |
|
Before Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 484 KiB |