diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/GaslessScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/GaslessScenarios.kt
index 7617efa34f..90ef47eb41 100644
--- a/app/src/androidTest/kotlin/com/tangem/scenarios/GaslessScenarios.kt
+++ b/app/src/androidTest/kotlin/com/tangem/scenarios/GaslessScenarios.kt
@@ -87,4 +87,53 @@ fun BaseTestCase.openGaslessSendScreenWithHotWallet(
step("Click on 'Send' button in bottom sheet") {
onTransferBottomSheet { sendButton.clickWithAssertion() }
}
+}
+
+/**
+ * Open an existing hot wallet, select the token to send and choose the swap target token/network —
+ * the shared entry into the gasless send-via-swap flow. Scenario states stay in the test body.
+ */
+fun BaseTestCase.openSendViaSwapScreenWithHotWallet(
+ seedPhrase: String,
+ tokenName: String,
+ swapTokenName: String,
+ networkName: String,
+ networkType: String? = null,
+) {
+ step("Open 'Main' screen with existing hot wallet") {
+ openMainScreenWithExistingHotWallet(seedPhrase)
+ }
+ step("Click on token with name: '$tokenName'") {
+ onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() }
+ }
+ step("Select '$swapTokenName' as the token to receive via swap") {
+ selectTokenToSendViaSwap(
+ swapTokenName = swapTokenName,
+ networkName = networkName,
+ networkType = networkType,
+ )
+ }
+}
+
+/**
+ * Send-via-swap amount entry: type the amount, advance past the quote-gated 'Next' button (waiting
+ * until it becomes enabled once the swap quote loads), then fill the recipient and open the
+ * 'Send confirm' screen. Uses `composeTestRule.waitUntil` because `flakySafely` is unavailable in
+ * extensions on [BaseTestCase].
+ */
+fun BaseTestCase.enterSwapAmountAndOpenSendConfirm(amount: String, recipientAddress: String) {
+ step("Type amount '$amount' in input field") {
+ onSendScreen { amountInputTextField.performTextReplacement(amount) }
+ }
+ step("Click on 'Next' button") {
+ composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_LONG) {
+ runCatching {
+ onSendScreen {
+ nextButton.assertIsEnabled()
+ nextButton.performClick()
+ }
+ }.isSuccess
+ }
+ }
+ enterRecipientAndOpenSendConfirm(recipientAddress)
}
\ No newline at end of file
diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/sendViaSwap/GaslessSendViaSwapTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/sendViaSwap/GaslessSendViaSwapTest.kt
new file mode 100644
index 0000000000..582555f882
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/tangem/tests/send/sendViaSwap/GaslessSendViaSwapTest.kt
@@ -0,0 +1,254 @@
+package com.tangem.tests.send.sendViaSwap
+
+import com.tangem.common.BaseTestCase
+import com.tangem.common.R
+import com.tangem.common.constants.TestConstants.BITCOIN_RECIPIENT_ADDRESS
+import com.tangem.common.constants.TestConstants.PROVIDERS_API_SCENARIO
+import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO
+import com.tangem.common.constants.TestConstants.SVS_SEED_PHRASE_12
+import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO
+import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG
+import com.tangem.common.utils.resetWireMockScenarioState
+import com.tangem.common.utils.setWireMockScenarioState
+import com.tangem.scenarios.*
+import com.tangem.screens.*
+import dagger.hilt.android.testing.HiltAndroidTest
+import io.github.kakaocup.kakao.common.utilities.getResourceString
+import io.qameta.allure.kotlin.AllureId
+import io.qameta.allure.kotlin.junit4.DisplayName
+import org.junit.Test
+
+/**
+ * Gasless send-via-swap: paying the network fee with the stablecoin while converting it through an
+ * express swap. Covers the fee-token selection on the swap summary, the stablecoin balance validation
+ * against the gasless fee, and the full signed swap-and-send on a hot wallet.
+ */
+@HiltAndroidTest
+class GaslessSendViaSwapTest : BaseTestCase() {
+
+ private val tokenName = "USDC"
+ private val currencySymbol = "USDC"
+ private val nativeTokenName = "Polygon"
+ private val swapTokenName = "Bitcoin"
+ private val mainNetwork = "MAIN"
+ private val providerName = "Changelly"
+ private val tokenAmount = "1"
+ private val hotWalletTokensState = "PolygonUSDCHotWallet"
+ private val quotesState = "PolygonUSDC"
+ private val assetsScenarioName = "express_api_assets"
+ private val assetsExchangeEnabledState = "BitcoinExchangeEnabled"
+ private val providersState = "HotWalletSvS"
+
+ @AllureId("5120")
+ @DisplayName("Gasless Send via Swap: the network fee is selectable and payable with the stablecoin")
+ @Test
+ fun checkFeeTokenSelectionForSwapTest() {
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ resetWireMockScenarioState(assetsScenarioName)
+ resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
+ }
+ ).run {
+ step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
+ }
+ step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
+ }
+ step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
+ setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
+ }
+ step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
+ setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
+ }
+
+ step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
+ openSendViaSwapScreenWithHotWallet(
+ seedPhrase = SVS_SEED_PHRASE_12,
+ tokenName = tokenName,
+ swapTokenName = swapTokenName,
+ networkName = swapTokenName,
+ networkType = mainNetwork,
+ )
+ }
+ step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
+ enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
+ }
+ step("Assert 'Network fee' block with token selection is displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSendConfirmScreen {
+ feeSelectorTitle.assertIsDisplayed()
+ selectFeeIcon.assertIsDisplayed()
+ }
+ }
+ }
+ step("Click on 'Network fee' block") {
+ onSendConfirmScreen { feeSelectorBlock.performClick() }
+ }
+ step("Click on '$nativeTokenName' fee token to open 'Choose token'") {
+ onSendFeeSelectorBottomSheet { feeTokenItem(nativeTokenName).performClick() }
+ }
+ step("Assert 'Choose token' bottom sheet is displayed") {
+ onSendFeeSelectorBottomSheet { chooseTokenTitle.assertIsDisplayed() }
+ }
+ step("Assert '$tokenName' is available for the fee payment") {
+ onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).assertIsDisplayed() }
+ }
+ step("Select '$tokenName' as the fee-paying token") {
+ onSendFeeSelectorBottomSheet { feeTokenItem(tokenName).performClick() }
+ }
+ step("Click on 'Apply' button") {
+ onSendFeeSelectorBottomSheet { applyButton.performClick() }
+ }
+ step("Assert the network fee is calculated in '$currencySymbol' on the summary") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
+ }
+ }
+ }
+ }
+
+ @AllureId("5121")
+ @DisplayName("Gasless Send via Swap: insufficient stablecoin balance to cover the fee blocks the swap")
+ @Test
+ fun checkBalanceValidationForFeeTest() {
+ val usdcBalanceScenario = "polygon_usdc_balance"
+ val lowBalanceState = "LowBalance"
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ resetWireMockScenarioState(assetsScenarioName)
+ resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
+ resetWireMockScenarioState(usdcBalanceScenario)
+ }
+ ).run {
+ step("Set WireMock scenario '$usdcBalanceScenario' to '$lowBalanceState'") {
+ setWireMockScenarioState(scenarioName = usdcBalanceScenario, state = lowBalanceState)
+ }
+ step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
+ }
+ step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
+ }
+ step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
+ setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
+ }
+ step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
+ setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
+ }
+
+ step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
+ openSendViaSwapScreenWithHotWallet(
+ seedPhrase = SVS_SEED_PHRASE_12,
+ tokenName = tokenName,
+ swapTokenName = swapTokenName,
+ networkName = swapTokenName,
+ networkType = mainNetwork,
+ )
+ }
+ step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
+ enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
+ }
+ step("Pay the network fee with '$tokenName' via the fee selector") {
+ selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
+ }
+ step("Assert 'Not enough funds' error is displayed in the fee selector") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSendFeeSelectorBottomSheet { notEnoughFundsError.assertIsDisplayed() }
+ }
+ }
+ step("Assert 'Apply' button is disabled (cannot pay the fee with insufficient balance)") {
+ onSendFeeSelectorBottomSheet { applyButton.assertIsNotEnabled() }
+ }
+ }
+ }
+
+ @AllureId("5122")
+ @DisplayName("Gasless Send via Swap: sign and send a swap paying the fee with the stablecoin")
+ @Test
+ fun checkSendViaSwapFinalScreenAndSendTest() {
+ val exchangeStatusScenario = "exchange_status_provider"
+ val changellyStatusState = "Changelly"
+ val expressStatusItemTitle = getResourceString(R.string.express_exchange_by, providerName)
+
+ setupHooks(
+ additionalAfterSection = {
+ resetWireMockScenarioState(USER_TOKENS_API_SCENARIO)
+ resetWireMockScenarioState(QUOTES_API_SCENARIO)
+ resetWireMockScenarioState(assetsScenarioName)
+ resetWireMockScenarioState(PROVIDERS_API_SCENARIO)
+ resetWireMockScenarioState(exchangeStatusScenario)
+ }
+ ).run {
+ step("Set WireMock scenario '$USER_TOKENS_API_SCENARIO' to '$hotWalletTokensState'") {
+ setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = hotWalletTokensState)
+ }
+ step("Set WireMock scenario '$QUOTES_API_SCENARIO' to '$quotesState'") {
+ setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = quotesState)
+ }
+ step("Set WireMock scenario '$assetsScenarioName' to '$assetsExchangeEnabledState'") {
+ setWireMockScenarioState(scenarioName = assetsScenarioName, state = assetsExchangeEnabledState)
+ }
+ step("Set WireMock scenario '$PROVIDERS_API_SCENARIO' to '$providersState'") {
+ setWireMockScenarioState(scenarioName = PROVIDERS_API_SCENARIO, state = providersState)
+ }
+ step("Set WireMock scenario '$exchangeStatusScenario' to '$changellyStatusState'") {
+ setWireMockScenarioState(scenarioName = exchangeStatusScenario, state = changellyStatusState)
+ }
+
+ step("Open the send-via-swap flow for '$tokenName' on an existing hot wallet") {
+ openSendViaSwapScreenWithHotWallet(
+ seedPhrase = SVS_SEED_PHRASE_12,
+ tokenName = tokenName,
+ swapTokenName = swapTokenName,
+ networkName = swapTokenName,
+ networkType = mainNetwork,
+ )
+ }
+ step("Enter amount '$tokenAmount' and open the 'Send confirm' screen") {
+ enterSwapAmountAndOpenSendConfirm(amount = tokenAmount, recipientAddress = BITCOIN_RECIPIENT_ADDRESS)
+ }
+ step("Pay the network fee with '$tokenName' via the fee selector") {
+ selectStablecoinAsFeeToken(coinName = nativeTokenName, tokenName = tokenName)
+ }
+ step("Click on 'Apply' button") {
+ onSendFeeSelectorBottomSheet { applyButton.performClick() }
+ }
+ step("Assert the sent '$tokenName' amount is displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onSendConfirmScreen { primaryAmount.assertIsDisplayed() }
+ }
+ }
+ step("Assert the recipient address is displayed") {
+ onSendConfirmScreen { recipientAddress(BITCOIN_RECIPIENT_ADDRESS).assertIsDisplayed() }
+ }
+ step("Assert the amount to receive after the swap is displayed") {
+ onSendConfirmScreen { secondaryAmount.assertIsDisplayed() }
+ }
+ step("Assert the network fee is paid in '$currencySymbol'") {
+ onSendConfirmScreen { feeBlockCurrency(currencySymbol).assertIsDisplayed() }
+ }
+ step("Sign, send and open the 'Transaction sent' screen") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ openSendSuccessScreenViaLongClickOnSendButton()
+ }
+ }
+ step("Check 'Send via swap' success screen") {
+ checkSendViaSwapSuccessScreen()
+ }
+ step("Click on 'Close' button") {
+ onSendSuccessScreen { closeButton.performClick() }
+ }
+ step("Assert 'Express status' item with title '$expressStatusItemTitle' is displayed") {
+ flakySafely(WAIT_UNTIL_TIMEOUT_LONG) {
+ onTokenDetailsScreen { expressStatusItem(expressStatusItemTitle).assertIsDisplayed() }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
index 6be9860e0c..00c73a1027 100644
--- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
+++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt
@@ -214,33 +214,18 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
FeatureToggles.AND_15101_TANGEM_PAY_HOT_WALLET_ONBOARDING,
)
TangemLogger.i("[TangemPay][HWO] Feature toggle enabled=$isHotWalletOnboardingEnabled")
- if (isHotWalletOnboardingEnabled) {
+ val afterEmptyRoute: AppRoute = if (isHotWalletOnboardingEnabled) {
val tangemPayHotWalletOnboardingDeepLink = withTimeoutOrNull(2.seconds) {
appsFlyerReferralParamsHandler.waitForDeeplink(AppsFlyerDeeplinkSource.TangemPayHotWalletOnboarding)
}
TangemLogger.i("[TangemPay][HWO] Deep link present=${tangemPayHotWalletOnboardingDeepLink != null}")
if (tangemPayHotWalletOnboardingDeepLink != null) {
- val hotWalletRoute = AppRoute.TangemPayHotWalletOnboarding
- val shouldShowTos = !cardRepository.isTangemTOSAccepted()
- val route = if (shouldShowTos) "Disclaimer" else "HotWalletOnboarding"
- TangemLogger.i("[TangemPay][HWO] TOS accepted=${!shouldShowTos}, navigating to $route")
- return if (shouldShowTos) {
- AppRoute.Disclaimer(isTosAccepted = false, nextRoute = hotWalletRoute)
- } else {
- hotWalletRoute
- }
+ AppRoute.TangemPayHotWalletOnboarding
+ } else {
+ getDefaultRoute()
}
- }
-
- val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
- FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
- )
- // Referral users skip the Home stories screen and land directly on the
- // mobile wallet creation flow.
- val afterEmptyRoute: AppRoute = if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
- AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
} else {
- AppRoute.Home(launchMode = launchMode)
+ getDefaultRoute()
}
val shouldAskPushPermission = shouldInitiallyAskPermissionUseCase(PUSH_PERMISSION).getOrNull()
@@ -261,6 +246,19 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
}
}
+ private suspend fun getDefaultRoute(): AppRoute {
+ val isHideStoriesForReferralEnabled = featureTogglesManager.isFeatureEnabled(
+ FeatureToggles.TWI_1512_HIDE_STORIES_FOR_REFERRAL_ENABLED,
+ )
+ // Referral users skip the Home stories screen and land directly on the
+ // mobile wallet creation flow.
+ return if (isHideStoriesForReferralEnabled && shouldShowMobileWalletPromoUseCase()) {
+ AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.HotWallet)
+ } else {
+ AppRoute.Home(launchMode = launchMode)
+ }
+ }
+
@Composable
override fun Content(modifier: Modifier) {
RootContent(
diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
index 5ef88d9866..6400bc4ab8 100644
--- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
+++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt
@@ -32,6 +32,7 @@ import com.tangem.features.onramp.component.*
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacksStub
import com.tangem.features.pushnotifications.api.PushNotificationsParams
+import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
import com.tangem.features.send.api.NFTSendComponent
import com.tangem.features.send.api.SendComponent
import com.tangem.features.send.api.SendEntryPointComponent
@@ -87,6 +88,7 @@ internal class ChildFactory @Inject constructor(
private val resetCardComponentFactory: ResetCardComponent.Factory,
private val referralComponentFactory: ReferralComponent.Factory,
private val pushNotificationsComponentFactory: PushNotificationsComponent.Factory,
+ private val pushNotificationSettingsComponentFactory: PushNotificationSettingsComponent.Factory,
private val walletComponentFactory: WalletEntryComponent.Factory,
private val sendComponentFactoryV2: SendComponent.Factory,
private val redesignedWalletConnectComponentFactory: WalletConnectEntryComponent.Factory,
@@ -172,6 +174,13 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
+ is AppRoute.PushNotificationSettings -> {
+ createComponentChild(
+ context = context,
+ params = PushNotificationSettingsComponent.Params(route.userWalletId),
+ componentFactory = pushNotificationSettingsComponentFactory,
+ )
+ }
is AppRoute.WalletBackup -> {
createComponentChild(
context = context,
diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
index 73d0e6bebb..5ae1808b7e 100644
--- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
+++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt
@@ -257,6 +257,11 @@ sealed class AppRoute(val path: String) : Route {
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
+ @Serializable
+ data class PushNotificationSettings(
+ val userWalletId: UserWalletId,
+ ) : AppRoute(path = "/push_notification_settings/${userWalletId.stringValue}")
+
@Serializable
data class WalletBackup(
val userWalletId: UserWalletId,
diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt
index 71bce0a129..41b174cf8d 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt
@@ -40,6 +40,7 @@ sealed interface PaymentAccountStatusValueDM {
@NameLabel("active_account")
data class ActiveAccount(
+ @Json(name = "active_account") val marker: Boolean = true,
@Json(name = "customer_id") val customerId: String,
@Json(name = "currency_code") val currencyCode: String,
@Json(name = "deposit_address") val depositAddress: String?,
@@ -59,9 +60,11 @@ sealed interface PaymentAccountStatusValueDM {
@NameLabel("deactivated_account")
data class DeactivatedAccount(
@Json(name = "deactivated_account") val marker: Boolean = true,
+ @Json(name = "customer_id") val customerId: String,
@Json(name = "fiat_rate") val fiatRate: BigDecimal?,
@Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM,
@Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM,
+ @Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal,
) : PaymentAccountStatusValueDM
@JsonClass(generateAdapter = true)
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 57b95dc184..0c188411c0 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -102,6 +102,7 @@
- %d address
- %d addresses
+ Contact
Contact name
Copy address
We couldn’t create contact. Please try again later.
diff --git a/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml b/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml
new file mode 100644
index 0000000000..8cbc8e1bcf
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_push_notification_settings_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt
index aab76125e5..c586313c67 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt
+++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt
@@ -39,11 +39,11 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValue.IssuingCard -> PaymentAccountStatusValueDM.IssuingCard()
is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveAccount(
customerId = value.customerId,
- currencyCode = value.currencyCode,
+ currencyCode = value.balance.fiatBalance.currency,
depositAddress = value.depositAddress,
- fiatBalance = value.fiatBalance.toDM(),
- cryptoBalance = value.cryptoBalance.toDM(),
- availableForWithdrawal = value.availableForWithdrawal,
+ fiatBalance = value.balance.fiatBalance.toDM(),
+ cryptoBalance = value.balance.cryptoBalance.toDM(),
+ availableForWithdrawal = value.balance.availableForWithdrawal,
fiatRate = value.fiatRate,
cards = value.cards.map { card ->
PaymentAccountStatusValueDM.TangemPayCard(
@@ -63,9 +63,11 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
)
is PaymentAccountStatusValue.Empty -> PaymentAccountStatusValueDM.Empty()
is PaymentAccountStatusValue.Deactivated -> PaymentAccountStatusValueDM.DeactivatedAccount(
+ customerId = value.customerId,
fiatRate = value.fiatRate,
- fiatBalance = value.fiatBalance.toDM(),
- cryptoBalance = value.cryptoBalance.toDM(),
+ fiatBalance = value.balance.fiatBalance.toDM(),
+ cryptoBalance = value.balance.cryptoBalance.toDM(),
+ availableForWithdrawal = value.balance.availableForWithdrawal,
)
// Transient statuses are not persisted
is PaymentAccountStatusValue.Loading,
@@ -90,11 +92,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
is PaymentAccountStatusValueDM.ActiveAccount -> PaymentAccountStatusValue.Loaded(
source = StatusSource.CACHE,
customerId = value.customerId,
- currencyCode = value.currencyCode,
depositAddress = value.depositAddress,
- fiatBalance = value.fiatBalance.toDomain(),
- cryptoBalance = value.cryptoBalance.toDomain(),
- availableForWithdrawal = value.availableForWithdrawal,
+ balance = PaymentAccountStatusValue.Balance(
+ fiatBalance = value.fiatBalance.toDomain(),
+ cryptoBalance = value.cryptoBalance.toDomain(),
+ availableForWithdrawal = value.availableForWithdrawal,
+ ),
cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate,
cards = value.cards.map { card ->
@@ -123,8 +126,12 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor(
)
is PaymentAccountStatusValueDM.DeactivatedAccount -> PaymentAccountStatusValue.Deactivated(
source = StatusSource.CACHE,
- fiatBalance = value.fiatBalance.toDomain(),
- cryptoBalance = value.cryptoBalance.toDomain(),
+ customerId = value.customerId,
+ balance = PaymentAccountStatusValue.Balance(
+ fiatBalance = value.fiatBalance.toDomain(),
+ cryptoBalance = value.cryptoBalance.toDomain(),
+ availableForWithdrawal = value.availableForWithdrawal,
+ ),
cryptoCurrency = cryptoCurrency,
fiatRate = value.fiatRate,
)
diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt
index 746221090a..7c205fffd6 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt
+++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt
@@ -33,6 +33,7 @@ import com.tangem.domain.pay.repository.TangemPayCloseCardRepository
import com.tangem.security.DeviceSecurityInfoProvider
import com.tangem.security.isSecurityExposed
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.utils.extensions.orZero
import com.tangem.utils.logging.TangemLogger
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
@@ -287,11 +288,16 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
customerId = requireNotNull(customerId) { "CustomerId must not be null" },
)
}
- fiatBalance != null && cryptoBalance != null && (isDeactivated || isFormer) -> {
+ fiatBalance != null && cryptoBalance != null && !customerId.isNullOrEmpty() &&
+ (isDeactivated || isFormer) -> {
PaymentAccountStatusValue.Deactivated(
source = StatusSource.ACTUAL,
- fiatBalance = fiatBalance,
- cryptoBalance = cryptoBalance,
+ customerId = requireNotNull(customerId) { "CustomerId must not be null" },
+ balance = PaymentAccountStatusValue.Balance(
+ fiatBalance = fiatBalance,
+ cryptoBalance = cryptoBalance,
+ availableForWithdrawal = availableForWithdrawal.orZero(),
+ ),
cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId),
fiatRate = quotesData?.fiatRate,
)
@@ -321,11 +327,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor(
return PaymentAccountStatusValue.Loaded(
source = StatusSource.ACTUAL,
customerId = customerId,
- currencyCode = cardInfo.currencyCode,
depositAddress = cardInfo.depositAddress,
- fiatBalance = cardInfo.fiatBalance,
- cryptoBalance = cardInfo.cryptoBalance,
- availableForWithdrawal = cardInfo.availableForWithdrawal,
+ balance = PaymentAccountStatusValue.Balance(
+ fiatBalance = cardInfo.fiatBalance,
+ cryptoBalance = cardInfo.cryptoBalance,
+ availableForWithdrawal = cardInfo.availableForWithdrawal,
+ ),
cryptoCurrency = cryptoCurrency,
fiatRate = fiatRate,
cards = listOf(
diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt
index 746b701bc8..95f70c31bc 100644
--- a/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt
+++ b/data/visa/src/main/kotlin/com/tangem/data/pay/util/CustomerInfoConverter.kt
@@ -65,6 +65,7 @@ internal object CustomerInfoConverter : Converter TotalFiatBalance.Loading
is Loaded -> {
val rate = this.fiatRate ?: return TotalFiatBalance.Failed
- TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
+ TotalFiatBalance.Loaded(amount = balance.fiatBalance.availableBalance.multiply(rate), source = source)
}
is Deactivated -> {
val rate = this.fiatRate ?: return TotalFiatBalance.Failed
- TotalFiatBalance.Loaded(amount = fiatBalance.availableBalance.multiply(rate), source = source)
+ TotalFiatBalance.Loaded(amount = balance.fiatBalance.availableBalance.multiply(rate), source = source)
}
}
@@ -104,8 +104,8 @@ sealed class PaymentAccountStatusValue {
* Represents a state where the account is deactivated.
*
* @property source The source of the status information.
- * @property fiatBalance The fiat balance details.
- * @property cryptoBalance The crypto balance details.
+ * @property customerId The unique identifier of the customer.
+ * @property balance The balance details (fiat, crypto and amount available for withdrawal).
* @property cryptoCurrency The crypto currency held by the deactivated account.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
* or `null` if the quote is not yet available. When `null`,
@@ -114,18 +114,18 @@ sealed class PaymentAccountStatusValue {
@Serializable
data class Deactivated(
override val source: StatusSource,
- val fiatBalance: FiatBalance,
- val cryptoBalance: CryptoBalance,
+ val customerId: String,
+ val balance: Balance,
val cryptoCurrency: CryptoCurrency.Token,
val fiatRate: SerializedBigDecimal?,
) : PaymentAccountStatusValue() {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = buildCryptoCurrencyStatusValue(
- amount = cryptoBalance.balance,
- fiatAmount = fiatBalance.availableBalance,
+ amount = balance.cryptoBalance.balance,
+ fiatAmount = balance.fiatBalance.availableBalance,
fiatRate = fiatRate,
- depositAddress = cryptoBalance.depositAddress,
+ depositAddress = balance.cryptoBalance.depositAddress,
),
)
}
@@ -135,11 +135,9 @@ sealed class PaymentAccountStatusValue {
*
* @property source The source of the status information.
* @property customerId The unique identifier of the customer.
- * @property currencyCode The code of the currency.
* @property depositAddress The address for deposits, if available.
- * @property fiatBalance The fiat balance details.
- * @property cryptoBalance The crypto balance details.
- * @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds).
+ * @property balance The balance details (fiat, crypto and amount available for withdrawal).
+ * The fiat currency code is available via [Balance.fiatBalance].
* @property cryptoCurrency The crypto currency held by the account.
* @property cards The list of user's cards.
* @property fiatRate Exchange rate of [cryptoCurrency] to the account's fiat currency,
@@ -150,11 +148,8 @@ sealed class PaymentAccountStatusValue {
data class Loaded(
override val source: StatusSource,
val customerId: String,
- val currencyCode: String,
val depositAddress: String?,
- val fiatBalance: FiatBalance,
- val cryptoBalance: CryptoBalance,
- val availableForWithdrawal: SerializedBigDecimal,
+ val balance: Balance,
val cryptoCurrency: CryptoCurrency.Token,
val cards: List,
val fiatRate: SerializedBigDecimal?,
@@ -162,10 +157,10 @@ sealed class PaymentAccountStatusValue {
val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(
currency = cryptoCurrency,
value = buildCryptoCurrencyStatusValue(
- amount = availableForWithdrawal,
- fiatAmount = fiatBalance.availableBalance,
+ amount = balance.availableForWithdrawal,
+ fiatAmount = balance.fiatBalance.availableBalance,
fiatRate = fiatRate,
- depositAddress = cryptoBalance.depositAddress,
+ depositAddress = balance.cryptoBalance.depositAddress,
),
)
}
@@ -202,6 +197,21 @@ sealed class PaymentAccountStatusValue {
}
}
+ /**
+ * Aggregates all balance data of a payment account, as returned by the `customer/me` endpoint.
+ *
+ * @property fiatBalance The fiat balance details.
+ * @property cryptoBalance The crypto balance details.
+ * @property availableForWithdrawal The crypto amount currently available for withdrawal/swap
+ * (excludes pending/locked funds).
+ */
+ @Serializable
+ data class Balance(
+ val fiatBalance: FiatBalance,
+ val cryptoBalance: CryptoBalance,
+ val availableForWithdrawal: SerializedBigDecimal,
+ )
+
/**
* Represents the fiat balance of the payment account.
*
diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
index 952e701938..fbc32175d5 100644
--- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
+++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt
@@ -28,6 +28,7 @@ data class CustomerInfo(
val state: State,
val fiatBalance: PaymentAccountStatusValue.FiatBalance?,
val cryptoBalance: PaymentAccountStatusValue.CryptoBalance?,
+ val availableForWithdrawal: BigDecimal?,
) {
enum class State {
NEW,
diff --git a/features/address-book/impl/build.gradle.kts b/features/address-book/impl/build.gradle.kts
index 0e9ca8c98a..4711602801 100644
--- a/features/address-book/impl/build.gradle.kts
+++ b/features/address-book/impl/build.gradle.kts
@@ -19,6 +19,9 @@ dependencies {
implementation(projects.domain.models)
implementation(projects.domain.addressBook)
+ /** Common */
+ implementation(projects.common.ui)
+
/** Core modules */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
@@ -41,4 +44,7 @@ dependencies {
/** Other */
implementation(deps.kotlin.immutable.collections)
+
+ /** Tests */
+ testImplementation(projects.test.core)
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt
index baa9ee7021..49e12ab00f 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/AddressBookRoute.kt
@@ -7,4 +7,12 @@ internal sealed class AddressBookRoute {
@Serializable
data object List : AddressBookRoute()
+
+ /**
+ * if [contactId] is not null we should fetch existing contact
+ */
+ @Serializable
+ data class EditContact(
+ val contactId: String? = null,
+ ) : AddressBookRoute()
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt
index 78c5312faa..4b4ee9140a 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/component/DefaultAddressBookComponent.kt
@@ -9,11 +9,15 @@ import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
+import com.arkivanov.decompose.router.stack.pop
+import com.arkivanov.decompose.router.stack.pushNew
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.domain.addressbook.model.ContactId
import com.tangem.features.addressbook.AddressBookComponent
import com.tangem.features.addressbook.list.AddressBookListComponent
+import com.tangem.features.addressbook.editcontact.EditContactComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@@ -22,6 +26,7 @@ internal class DefaultAddressBookComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted private val params: AddressBookComponent.Params,
private val addressBookListComponentFactory: AddressBookListComponent.Factory,
+ private val editContactComponentFactory: EditContactComponent.Factory,
) : AddressBookComponent, AppComponentContext by context {
private val navigation = StackNavigation()
@@ -51,11 +56,16 @@ internal class DefaultAddressBookComponent @AssistedInject constructor(
context = childByContext(componentContext),
params = AddressBookListComponent.Params(
onContactClick = { contactId ->
- // TODO [REDACTED_TASK_KEY] router.push(EditContact(contactId))
- },
- onAddContactClick = {
- // TODO [REDACTED_TASK_KEY] router.push(AddContact)
+ navigation.pushNew(AddressBookRoute.EditContact(contactId))
},
+ onAddContactClick = { navigation.pushNew(AddressBookRoute.EditContact()) },
+ ),
+ )
+ is AddressBookRoute.EditContact -> editContactComponentFactory.create(
+ context = childByContext(componentContext),
+ params = EditContactComponent.Params(
+ contactId = config.contactId?.let(::ContactId),
+ onBackClick = { navigation.pop() },
),
)
}
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt
index 00e407405f..188884bab3 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookComponentModule.kt
@@ -4,6 +4,8 @@ import com.tangem.features.addressbook.AddressBookComponent
import com.tangem.features.addressbook.component.DefaultAddressBookComponent
import com.tangem.features.addressbook.list.AddressBookListComponent
import com.tangem.features.addressbook.list.DefaultAddressBookListComponent
+import com.tangem.features.addressbook.editcontact.DefaultEditContactComponent
+import com.tangem.features.addressbook.editcontact.EditContactComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -23,4 +25,8 @@ internal interface AddressBookComponentModule {
fun bindAddressBookListComponentFactory(
factory: DefaultAddressBookListComponent.Factory,
): AddressBookListComponent.Factory
+
+ @Binds
+ @Singleton
+ fun bindEditContactComponentFactory(factory: DefaultEditContactComponent.Factory): EditContactComponent.Factory
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt
index 7c666b0b6d..0fb085f06d 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/di/AddressBookModelModule.kt
@@ -3,6 +3,7 @@ package com.tangem.features.addressbook.di
import com.tangem.core.decompose.di.ModelComponent
import com.tangem.core.decompose.model.Model
import com.tangem.features.addressbook.list.model.AddressBookListModel
+import com.tangem.features.addressbook.editcontact.model.EditContactModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -17,4 +18,9 @@ internal interface AddressBookModelModule {
@IntoMap
@ClassKey(AddressBookListModel::class)
fun bindAddressBookModel(model: AddressBookListModel): Model
+
+ @Binds
+ @IntoMap
+ @ClassKey(EditContactModel::class)
+ fun bindEditContactModel(model: EditContactModel): Model
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt
new file mode 100644
index 0000000000..8f83105d52
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/DefaultEditContactComponent.kt
@@ -0,0 +1,40 @@
+package com.tangem.features.addressbook.editcontact
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.decompose.model.getOrCreateModel
+import com.tangem.features.addressbook.editcontact.model.EditContactModel
+import com.tangem.features.addressbook.editcontact.ui.EditContactContent
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultEditContactComponent @AssistedInject constructor(
+ @Assisted context: AppComponentContext,
+ @Assisted params: EditContactComponent.Params,
+) : EditContactComponent, AppComponentContext by context {
+
+ private val model: EditContactModel = getOrCreateModel(params)
+
+ @Composable
+ override fun Content(modifier: Modifier) {
+ val state by model.state.collectAsStateWithLifecycle()
+ EditContactContent(
+ state = state,
+ modifier = modifier,
+ )
+ BackHandler(onBack = state.onCloseClick)
+ }
+
+ @AssistedFactory
+ interface Factory : EditContactComponent.Factory {
+ override fun create(
+ context: AppComponentContext,
+ params: EditContactComponent.Params,
+ ): DefaultEditContactComponent
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt
new file mode 100644
index 0000000000..ede28263b7
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/EditContactComponent.kt
@@ -0,0 +1,15 @@
+package com.tangem.features.addressbook.editcontact
+
+import com.tangem.core.decompose.factory.ComponentFactory
+import com.tangem.core.ui.decompose.ComposableContentComponent
+import com.tangem.domain.addressbook.model.ContactId
+
+internal interface EditContactComponent : ComposableContentComponent {
+
+ interface Factory : ComponentFactory
+
+ data class Params(
+ val contactId: ContactId?,
+ val onBackClick: () -> Unit,
+ )
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt
new file mode 100644
index 0000000000..2a54242a81
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/contract/EditContactUM.kt
@@ -0,0 +1,26 @@
+package com.tangem.features.addressbook.editcontact.contract
+
+import androidx.compose.runtime.Immutable
+import com.tangem.common.ui.account.AccountIconUM
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import kotlinx.collections.immutable.ImmutableList
+
+@Immutable
+internal data class EditContactUM(
+ val title: TextReference,
+ val name: String,
+ val namePlaceholder: TextReference,
+ val portfolioIcon: AccountIconUM.CryptoPortfolio,
+ val colors: Colors,
+ val onNameChange: (String) -> Unit,
+ val onCloseClick: () -> Unit,
+) {
+
+ @Immutable
+ data class Colors(
+ val selected: CryptoPortfolioIcon.Color,
+ val list: ImmutableList,
+ val onColorSelect: (CryptoPortfolioIcon.Color) -> Unit,
+ )
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt
new file mode 100644
index 0000000000..1f1035618b
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModel.kt
@@ -0,0 +1,68 @@
+package com.tangem.features.addressbook.editcontact.model
+
+import com.tangem.common.ui.account.AccountIconUM
+import com.tangem.core.decompose.di.ModelScoped
+import com.tangem.core.decompose.model.Model
+import com.tangem.core.decompose.model.ParamsContainer
+import com.tangem.core.ui.R
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.features.addressbook.editcontact.EditContactComponent
+import com.tangem.features.addressbook.editcontact.contract.EditContactUM
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.update
+import javax.inject.Inject
+
+@ModelScoped
+internal class EditContactModel @Inject constructor(
+ paramsContainer: ParamsContainer,
+ override val dispatchers: CoroutineDispatcherProvider,
+) : Model() {
+
+ private val params: EditContactComponent.Params = paramsContainer.require()
+
+ val state: StateFlow
+ field = MutableStateFlow(getInitialState())
+
+ private fun onNameChange(name: String) {
+ state.update { it.copy(name = name) }
+ }
+
+ private fun onColorSelect(color: CryptoPortfolioIcon.Color) {
+ state.update { oldState ->
+ oldState.copy(
+ colors = oldState.colors.copy(selected = color),
+ portfolioIcon = oldState.portfolioIcon.copy(color = color),
+ )
+ }
+ }
+
+ private fun getInitialState(): EditContactUM {
+ val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
+ val selectedColor = colors.first()
+ val titleResId = if (params.contactId == null) {
+ R.string.address_book_new_contact
+ } else {
+ R.string.address_book_contact
+ }
+ return EditContactUM(
+ title = resourceReference(titleResId),
+ name = "",
+ namePlaceholder = resourceReference(R.string.address_book_new_contact),
+ portfolioIcon = AccountIconUM.CryptoPortfolio(
+ value = CryptoPortfolioIcon.Icon.Letter,
+ color = selectedColor,
+ ),
+ colors = EditContactUM.Colors(
+ selected = selectedColor,
+ list = colors,
+ onColorSelect = ::onColorSelect,
+ ),
+ onNameChange = ::onNameChange,
+ onCloseClick = params.onBackClick,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt
new file mode 100644
index 0000000000..2183d33c43
--- /dev/null
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/editcontact/ui/EditContactContent.kt
@@ -0,0 +1,185 @@
+package com.tangem.features.addressbook.editcontact.ui
+
+import android.content.res.Configuration
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import com.tangem.common.ui.account.AccountIcon
+import com.tangem.common.ui.account.AccountIconUM
+import com.tangem.common.ui.account.getUiColor
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.account.AccountIconSize
+import com.tangem.core.ui.components.fields.AutoSizeTextField
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.ds.topbar.TangemTopBar
+import com.tangem.core.ui.ds2.button.TangemButton
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.core.ui.res.TangemThemePreview
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.features.addressbook.editcontact.contract.EditContactUM
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+internal fun EditContactContent(state: EditContactUM, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxSize()
+ .background(color = TangemTheme.colors3.bg.primary)
+ .systemBarsPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ TangemTopBar(
+ modifier = Modifier.statusBarsPadding(),
+ title = state.title,
+ endContent = {
+ TangemButton(
+ iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24),
+ onClick = state.onCloseClick,
+ size = TangemButton.Size.X11,
+ variant = TangemButton.Variant.Material,
+ )
+ },
+ )
+
+ Column(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .weight(1f),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ ContactSummary(state = state)
+ ContactColor(colors = state.colors)
+ }
+ }
+}
+
+@Composable
+private fun ContactSummary(state: EditContactUM) {
+ val avatarName = state.name.ifBlank { state.namePlaceholder.resolveReference() }
+ Column(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .fillMaxWidth()
+ .background(TangemTheme.colors3.bg.secondary),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(modifier = Modifier.height(24.dp))
+
+ AccountIcon(
+ name = stringReference(avatarName),
+ icon = state.portfolioIcon,
+ size = AccountIconSize.Large,
+ )
+ Spacer(modifier = Modifier.height(24.dp))
+
+ Text(
+ text = stringResourceSafe(R.string.address_book_contact_name),
+ style = TangemTheme.typography3.caption.medium,
+ color = TangemTheme.colors3.text.tertiary,
+ )
+ Spacer(modifier = Modifier.height(2.dp))
+
+ AutoSizeTextField(
+ value = state.name,
+ onValueChange = state.onNameChange,
+ centered = true,
+ singleLine = true,
+ placeholder = state.namePlaceholder,
+ textStyle = TangemTheme.typography3.heading.medium,
+ color = TangemTheme.colors3.text.primary,
+ placeholderColor = TangemTheme.colors3.text.tertiary,
+ )
+ Spacer(modifier = Modifier.height(20.dp))
+ }
+}
+
+@OptIn(ExperimentalLayoutApi::class)
+@Suppress("MagicNumber")
+@Composable
+private fun ContactColor(colors: EditContactUM.Colors) {
+ Box(
+ modifier = Modifier
+ .clip(RoundedCornerShape(16.dp))
+ .fillMaxWidth()
+ .background(TangemTheme.colors3.bg.secondary),
+ ) {
+ FlowRow(
+ maxItemsInEachRow = 6,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
+ ) {
+ colors.list.fastForEach { color ->
+ val isSelected = color == colors.selected
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .clip(CircleShape)
+ .clickable(onClick = { colors.onColorSelect(color) })
+ .size(48.dp),
+ ) {
+ if (isSelected) {
+ Box(
+ modifier = Modifier
+ .size(47.dp)
+ .border(2.dp, color.getUiColor(), shape = CircleShape),
+ )
+ Box(
+ modifier = Modifier
+ .size(36.dp)
+ .background(color = color.getUiColor(), shape = CircleShape),
+ )
+ } else {
+ Box(
+ modifier = Modifier
+ .size(40.dp)
+ .background(color = color.getUiColor(), shape = CircleShape),
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun Preview_EditContactContent() {
+ val colors = CryptoPortfolioIcon.Color.entries.toImmutableList()
+ TangemThemePreview {
+ EditContactContent(
+ state = EditContactUM(
+ title = stringReference("New contact"),
+ name = "",
+ namePlaceholder = stringReference("New contact"),
+ portfolioIcon = AccountIconUM.CryptoPortfolio(
+ value = CryptoPortfolioIcon.Icon.Letter,
+ color = colors.first(),
+ ),
+ colors = EditContactUM.Colors(
+ selected = colors.first(),
+ list = colors,
+ onColorSelect = {},
+ ),
+ onNameChange = {},
+ onCloseClick = {},
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt
index 416edbd08c..63733b90f9 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/DefaultAddressBookListComponent.kt
@@ -23,9 +23,9 @@ internal class DefaultAddressBookListComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
- when (state) {
- AddressBookListUM.Empty -> AddressBookEmptyScreen(
- onAddContactClick = params.onAddContactClick,
+ when (val addressBookListUM = state) {
+ is AddressBookListUM.Empty -> AddressBookEmptyScreen(
+ tangemButtonUM = addressBookListUM.tangemButtonUM,
onBackClick = router::pop,
modifier = modifier,
)
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt
index f5cf907646..4c0c74bab4 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/contract/AddressBookListUM.kt
@@ -1,12 +1,15 @@
package com.tangem.features.addressbook.list.contract
import androidx.compose.runtime.Immutable
+import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.domain.addressbook.model.Contact
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class AddressBookListUM {
- data object Empty : AddressBookListUM()
+ data class Empty(
+ val tangemButtonUM: TangemButtonUM,
+ ) : AddressBookListUM()
data class AddressList(val contacts: ImmutableList) : AddressBookListUM()
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt
index 224fa6edd0..27039d82aa 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/model/AddressBookListModel.kt
@@ -2,6 +2,16 @@ package com.tangem.features.addressbook.list.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
+import com.tangem.core.decompose.model.ParamsContainer
+import com.tangem.core.ui.R
+import com.tangem.core.ui.R.drawable.ic_plus_24
+import com.tangem.core.ui.ds.button.TangemButtonIconPosition
+import com.tangem.core.ui.ds.button.TangemButtonType
+import com.tangem.core.ui.ds.button.TangemButtonUM
+import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.addressbook.list.AddressBookListComponent
import com.tangem.features.addressbook.list.contract.AddressBookListUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
@@ -10,10 +20,24 @@ import javax.inject.Inject
@ModelScoped
internal class AddressBookListModel @Inject constructor(
+ paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
+ private val params = paramsContainer.require()
+
val state: StateFlow = MutableStateFlow(
- AddressBookListUM.Empty,
+ AddressBookListUM.Empty(
+ tangemButtonUM = TangemButtonUM(
+ text = TextReference.Res(R.string.address_book_new_contact),
+ tangemIconUM = TangemIconUM.Icon(
+ iconRes = ic_plus_24,
+ tintReference = { TangemTheme.colors3.text.inverse.primary },
+ ),
+ iconPosition = TangemButtonIconPosition.End,
+ type = TangemButtonType.Primary,
+ onClick = params.onAddContactClick,
+ ),
+ ),
)
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt
index 8c258eb5eb..b1045b5cc2 100644
--- a/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt
+++ b/features/address-book/impl/src/main/kotlin/com/tangem/features/addressbook/list/ui/AddressBookEmptyScreen.kt
@@ -14,17 +14,21 @@ 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.R
-import com.tangem.core.ui.components.PrimaryButtonIconEnd
+import com.tangem.core.ui.ds.button.PrimaryTangemButton
+import com.tangem.core.ui.ds.button.TangemButtonIconPosition
+import com.tangem.core.ui.ds.button.TangemButtonType
+import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds2.button.TangemButton
+import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun AddressBookEmptyScreen(
- onAddContactClick: () -> Unit,
+ tangemButtonUM: TangemButtonUM,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -40,19 +44,17 @@ internal fun AddressBookEmptyScreen(
iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_back_24),
onClick = onBackClick,
size = TangemButton.Size.X11,
- variant = TangemButton.Variant.Secondary,
+ variant = TangemButton.Variant.Material,
)
},
)
NoContactInfo()
- PrimaryButtonIconEnd(
+ PrimaryTangemButton(
modifier = Modifier
.fillMaxWidth()
.navigationBarsPadding()
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp),
- text = stringResourceSafe(R.string.address_book_add_contact),
- iconResId = R.drawable.ic_plus_24,
- onClick = onAddContactClick,
+ buttonUM = tangemButtonUM,
)
}
}
@@ -104,5 +106,14 @@ private fun ContactImage() {
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_AddressBookEmptyScreen() {
- AddressBookEmptyScreen(onAddContactClick = {}, onBackClick = {})
+ AddressBookEmptyScreen(
+ tangemButtonUM = TangemButtonUM(
+ text = TextReference.Res(R.string.address_book_new_contact),
+ tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_plus_24),
+ iconPosition = TangemButtonIconPosition.End,
+ type = TangemButtonType.Secondary,
+ onClick = {},
+ ),
+ onBackClick = {},
+ )
}
\ No newline at end of file
diff --git a/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt
new file mode 100644
index 0000000000..d44b437435
--- /dev/null
+++ b/features/address-book/impl/src/test/kotlin/com/tangem/features/addressbook/editcontact/model/EditContactModelTest.kt
@@ -0,0 +1,113 @@
+package com.tangem.features.addressbook.editcontact.model
+
+import com.google.common.truth.Truth.assertThat
+import com.tangem.common.ui.account.AccountIconUM
+import com.tangem.core.decompose.model.MutableParamsContainer
+import com.tangem.core.decompose.model.ParamsContainer
+import com.tangem.core.ui.R
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.domain.addressbook.model.ContactId
+import com.tangem.domain.models.account.CryptoPortfolioIcon
+import com.tangem.features.addressbook.editcontact.EditContactComponent
+import com.tangem.features.addressbook.editcontact.contract.EditContactUM
+import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+internal class EditContactModelTest {
+
+ @Test
+ fun `WHEN model created THEN initial state is correct`() = runTest {
+ val expectedColors = CryptoPortfolioIcon.Color.entries.toImmutableList()
+ val expectedSelectedColor = expectedColors.first()
+
+ val model = createModel(testScope = this)
+ val state = model.state.value
+
+ val expected = EditContactUM(
+ title = resourceReference(R.string.address_book_new_contact),
+ name = "",
+ namePlaceholder = resourceReference(R.string.address_book_new_contact),
+ portfolioIcon = AccountIconUM.CryptoPortfolio(
+ value = CryptoPortfolioIcon.Icon.Letter,
+ color = expectedSelectedColor,
+ ),
+ colors = EditContactUM.Colors(
+ selected = expectedSelectedColor,
+ list = expectedColors,
+ onColorSelect = state.colors.onColorSelect,
+ ),
+ onNameChange = state.onNameChange,
+ onCloseClick = state.onCloseClick,
+ )
+ assertThat(state).isEqualTo(expected)
+ }
+
+ @Test
+ fun `GIVEN existing contactId WHEN model created THEN title is contact`() = runTest {
+ // Arrange
+ val params = EditContactComponent.Params(
+ contactId = ContactId(value = "contact-id"),
+ onBackClick = {},
+ )
+
+ // Act
+ val model = createModel(testScope = this, params = params)
+ val state = model.state.value
+
+ // Assert
+ assertThat(state.title).isEqualTo(resourceReference(R.string.address_book_contact))
+ }
+
+ @Test
+ fun `GIVEN initial state WHEN onNameChange THEN name updated`() = runTest {
+ val model = createModel(testScope = this)
+ val newName = "Satoshi"
+
+ model.state.value.onNameChange(newName)
+
+ assertThat(model.state.value.name).isEqualTo(newName)
+ }
+
+ @Test
+ fun `GIVEN initial state WHEN onColorSelect THEN selected color and portfolio icon updated`() = runTest {
+ val model = createModel(testScope = this)
+ val newColor = CryptoPortfolioIcon.Color.entries.last()
+
+ model.state.value.colors.onColorSelect(newColor)
+
+ val state = model.state.value
+ assertThat(state.colors.selected).isEqualTo(newColor)
+ assertThat(state.portfolioIcon.color).isEqualTo(newColor)
+ }
+
+ private fun createModel(
+ testScope: TestScope,
+ params: EditContactComponent.Params = EditContactComponent.Params(
+ contactId = null,
+ onBackClick = {},
+ ),
+ paramsContainer: ParamsContainer = MutableParamsContainer(value = params),
+ ): EditContactModel {
+ return EditContactModel(
+ paramsContainer = paramsContainer,
+ dispatchers = testScope.createTestingCoroutineDispatcherProvider(),
+ )
+ }
+
+ private fun TestScope.createTestingCoroutineDispatcherProvider(): TestingCoroutineDispatcherProvider {
+ val testDispatcher = StandardTestDispatcher(testScheduler)
+ return TestingCoroutineDispatcherProvider(
+ main = testDispatcher,
+ mainImmediate = testDispatcher,
+ io = testDispatcher,
+ default = testDispatcher,
+ single = testDispatcher,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/build.gradle.kts b/features/push-notification-settings/impl/build.gradle.kts
index bed7c654b6..b69c5cd2a8 100644
--- a/features/push-notification-settings/impl/build.gradle.kts
+++ b/features/push-notification-settings/impl/build.gradle.kts
@@ -31,6 +31,7 @@ dependencies {
implementation(projects.domain.pushNotificationPreferences)
/* AndroidX */
+ implementation(deps.androidx.activity.compose)
implementation(deps.lifecycle.compose)
/* Compose */
@@ -50,7 +51,7 @@ dependencies {
implementation(deps.kotlin.immutable.collections)
/* Tests */
- testImplementation(deps.test.junit)
+ testImplementation(deps.test.junit5)
testImplementation(deps.test.mockk)
testImplementation(deps.test.coroutine)
testImplementation(deps.test.truth)
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt
new file mode 100644
index 0000000000..491b9dcb17
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/component/DefaultPushNotificationSettingsComponent.kt
@@ -0,0 +1,96 @@
+package com.tangem.features.pushnotificationsettings.impl.component
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.arkivanov.decompose.ComponentContext
+import com.arkivanov.decompose.extensions.compose.subscribeAsState
+import com.arkivanov.decompose.router.slot.childSlot
+import com.arkivanov.decompose.router.slot.dismiss
+import com.arkivanov.essenty.lifecycle.doOnResume
+import com.tangem.core.decompose.context.AppComponentContext
+import com.tangem.core.decompose.context.childByContext
+import com.tangem.core.decompose.model.getOrCreateModel
+import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
+import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent
+import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull
+import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
+import com.tangem.features.pushnotificationsettings.impl.entity.NetworksAvailableForNotificationBSConfig
+import com.tangem.features.pushnotificationsettings.impl.model.PushNotificationSettingsModel
+import com.tangem.features.pushnotificationsettings.impl.ui.PushNotificationSettingsScreen
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+internal class DefaultPushNotificationSettingsComponent @AssistedInject constructor(
+ @Assisted context: AppComponentContext,
+ @Assisted private val params: PushNotificationSettingsComponent.Params,
+ private val networksAvailableForNotificationsComponentFactory: NetworksAvailableForNotificationsComponent.Factory,
+) : PushNotificationSettingsComponent, AppComponentContext by context {
+
+ private val model: PushNotificationSettingsModel = getOrCreateModel(params)
+
+ private val bottomSheetSlot = childSlot(
+ source = model.bottomSheetNavigation,
+ serializer = null,
+ handleBackButton = false,
+ key = "moreInfoBottomSheet",
+ childFactory = ::bottomSheetChild,
+ )
+
+ init {
+ lifecycle.doOnResume { model.onResume() }
+ }
+
+ @Composable
+ override fun Content(modifier: Modifier) {
+ val state by model.uiState.collectAsStateWithLifecycle()
+ val bottomSheet by bottomSheetSlot.subscribeAsState()
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ contract = ActivityResultContracts.RequestPermission(),
+ onResult = model::onPermissionResult,
+ )
+
+ LaunchedEffect(Unit) {
+ model.requestPushPermission.collect {
+ val permission = getPushPermissionOrNull()
+ if (permission != null) {
+ permissionLauncher.launch(permission)
+ } else {
+ model.onPermissionResult(isGranted = false)
+ }
+ }
+ }
+
+ PushNotificationSettingsScreen(
+ modifier = modifier,
+ state = state,
+ onBackClick = router::pop,
+ )
+
+ bottomSheet.child?.instance?.BottomSheet()
+ }
+
+ private fun bottomSheetChild(
+ @Suppress("UNUSED_PARAMETER") config: NetworksAvailableForNotificationBSConfig,
+ componentContext: ComponentContext,
+ ): ComposableBottomSheetComponent = networksAvailableForNotificationsComponentFactory.create(
+ context = childByContext(componentContext),
+ params = NetworksAvailableForNotificationsComponent.Params(
+ onDismiss = model.bottomSheetNavigation::dismiss,
+ ),
+ )
+
+ @AssistedFactory
+ interface Factory : PushNotificationSettingsComponent.Factory {
+ override fun create(
+ context: AppComponentContext,
+ params: PushNotificationSettingsComponent.Params,
+ ): DefaultPushNotificationSettingsComponent
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt
new file mode 100644
index 0000000000..b6ecab63ea
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/di/PushNotificationSettingsComponentModule.kt
@@ -0,0 +1,20 @@
+package com.tangem.features.pushnotificationsettings.impl.di
+
+import com.tangem.features.pushnotificationsettings.component.PushNotificationSettingsComponent
+import com.tangem.features.pushnotificationsettings.impl.component.DefaultPushNotificationSettingsComponent
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+internal interface PushNotificationSettingsComponentModule {
+
+ @Binds
+ @Singleton
+ fun bindPushNotificationSettingsComponentFactory(
+ factory: DefaultPushNotificationSettingsComponent.Factory,
+ ): PushNotificationSettingsComponent.Factory
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
index f31df5f7cf..b24b6ed544 100644
--- a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/entity/PushNotificationSettingsUM.kt
@@ -1,7 +1,6 @@
package com.tangem.features.pushnotificationsettings.impl.entity
import androidx.compose.runtime.Immutable
-import com.tangem.core.ui.event.StateEvent
import kotlinx.collections.immutable.PersistentList
@Immutable
@@ -12,7 +11,6 @@ internal sealed interface PushNotificationSettingsUM {
data class Content(
val banner: AllowPushNotificationsBannerUM?,
val toggles: PersistentList,
- val requestPermissionEvent: StateEvent,
val onMoreInfoClick: () -> Unit,
) : PushNotificationSettingsUM
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
index f27b886fbb..cbc841a27c 100644
--- a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModel.kt
@@ -9,9 +9,6 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.notifications.SystemNotificationsStateProvider
import com.tangem.core.navigation.settings.SettingsManager
-import com.tangem.core.ui.event.StateEvent
-import com.tangem.core.ui.event.consumedEvent
-import com.tangem.core.ui.event.triggeredEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.DialogMessage
@@ -38,6 +35,8 @@ import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.toPersistentList
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
@@ -45,6 +44,7 @@ import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@@ -69,7 +69,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
private val loadState = MutableStateFlow(LoadState.Loading)
private val osNotificationsEnabled = MutableStateFlow(systemNotificationsStateProvider.areNotificationsEnabled())
- private val pendingRequest = MutableStateFlow>(consumedEvent())
private var pendingPermissionToggle: ToggleSpec? = null
private val preferencesJobHolder = JobHolder()
@@ -77,15 +76,19 @@ internal class PushNotificationSettingsModel @Inject constructor(
private val cachedPrefs: WalletPushNotificationPreferences?
get() = (loadState.value as? LoadState.Content)?.prefs
+ private val requestPushPermissionChannel = Channel(Channel.BUFFERED)
+
+ /** One-shot requests to launch the system push permission prompt, consumed by the component. */
+ val requestPushPermission: Flow = requestPushPermissionChannel.receiveAsFlow()
+
val uiState: StateFlow = combine(
loadState,
osNotificationsEnabled,
- pendingRequest,
- ) { load, osEnabled, request ->
+ ) { load, osEnabled ->
when (load) {
is LoadState.Failed -> PushNotificationSettingsUM.Error(onRetryClick = ::onRetry)
is LoadState.Loading -> PushNotificationSettingsUM.Loading
- is LoadState.Content -> buildContent(prefs = load.prefs, osEnabled = osEnabled, request = request)
+ is LoadState.Content -> buildContent(prefs = load.prefs, osEnabled = osEnabled)
}
}.stateIn(
scope = modelScope,
@@ -117,7 +120,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
}
fun onPermissionResult(isGranted: Boolean) {
- pendingRequest.value = consumedEvent()
val tapped = pendingPermissionToggle
pendingPermissionToggle = null
modelScope.launch {
@@ -145,12 +147,10 @@ internal class PushNotificationSettingsModel @Inject constructor(
private fun buildContent(
prefs: WalletPushNotificationPreferences,
osEnabled: Boolean,
- request: StateEvent,
): PushNotificationSettingsUM.Content {
return PushNotificationSettingsUM.Content(
banner = buildBanner(prefs = prefs, osEnabled = osEnabled),
toggles = buildToggles(prefs),
- requestPermissionEvent = request,
onMoreInfoClick = ::onMoreInfoClick,
)
}
@@ -190,7 +190,7 @@ internal class PushNotificationSettingsModel @Inject constructor(
private fun requestPermission(tapped: ToggleSpec? = null) {
pendingPermissionToggle = tapped
- pendingRequest.value = triggeredEvent(data = Unit, onConsume = ::onPermissionEventConsumed)
+ requestPushPermissionChannel.trySend(Unit)
}
private fun onBannerCtaClick() {
@@ -219,10 +219,6 @@ internal class PushNotificationSettingsModel @Inject constructor(
applyOptimisticToggle(spec, newValue)
}
- private fun onPermissionEventConsumed() {
- pendingRequest.value = consumedEvent()
- }
-
private fun applyOptimisticToggle(spec: ToggleSpec, newValue: Boolean) {
val current = cachedPrefs ?: return
loadState.value = LoadState.Content(current.withCategory(spec.category, newValue))
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt
new file mode 100644
index 0000000000..da20ec497a
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/AllowPushNotificationsBanner.kt
@@ -0,0 +1,28 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.R as CoreUiR
+import com.tangem.core.ui.components.notifications.Notification
+import com.tangem.core.ui.components.notifications.NotificationConfig
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM
+
+@Composable
+internal fun AllowPushNotificationsBanner(state: AllowPushNotificationsBannerUM, modifier: Modifier = Modifier) {
+ Notification(
+ modifier = modifier.fillMaxWidth(),
+ config = NotificationConfig(
+ title = resourceReference(R.string.push_notification_settings_banner_title),
+ subtitle = resourceReference(R.string.push_notification_settings_banner_description),
+ iconResId = CoreUiR.drawable.ic_alert_circle_24,
+ iconTint = NotificationConfig.IconTint.Warning,
+ buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
+ text = resourceReference(R.string.common_open_settings_button_title),
+ onClick = state.onOpenSettingsClick,
+ ),
+ ),
+ )
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt
new file mode 100644
index 0000000000..751b904642
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/NotificationSettingRow.kt
@@ -0,0 +1,92 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+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 com.tangem.core.ui.components.TangemSwitch
+import com.tangem.core.ui.components.block.BlockCard
+import com.tangem.core.ui.extensions.TextReference
+import com.tangem.core.ui.extensions.combinedReference
+import com.tangem.core.ui.extensions.resolveAnnotatedReference
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.extensions.styledResourceReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.ToggleUM
+
+@Composable
+internal fun NotificationSettingRow(
+ toggle: ToggleUM,
+ showInlineMoreInfoLink: Boolean,
+ onMoreInfoClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ BlockCard(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
+ ) {
+ Text(
+ modifier = Modifier.weight(1f),
+ text = stringResourceSafe(id = toggle.titleRes),
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.primary1,
+ )
+ TangemSwitch(
+ checked = toggle.isOn,
+ onCheckedChange = toggle.onCheckedChange,
+ )
+ }
+ }
+
+ SubtitleText(
+ base = toggle.subtitle,
+ showMoreInfo = showInlineMoreInfoLink,
+ onMoreInfoClick = onMoreInfoClick,
+ )
+ }
+}
+
+@Composable
+private fun SubtitleText(base: TextReference, showMoreInfo: Boolean, onMoreInfoClick: () -> Unit) {
+ val reference: TextReference = if (showMoreInfo) {
+ combinedReference(
+ base,
+ stringReference(" "),
+ styledResourceReference(
+ id = R.string.push_notifications_more_info,
+ spanStyleReference = {
+ TangemTheme.typography.caption2
+ .copy(color = TangemTheme.colors.text.accent)
+ .toSpanStyle()
+ },
+ onClick = onMoreInfoClick,
+ ),
+ )
+ } else {
+ base
+ }
+ Text(
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
+ text = reference.resolveAnnotatedReference(),
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ )
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt
new file mode 100644
index 0000000000..391208437f
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsContent.kt
@@ -0,0 +1,68 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.entity.AllowPushNotificationsBannerUM
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+import com.tangem.features.pushnotificationsettings.impl.entity.ToggleId
+
+@Composable
+internal fun PushNotificationSettingsContent(
+ state: PushNotificationSettingsUM.Content,
+ modifier: Modifier = Modifier,
+) {
+ LazyColumn(
+ modifier = modifier
+ .fillMaxSize()
+ .navigationBarsPadding(),
+ contentPadding = PaddingValues(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
+ ) {
+ item(key = "banner") {
+ AnimatedBannerSlot(banner = state.banner)
+ }
+
+ items(items = state.toggles, key = { it.id.name }) { toggle ->
+ NotificationSettingRow(
+ modifier = Modifier.animateItem(),
+ toggle = toggle,
+ showInlineMoreInfoLink = toggle.id == ToggleId.TransactionAlerts,
+ onMoreInfoClick = state.onMoreInfoClick,
+ )
+ }
+ }
+}
+
+@Composable
+private fun AnimatedBannerSlot(banner: AllowPushNotificationsBannerUM?) {
+ var lastVisible by remember { mutableStateOf(banner) }
+ if (banner != null) {
+ lastVisible = banner
+ }
+ AnimatedVisibility(
+ visible = banner != null,
+ enter = fadeIn() + expandVertically(),
+ exit = fadeOut() + shrinkVertically(),
+ ) {
+ lastVisible?.let { AllowPushNotificationsBanner(state = it) }
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt
new file mode 100644
index 0000000000..46c152f6f0
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsError.kt
@@ -0,0 +1,19 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.UnableToLoadData
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+
+@Composable
+internal fun PushNotificationSettingsError(state: PushNotificationSettingsUM.Error, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ UnableToLoadData(onRetryClick = state.onRetryClick)
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt
new file mode 100644
index 0000000000..bc2bcf7d3d
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsLoading.kt
@@ -0,0 +1,80 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.RectangleShimmer
+import com.tangem.core.ui.components.block.BlockCard
+import com.tangem.core.ui.res.TangemTheme
+
+@Composable
+internal fun PushNotificationSettingsLoading(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
+ ) {
+ repeat(SHIMMER_ROW_COUNT) {
+ ShimmerRow()
+ }
+ }
+}
+
+@Composable
+private fun ShimmerRow(modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ BlockCard(modifier = Modifier.fillMaxWidth()) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ horizontal = TangemTheme.dimens.spacing16,
+ vertical = TangemTheme.dimens.spacing12,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ RectangleShimmer(
+ modifier = Modifier
+ .width(SHIMMER_TITLE_WIDTH)
+ .height(SHIMMER_TITLE_HEIGHT),
+ )
+ RectangleShimmer(
+ modifier = Modifier
+ .width(SHIMMER_SWITCH_WIDTH)
+ .height(SHIMMER_SWITCH_HEIGHT),
+ radius = TangemTheme.dimens.radius12,
+ )
+ }
+ }
+ RectangleShimmer(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.spacing16)
+ .width(SHIMMER_SUBTITLE_WIDTH)
+ .height(SHIMMER_SUBTITLE_HEIGHT),
+ )
+ }
+}
+
+private const val SHIMMER_ROW_COUNT = 3
+private val SHIMMER_TITLE_WIDTH = 160.dp
+private val SHIMMER_TITLE_HEIGHT = 18.dp
+private val SHIMMER_SWITCH_WIDTH = 40.dp
+private val SHIMMER_SWITCH_HEIGHT = 22.dp
+private val SHIMMER_SUBTITLE_WIDTH = 220.dp
+private val SHIMMER_SUBTITLE_HEIGHT = 14.dp
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt
new file mode 100644
index 0000000000..586d61f56a
--- /dev/null
+++ b/features/push-notification-settings/impl/src/main/kotlin/com/tangem/features/pushnotificationsettings/impl/ui/PushNotificationSettingsScreen.kt
@@ -0,0 +1,46 @@
+package com.tangem.features.pushnotificationsettings.impl.ui
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.appbar.TangemTopAppBar
+import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
+import com.tangem.core.ui.extensions.resourceReference
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.features.pushnotificationsettings.impl.R
+import com.tangem.features.pushnotificationsettings.impl.entity.PushNotificationSettingsUM
+
+@Composable
+internal fun PushNotificationSettingsScreen(
+ state: PushNotificationSettingsUM,
+ onBackClick: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ Scaffold(
+ modifier = modifier,
+ containerColor = TangemTheme.colors.background.secondary,
+ topBar = {
+ TangemTopAppBar(
+ modifier = Modifier.statusBarsPadding(),
+ title = resourceReference(R.string.push_notification_settings_title),
+ startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick),
+ )
+ },
+ ) { padding ->
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding),
+ ) {
+ when (state) {
+ is PushNotificationSettingsUM.Loading -> PushNotificationSettingsLoading()
+ is PushNotificationSettingsUM.Content -> PushNotificationSettingsContent(state = state)
+ is PushNotificationSettingsUM.Error -> PushNotificationSettingsError(state = state)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
index 152d433421..500cfcf9b6 100644
--- a/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
+++ b/features/push-notification-settings/impl/src/test/kotlin/com/tangem/features/pushnotificationsettings/impl/model/PushNotificationSettingsModelTest.kt
@@ -29,7 +29,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
-import org.junit.Test
+import org.junit.jupiter.api.Test
@Suppress("LongParameterList")
class PushNotificationSettingsModelTest {
@@ -204,13 +204,15 @@ class PushNotificationSettingsModelTest {
val model = model(osEnabled = false, preferencesFlow = flow)
advanceUntilIdle()
- val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
- .toggles.first { it.id == ToggleId.OffersUpdates }
- offers.onCheckedChange(true)
- advanceUntilIdle()
+ model.requestPushPermission.test {
+ val offers = (model.uiState.value as PushNotificationSettingsUM.Content)
+ .toggles.first { it.id == ToggleId.OffersUpdates }
+ offers.onCheckedChange(true)
+ advanceUntilIdle()
- val content = model.uiState.value as PushNotificationSettingsUM.Content
- assertThat(content.requestPermissionEvent.javaClass.simpleName).isEqualTo("Triggered")
+ awaitItem()
+ expectNoEvents()
+ }
coVerify(exactly = 0) { updatePreference(any(), any(), any()) }
}
@@ -224,12 +226,14 @@ class PushNotificationSettingsModelTest {
val banner = requireNotNull(
(model.uiState.value as PushNotificationSettingsUM.Content).banner,
)
- banner.onOpenSettingsClick()
- advanceUntilIdle()
+ model.requestPushPermission.test {
+ banner.onOpenSettingsClick()
+ advanceUntilIdle()
+ // The banner CTA opens system settings directly and never asks for the permission.
+ expectNoEvents()
+ }
verify(exactly = 1) { settingsManager.openAppNotificationSettings() }
- val refreshed = model.uiState.value as PushNotificationSettingsUM.Content
- assertThat(refreshed.requestPermissionEvent.javaClass.simpleName).isEqualTo("Consumed")
}
@Test
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt
index 86a15ad9e0..419e8a6b2b 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/subcomponents/destination/model/SendDestinationModel.kt
@@ -271,7 +271,7 @@ internal class SendDestinationModel @Inject constructor(
private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? {
val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null
val (paymentAccountAddress, currency) = when (val status = this.value) {
- is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency
+ is PaymentAccountStatusValue.Loaded -> status.balance.cryptoBalance.depositAddress to status.cryptoCurrency
else -> return null
}
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt
index a3b81fab45..9b33e3f33a 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt
@@ -97,7 +97,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor(
?.takeIf { it.period == TangemPayCardLimitPeriod.DAY }
?.amount
- val currency = getJavaCurrencyByCode(status.currencyCode)
+ val currency = getJavaCurrencyByCode(status.balance.fiatBalance.currency)
uiState.update { state ->
val amount = if (index == 0) {
currentLimit?.stripTrailingZeros()?.toPlainString().orEmpty()
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt
index 9acc3c9982..31c439172e 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt
@@ -118,8 +118,9 @@ internal class TangemPayCardPageModel @Inject constructor(
val dailyLimitState = if (limit != null) {
TangemPayDailyLimitBlockState.Content(
limit = limit.amount.format {
- val symbol = getJavaCurrencyByCode(status.currencyCode).symbol
- fiat(status.currencyCode, symbol).optionalDecimals()
+ val currencyCode = status.balance.fiatBalance.currency
+ val symbol = getJavaCurrencyByCode(currencyCode).symbol
+ fiat(currencyCode, symbol).optionalDecimals()
},
onChangeClick = ::onClickLimitChange,
)
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt
index 5e47660a9b..67e7bc30fe 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt
@@ -25,8 +25,8 @@ import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.pay.TangemPayCardFrozenState
import com.tangem.domain.models.pay.TangemPayCardState
+import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher
import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier
-import com.tangem.domain.pay.model.TangemPayCardBalance
import com.tangem.domain.pay.model.TangemPayTopUpData
import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository
import com.tangem.domain.pay.repository.TangemPayWithdrawRepository
@@ -40,8 +40,6 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsErrorType
import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation
import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
-import com.tangem.features.tangempay.model.listener.CardDetailsEvent
-import com.tangem.features.tangempay.model.listener.CardDetailsEventListener
import com.tangem.features.tangempay.model.transformers.*
import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute
import com.tangem.features.tangempay.utils.*
@@ -52,7 +50,6 @@ import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import com.tangem.utils.logging.TangemLogger
import com.tangem.utils.transformer.update
-import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -70,33 +67,36 @@ internal class TangemPayDetailsModel @Inject constructor(
private val cardDetailsRepository: TangemPayCardDetailsRepository,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val uiMessageSender: UiMessageSender,
- private val cardDetailsEventListener: CardDetailsEventListener,
private val txHistoryUpdateListener: TangemPayTxHistoryUpdateListener,
private val tangemPayWithdrawRepository: TangemPayWithdrawRepository,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val expressTransactionsEventListener: ExpressTransactionsEventListener,
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
+ private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
) : Model(), TangemPayTxHistoryUiActions, TangemPayDetailIntents, AddFundsListener {
private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require()
- private val userWalletId = params.initialStatus.userWalletId
private val isTangemPayDeactivated = params.initialStatus.isDeactivated
- private val loaded: PaymentAccountStatusValue.Loaded? =
- params.initialStatus.value as? PaymentAccountStatusValue.Loaded
- private val firstCard = loaded?.cards?.firstOrNull()
- val cryptoCurrency: CryptoCurrency = params.initialStatus.cryptoCurrency
- private val initialCardFrozenState: TangemPayCardFrozenState = when {
- firstCard == null -> TangemPayCardFrozenState.Unfrozen
- else -> firstCard.frozenState
- }
+ private val initialCard = params.initialStatus.ifLoadedOrNull { it.cards.firstOrNull() }
+
+ private val currentStatus = MutableStateFlow(params.initialStatus)
+
+ private val userWalletId
+ get() = currentStatus.value.userWalletId
+
+ val cryptoCurrency
+ get() = currentStatus.value.cryptoCurrency
private val stateFactory = TangemPayDetailsStateFactory(
onBack = router::pop,
onOpenMenu = ::onOpenMenu,
intents = this,
- cardFrozenState = initialCardFrozenState,
+ cardFrozenState = when {
+ initialCard == null -> TangemPayCardFrozenState.Unfrozen
+ else -> initialCard.frozenState
+ },
isRedesignEnabled = isRedesignEnabled(),
)
@@ -104,39 +104,49 @@ internal class TangemPayDetailsModel @Inject constructor(
field = MutableStateFlow(
stateFactory.getInitialState(
isTangemPayDeactivated = isTangemPayDeactivated,
- cardNumberEnd = firstCard?.lastDigits.orEmpty(),
- isReissuing = firstCard == null || firstCard.state != TangemPayCardState.Active,
+ cardNumberEnd = initialCard?.lastDigits.orEmpty(),
+ isReissuing = initialCard == null || initialCard.state != TangemPayCardState.Active,
),
)
private val refreshStateJobHolder = JobHolder()
- private val fetchBalanceJobHolder = JobHolder()
private val addToWalletBannerJobHolder = JobHolder()
- private var balance: TangemPayCardBalance? = null
-
val bottomSheetNavigation: SlotNavigation = SlotNavigation()
init {
analytics.send(TangemPayAnalyticsEvents.MainScreenOpened())
handleBalanceHiding()
- fetchBalance()
- if (!isTangemPayDeactivated && firstCard != null) {
- subscribeToCardFrozenState(firstCard.id)
- fetchAddToWalletBanner()
- paymentAccountStatusSupplier.invoke(userWalletId)
- .map { it.value }
+ val statusFlow = paymentAccountStatusSupplier.invoke(userWalletId)
+ .onEach { status -> currentStatus.update { status } }
+ .map { it.value }
+
+ if (isTangemPayDeactivated) {
+ statusFlow
+ .filterIsInstance()
+ .onEach { state ->
+ uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
+ }
+ .launchIn(modelScope)
+ } else {
+ if (initialCard != null) {
+ subscribeToCardFrozenState(initialCard.id)
+ }
+ fetchAddToWalletBanner()
+ statusFlow
.filterIsInstance()
.filter { it.source == StatusSource.ACTUAL }
.onEach { state ->
- val card = state.cards.firstOrNull() ?: return@onEach
- uiState.update(
- TangemPayCardDataTransformer(
- card = card,
- onCardClick = { onCardClick() },
- ),
- )
+ uiState.update(DetailsBalanceTransformer(state.balance.fiatBalance))
+ state.cards.firstOrNull()?.let { card ->
+ uiState.update(
+ TangemPayCardDataTransformer(
+ card = card,
+ onCardClick = { onCardClick() },
+ ),
+ )
+ }
}
.launchIn(modelScope)
}
@@ -165,17 +175,16 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onClickAddFunds() {
analytics.send(TangemPayAnalyticsEvents.AddFundsClicked())
- val currentBalance = balance
- val depositAddress = currentBalance?.depositAddress
- if (currentBalance == null || depositAddress == null) {
+ val balance = currentStatus.value.balanceOrNull()
+ if (balance == null) {
showBottomSheetError(TangemPayDetailsErrorType.Receive)
} else {
bottomSheetNavigation.activate(
TangemPayDetailsNavigation.AddFunds(
walletId = userWalletId,
- fiatBalance = currentBalance.availableForWithdrawal,
- cryptoBalance = currentBalance.availableForWithdrawal,
- depositAddress = depositAddress,
+ fiatBalance = balance.availableForWithdrawal,
+ cryptoBalance = balance.availableForWithdrawal,
+ depositAddress = balance.cryptoBalance.depositAddress,
cryptoCurrency = cryptoCurrency,
),
)
@@ -184,12 +193,6 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onClickWithdraw() {
analytics.send(TangemPayAnalyticsEvents.WithdrawClicked())
- val currentBalance = balance
- val depositAddress = currentBalance?.depositAddress
- if (currentBalance == null || depositAddress == null) {
- showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
- return
- }
modelScope.launch {
val hasActiveWithdrawal = tangemPayWithdrawRepository.hasWithdrawOrder(userWalletId)
if (hasActiveWithdrawal) {
@@ -197,18 +200,19 @@ internal class TangemPayDetailsModel @Inject constructor(
} else {
uiMessageSender.send(
message = TangemPayMessagesFactory.createWithdrawWarning(
- onGotItClick = { onConfirmWithdrawal(cryptoCurrency, currentBalance, depositAddress) },
+ onGotItClick = { onConfirmWithdrawal(cryptoCurrency) },
),
)
}
}
}
- private fun onConfirmWithdrawal(
- currency: CryptoCurrency,
- currentBalance: TangemPayCardBalance,
- depositAddress: String,
- ) {
+ private fun onConfirmWithdrawal(currency: CryptoCurrency) {
+ val balance = currentStatus.value.balanceOrNull()
+ if (balance == null) {
+ showBottomSheetError(TangemPayDetailsErrorType.Withdraw)
+ return
+ }
router.push(
AppRoute.Swap(
cryptoCurrency = currency,
@@ -216,27 +220,15 @@ internal class TangemPayDetailsModel @Inject constructor(
screenSource = AnalyticsParam.ScreensSources.TangemPay.value,
currencyPosition = AppRoute.Swap.CurrencyPosition.FROM,
tangemPayInput = AppRoute.Swap.TangemPayInput(
- cryptoAmount = currentBalance.availableForWithdrawal,
- fiatAmount = currentBalance.availableForWithdrawal,
- depositAddress = depositAddress,
+ cryptoAmount = balance.availableForWithdrawal,
+ fiatAmount = balance.availableForWithdrawal,
+ depositAddress = balance.cryptoBalance.depositAddress,
isWithdrawal = true,
),
),
)
}
- private fun fetchBalance(): Job {
- return modelScope.launch {
- val result = try {
- cardDetailsRepository.getCardBalance(userWalletId).onRight { balance = it }
- } catch (e: Exception) {
- TangemLogger.e("Error", e)
- return@launch
- }
- uiState.update(transformer = DetailsBalanceTransformer(balance = result))
- }.saveIn(fetchBalanceJobHolder)
- }
-
private fun fetchAddToWalletBanner() {
modelScope.launch {
val isDone = try {
@@ -263,7 +255,7 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onContactSupportClicked() {
analytics.send(Basic.ButtonSupport(source = AnalyticsParam.ScreensSources.TangemPay))
- val customerId = loaded?.customerId ?: return
+ val customerId = currentStatus.value.ifLoadedOrNull { it.customerId } ?: return
modelScope.launch {
sendFeedbackEmailUseCase.invoke(
type = FeedbackEmailType.Visa.FeatureIsBeta(
@@ -277,10 +269,9 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onRefreshSwipe(refreshState: ShowRefreshState) {
modelScope.launch {
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = refreshState.value))
- cardDetailsEventListener.send(CardDetailsEvent.Hide)
+ paymentAccountStatusFetcher.invoke(userWalletId)
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
txHistoryUpdateListener.triggerUpdate()
- fetchBalance().join()
uiState.update(TangemPayDetailsRefreshTransformer(isRefreshing = false))
}.saveIn(refreshStateJobHolder)
}
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt
index eca90fdbb4..5cbd4bdb05 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsBalanceTransformer.kt
@@ -1,45 +1,32 @@
package com.tangem.features.tangempay.model.transformers
-import arrow.core.Either
-import com.tangem.core.error.UniversalError
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.res.TangemTheme
-import com.tangem.domain.pay.model.TangemPayCardBalance
+import com.tangem.domain.models.account.PaymentAccountStatusValue
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
import com.tangem.utils.transformer.Transformer
-import kotlinx.collections.immutable.persistentListOf
import java.util.Currency
internal class DetailsBalanceTransformer(
- private val balance: Either,
+ private val fiatBalance: PaymentAccountStatusValue.FiatBalance,
) : Transformer {
override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM {
- val balance = when (balance) {
- is Either.Left -> {
- TangemPayDetailsBalanceBlockState.Error(
- actionButtons = persistentListOf(),
- cardsBlockState = prevState.balanceBlockState.cardsBlockState,
- )
- }
- is Either.Right -> {
- TangemPayDetailsBalanceBlockState.Content(
- isBalanceFlickering = false,
- fiatBalance = getFiatBalanceText(balance.value),
- actionButtons = prevState.balanceBlockState.actionButtons,
- cardsBlockState = prevState.balanceBlockState.cardsBlockState,
- )
- }
- }
+ val balance = TangemPayDetailsBalanceBlockState.Content(
+ isBalanceFlickering = false,
+ fiatBalance = getFiatBalanceText(fiatBalance),
+ actionButtons = prevState.balanceBlockState.actionButtons,
+ cardsBlockState = prevState.balanceBlockState.cardsBlockState,
+ )
return prevState.copy(balanceBlockState = balance)
}
- private fun getFiatBalanceText(balance: TangemPayCardBalance): TextReference {
- val currency = Currency.getInstance(balance.currencyCode)
- return balance.fiatBalance.formatStyled {
+ private fun getFiatBalanceText(fiatBalance: PaymentAccountStatusValue.FiatBalance): TextReference {
+ val currency = Currency.getInstance(fiatBalance.currency)
+ return fiatBalance.availableBalance.formatStyled {
fiat(
fiatCurrencyCode = currency.currencyCode,
fiatCurrencySymbol = currency.symbol,
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt
index 24f15fce6d..4b87b1f576 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt
@@ -108,14 +108,11 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M
@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries")
@Composable
private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modifier: Modifier = Modifier) {
- val isRenaming = state.displayNameState is DisplayNameState.Editing
-
Box(modifier = modifier.fillMaxSize()) {
TangemPayCardBackground(
modifier = Modifier
.fillMaxSize()
.zIndex(0f),
- isRenaming = isRenaming,
cardFrozenState = state.cardFrozenState,
)
@@ -147,6 +144,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
)
}
CardNumberBlock(
+ isRenaming = state.displayNameState is DisplayNameState.Editing,
numberShort = state.numberShort,
cardNumberRef = cardNumberRef,
)
@@ -203,11 +201,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif
}
@Composable
-private fun TangemPayCardBackground(
- isRenaming: Boolean,
- cardFrozenState: TangemPayCardFrozenState,
- modifier: Modifier = Modifier,
-) {
+private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, modifier: Modifier = Modifier) {
val isFrozen = cardFrozenState == TangemPayCardFrozenState.Frozen
val freezeProgress by animateFloatAsState(
targetValue = if (isFrozen) 1f else 0f,
@@ -225,14 +219,6 @@ private fun TangemPayCardBackground(
contentDescription = null,
)
- if (isRenaming && LocalVisaRedesignEnabled.current) {
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(CardBackgroundColor.copy(alpha = 0.8f)),
- )
- }
-
if (isFrozen || freezeProgress > 0f) {
Image(
modifier = Modifier
@@ -344,6 +330,7 @@ private fun CardTopBlock(modifier: Modifier = Modifier) {
@Composable
private fun ConstraintLayoutScope.CardNumberBlock(
+ isRenaming: Boolean,
numberShort: String,
cardNumberRef: ConstrainedLayoutReference,
modifier: Modifier = Modifier,
@@ -352,7 +339,11 @@ private fun ConstraintLayoutScope.CardNumberBlock(
Text(
text = numberShort,
style = TangemTheme.typography3.body.medium,
- color = TangemTheme.colors3.text.staticDark.primary,
+ color = if (isRenaming) {
+ TangemTheme.colors3.text.staticDark.secondary
+ } else {
+ TangemTheme.colors3.text.staticDark.primary
+ },
modifier = modifier
.constrainAs(cardNumberRef) {
start.linkTo(parent.start)
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreenV2.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreenV2.kt
index 26c4737f33..40e461ac4e 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreenV2.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayChangePinScreenV2.kt
@@ -34,7 +34,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
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.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_cross_20
import com.tangem.core.ui.test.TangemPayTestTags
@@ -54,7 +54,7 @@ internal fun TangemPayChangePinScreenV2(
.statusBarsPadding(),
) {
TangemTopBar(
- title = resourceReference(R.string.tangempay_set_pin_title),
+ title = resourceReference(R.string.visa_onboarding_pin_code_title),
endContent = {
TangemButton(
iconStart = TangemIconUM.Icon(imageVector = Icons.ic_cross_20),
@@ -73,7 +73,7 @@ internal fun TangemPayChangePinScreenV2(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
- text = stringResourceSafe(R.string.tangempay_set_pin_header),
+ text = stringResourceSafe(R.string.visa_onboarding_pin_code_description),
style = TangemTheme.typography3.body.medium,
color = TangemTheme.colors3.text.secondary,
textAlign = TextAlign.Center,
@@ -94,6 +94,7 @@ private fun PinCodeSection(state: TangemPayChangePinUM, modifier: Modifier = Mod
val focusRequester = remember { FocusRequester() }
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
PinCode(
+ isError = state.error != null,
value = state.pinCode,
onValueChange = state.onPinCodeChange,
focusRequester = focusRequester,
@@ -109,7 +110,7 @@ private fun PinCodeSection(state: TangemPayChangePinUM, modifier: Modifier = Mod
Text(
text = error.resolveReference(),
style = TangemTheme.typography3.caption.medium,
- color = TangemTheme.colors3.text.status.warning,
+ color = TangemTheme.colors3.text.status.error,
textAlign = TextAlign.Center,
modifier = Modifier.testTag(TangemPayTestTags.PIN_ERROR_MESSAGE),
)
@@ -125,6 +126,7 @@ private fun PinCodeSection(state: TangemPayChangePinUM, modifier: Modifier = Mod
@Composable
private fun PinCode(
+ isError: Boolean,
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
@@ -180,10 +182,10 @@ private fun PinCode(
),
digit = digit,
backgroundColor = TangemTheme.colors3.bg.opaque.primary,
- borderColor = if (isActive) {
- TangemTheme.colors3.border.status.info
- } else {
- TangemTheme.colors3.border.secondary
+ borderColor = when {
+ isError -> TangemTheme.colors3.border.status.error
+ isActive -> TangemTheme.colors3.border.status.info
+ else -> TangemTheme.colors3.border.secondary
},
textColor = TangemTheme.colors3.text.primary,
textStyle = TangemTheme.typography3.heading.medium,
@@ -201,7 +203,7 @@ private fun PinCode(
private fun TangemPayChangePinScreenV2Preview(
@PreviewParameter(TangemPayChangePinUMPreviewProvider::class) state: TangemPayChangePinUM,
) {
- TangemThemePreview {
+ TangemThemePreviewRedesign {
TangemPayChangePinScreenV2(
state = state,
onBackClick = {},
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt
index f543b7507d..817eaffea4 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt
@@ -144,7 +144,7 @@ internal fun TangemPayEditDisplayNameScreenV2(
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x3, horizontal = TangemTheme.dimens2.x4)
.imePadding(),
- text = resourceReference(R.string.common_save),
+ text = resourceReference(R.string.common_done),
onClick = state.onDoneClick,
isLoading = state.isLoading,
isEnabled = !state.isLoading && state.isDoneEnabled,
diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt
index 29ab477218..56139acdaa 100644
--- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt
+++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/PaymentAccountStatusExt.kt
@@ -36,6 +36,12 @@ internal inline fun AccountStatus.Payment.ifLoadedOrNull(call: (PaymentAccou
}
}
+internal fun AccountStatus.Payment.balanceOrNull(): PaymentAccountStatusValue.Balance? = when (val v = value) {
+ is PaymentAccountStatusValue.Loaded -> v.balance
+ is PaymentAccountStatusValue.Deactivated -> v.balance
+ else -> null
+}
+
internal fun AccountStatus.Payment.findCard(
initialCardId: String,
initialStatus: AccountStatus.Payment,
diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt
index 9808c4f907..e192e54e5f 100644
--- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt
+++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt
@@ -85,7 +85,11 @@ internal class TangemPayCardLimitSetupModelTest {
val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) {
every { source } returns StatusSource.ACTUAL
every { cards } returns listOf(cardWithLimit)
- every { currencyCode } returns "USD"
+ every { balance } returns mockk(relaxed = true) {
+ every { fiatBalance } returns mockk(relaxed = true) {
+ every { currency } returns "USD"
+ }
+ }
}
val paymentStatusWithLimit: AccountStatus.Payment = mockk(relaxed = true) {
every { value } returns statusWithLimit
diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt
index f0585fb4f5..acead48f67 100644
--- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt
+++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModel.kt
@@ -7,7 +7,6 @@ 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.decompose.ui.UiMessageSender
-import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.dialog.Dialogs
@@ -16,7 +15,6 @@ import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
import com.tangem.domain.wallets.usecase.CreateHotWalletUseCase
-import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.features.tangempay.onboarding.api.R
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.MnemonicType
@@ -38,7 +36,6 @@ internal class TangemPayHotWalletOnboardingModel @Inject constructor(
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase,
private val router: Router,
private val uiMessageSender: UiMessageSender,
- private val urlOpener: UrlOpener,
) : Model() {
val uiState: StateFlow
@@ -51,7 +48,7 @@ internal class TangemPayHotWalletOnboardingModel @Inject constructor(
)
private fun onTermsClick() {
- urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK)
+ router.push(AppRoute.Disclaimer(isTosAccepted = true))
}
private fun onGetCardClick() {
diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt
index 6435e64bb2..3b769db3ba 100644
--- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt
+++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingScreen.kt
@@ -14,17 +14,16 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
import com.tangem.core.ui.R
-import com.tangem.core.ui.components.TextButton
-import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.components.SpacerH16
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
@@ -80,10 +79,11 @@ private fun Content(state: TangemPayHotWalletOnboardingUM, modifier: Modifier =
.fillMaxWidth()
.padding(horizontal = 40.dp),
)
+ SpacerH16()
Spacer(Modifier.weight(1f))
Column(
modifier = Modifier.padding(16.dp),
- verticalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp),
) {
NavigationPrimaryButton(
primaryButton = NavigationButton(
@@ -94,18 +94,36 @@ private fun Content(state: TangemPayHotWalletOnboardingUM, modifier: Modifier =
onClick = state.onGetCardClick,
),
)
- TextButton(
- modifier = Modifier.fillMaxWidth(),
- text = stringResourceSafe(R.string.tangem_pay_terms_fees_limits),
- onClick = state.onTermsClick,
- colors = TangemButtonsDefaults.defaultTextButtonColors.copy(
- contentColor = TangemTheme.colors.text.primary1,
- ),
- )
+ TosText(onClick = state.onTermsClick)
}
}
}
+@Composable
+private fun TosText(onClick: () -> Unit, modifier: Modifier = Modifier) {
+ val termsTemplate = stringResourceSafe(R.string.onboarding_create_wallet_term_of_conditions_text)
+ val termsLinkText = stringResourceSafe(R.string.disclaimer_title)
+ val termsLinkColor = TangemTheme.colors.text.accent
+ Text(
+ modifier = modifier.fillMaxWidth(),
+ text = buildAnnotatedString {
+ appendWithStyledPlaceholder(template = termsTemplate) {
+ withLink(
+ LinkAnnotation.Clickable(
+ tag = "tos_link",
+ styles = TextLinkStyles(SpanStyle(textDecoration = TextDecoration.None)),
+ ) { onClick() },
+ ) {
+ appendColored(text = termsLinkText, color = termsLinkColor)
+ }
+ }
+ },
+ style = TangemTheme.typography.caption2,
+ color = TangemTheme.colors.text.tertiary,
+ textAlign = TextAlign.Center,
+ )
+}
+
@Composable
private fun Features(modifier: Modifier = Modifier) {
Column(
diff --git a/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt b/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt
index a2d4591f4d..cb1af5576e 100644
--- a/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt
+++ b/features/tangempay/onboarding/impl/src/test/kotlin/com/tangem/features/tangempay/hotwallet/TangemPayHotWalletOnboardingModelTest.kt
@@ -6,7 +6,6 @@ import com.google.common.truth.Truth.assertThat
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
-import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.message.DialogMessage
import com.tangem.domain.appsflyer.AppsFlyerDeeplinkSource
import com.tangem.domain.appsflyer.usecase.ClearAppsFlyerDeeplinkUseCase
@@ -14,7 +13,6 @@ import com.tangem.domain.hotwallet.IsHotWalletCreationSupported
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.CreateHotWalletUseCase
-import com.tangem.features.tangempay.TangemPayConstants
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.MnemonicType
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
@@ -32,7 +30,6 @@ internal class TangemPayHotWalletOnboardingModelTest {
private val clearAppsFlyerDeeplinkUseCase: ClearAppsFlyerDeeplinkUseCase = mockk()
private val router: Router = mockk(relaxed = true)
private val uiMessageSender: UiMessageSender = mockk(relaxed = true)
- private val urlOpener: UrlOpener = mockk(relaxed = true)
private val testUserWalletId = UserWalletId("1234567890ABCDEF")
private val testUserWallet: UserWallet.Hot = mockk(relaxed = true) {
@@ -43,12 +40,12 @@ internal class TangemPayHotWalletOnboardingModelTest {
inner class OnTermsClick {
@Test
- fun `WHEN onTermsClick THEN urlOpener called with terms link`() = runTest {
+ fun `WHEN onTermsClick THEN navigate to Disclaimer`() = runTest {
val model = createModel()
model.uiState.value.onTermsClick.invoke()
- verify { urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) }
+ verify { router.push(AppRoute.Disclaimer(isTosAccepted = true)) }
}
}
@@ -118,7 +115,6 @@ internal class TangemPayHotWalletOnboardingModelTest {
clearAppsFlyerDeeplinkUseCase = clearAppsFlyerDeeplinkUseCase,
router = router,
uiMessageSender = uiMessageSender,
- urlOpener = urlOpener,
)
}
}
\ No newline at end of file
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt
index ac85882198..5aa1becb9c 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt
@@ -109,6 +109,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.Q
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetYieldSupplyBalanceTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateActionButtonsTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateAddFundsTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTransferTransformer
@@ -470,6 +471,7 @@ internal class TokenDetailsModel @Inject constructor(
yieldSupplyGetRewardsBalanceUseCase(status = status, appCurrency = selectedAppCurrencyFlow.value)
.onEach { formatted ->
uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted)
+ redesignStateController.update(SetYieldSupplyBalanceTransformer(formatted))
}
.flowOn(dispatchers.main)
.launchIn(modelScope)
@@ -479,6 +481,7 @@ internal class TokenDetailsModel @Inject constructor(
uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(
YieldSupplyRewardBalance.empty(),
)
+ redesignStateController.update(SetYieldSupplyBalanceTransformer(YieldSupplyRewardBalance.empty()))
}
}
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt
index 406042f571..8a1b6d4902 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsBalanceBlockUM.kt
@@ -37,6 +37,8 @@ internal sealed class TokenDetailsBalanceBlockUM {
val displayFiatBalanceAvailable: TextReference?,
val isBalanceFlickering: Boolean,
val isBalanceZero: Boolean,
+ val displayYieldSupplyFiatBalance: String? = null,
+ val displayYieldSupplyCryptoBalance: String? = null,
) : TokenDetailsBalanceBlockUM() {
val displayCryptoBalance: TextReference
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt
index e2f6f7f828..99bb251bdb 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformer.kt
@@ -86,6 +86,9 @@ internal class SetBalanceTransformer(
val totalCryptoAmount = computeTotal(status.value.amount, stakingCryptoAmount)
+ val isYieldSupplyActive = status.value.yieldSupplyStatus?.isActive == true
+ val prevContent = prev as? TokenDetailsBalanceBlockUM.Content
+
return TokenDetailsBalanceBlockUM.Content(
addFundsButton = prev.addFundsButton,
swapButton = prev.swapButton,
@@ -108,6 +111,17 @@ internal class SetBalanceTransformer(
},
isBalanceFlickering = status.value.sources.total == StatusSource.CACHE,
isBalanceZero = totalCryptoAmount.isNullOrZero(),
+ // Preserve the ticking yield balance produced by SetYieldSupplyBalanceTransformer across status updates
+ displayYieldSupplyFiatBalance = if (isYieldSupplyActive) {
+ prevContent?.displayYieldSupplyFiatBalance
+ } else {
+ null
+ },
+ displayYieldSupplyCryptoBalance = if (isYieldSupplyActive) {
+ prevContent?.displayYieldSupplyCryptoBalance
+ } else {
+ null
+ },
)
}
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt
new file mode 100644
index 0000000000..e924a6a7b6
--- /dev/null
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformer.kt
@@ -0,0 +1,27 @@
+package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
+
+import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
+import com.tangem.utils.transformer.Transformer
+
+/**
+ * Pushes the ticking yield supply balance (emitted every tick by [YieldSupplyGetRewardsBalanceUseCase])
+ * into the redesign [TokenDetailsBalanceBlockUM.Content], so the balance increments in real time.
+ */
+internal class SetYieldSupplyBalanceTransformer(
+ private val yieldSupplyRewardBalance: YieldSupplyRewardBalance,
+) : Transformer {
+
+ override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
+ val balanceBlockUM = prevState.balanceBlockUM
+ if (balanceBlockUM !is TokenDetailsBalanceBlockUM.Content) return prevState
+
+ return prevState.copy(
+ balanceBlockUM = balanceBlockUM.copy(
+ displayYieldSupplyFiatBalance = yieldSupplyRewardBalance.fiatBalance,
+ displayYieldSupplyCryptoBalance = yieldSupplyRewardBalance.cryptoBalance,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt
index 6592a5416a..56957faa3b 100644
--- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt
+++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt
@@ -11,7 +11,9 @@ 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.Color
import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
@@ -23,10 +25,12 @@ import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.text.TextAnimatedCounter
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.button.action.ActionButtons
import com.tangem.core.ui.ds.image.TangemIconUM
+import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
@@ -42,6 +46,9 @@ import kotlinx.collections.immutable.persistentListOf
private val CurrencyIconSize: Dp = 70.dp
private val NetworkBadgeSize: Dp = 24.dp
+/** OpenType "tabular figures" feature — makes every digit the same width to prevent horizontal jitter. */
+private const val TABULAR_FIGURES_FEATURE = "tnum"
+
@Composable
internal fun TokenDetailsBalanceBlock(
balanceBlockUM: TokenDetailsBalanceBlockUM,
@@ -124,20 +131,60 @@ private fun ContentBody(state: TokenDetailsBalanceBlockUM.Content, isBalanceHidd
}
}
SpacerH(TangemTheme.dimens2.x2)
- Text(
+ AnimatedBalance(
modifier = Modifier.testTag(TokenDetailsScreenTestTags.BALANCE_FIAT),
- text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
+ yieldBalance = state.displayYieldSupplyFiatBalance,
+ fallbackBalance = state.displayFiatBalance,
style = TangemTheme.typography2.titleRegular44,
color = TangemTheme.colors2.text.neutral.primary,
+ isBalanceHidden = isBalanceHidden,
)
SpacerH(TangemTheme.dimens2.x2_5)
- Text(
- text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
+ AnimatedBalance(
+ yieldBalance = state.displayYieldSupplyCryptoBalance,
+ fallbackBalance = state.displayCryptoBalance,
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.secondary,
+ isBalanceHidden = isBalanceHidden,
)
}
+/**
+ * Renders a balance that animates digit-by-digit ([TextAnimatedCounter]) while a ticking yield supply
+ * value is present, and falls back to a plain [Text] otherwise (or when the balance is hidden).
+ */
+@Composable
+private fun AnimatedBalance(
+ yieldBalance: String?,
+ fallbackBalance: TextReference,
+ style: TextStyle,
+ color: Color,
+ isBalanceHidden: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = TangemTheme.dimens2.x6),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (yieldBalance != null && !isBalanceHidden) {
+ TextAnimatedCounter(
+ text = yieldBalance,
+ // Tabular figures keep every digit the same width, so the centered balance doesn't
+ // jitter horizontally as digits roll during the increment animation.
+ style = style.copy(color = color, fontFeatureSettings = TABULAR_FIGURES_FEATURE),
+ )
+ } else {
+ Text(
+ text = fallbackBalance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
+ style = style,
+ color = color,
+ )
+ }
+ }
+}
+
@Composable
private fun LoadingBody() {
Text(
diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt
index bba4ce1980..7cd9696901 100644
--- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt
+++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetBalanceTransformerTest.kt
@@ -14,6 +14,7 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.staking.StakingBalance
+import com.tangem.domain.models.yield.supply.YieldSupplyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
@@ -434,6 +435,59 @@ class SetBalanceTransformerTest {
// endregion
+ // region Yield supply balance
+
+ @Test
+ fun `GIVEN yield active AND prev has yield balances WHEN transform THEN yield balances preserved`() {
+ // GIVEN
+ val status = createStatus(loadedValue(yieldSupplyStatus = activeYieldSupplyStatus()))
+ val transformer = createTransformer(status)
+ val prev = contentWithYieldBalances(fiat = "$21,000.12", crypto = "10.500001 ETH")
+ val state = initialState().copy(balanceBlockUM = prev)
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content
+ assertThat(content.displayYieldSupplyFiatBalance).isEqualTo("$21,000.12")
+ assertThat(content.displayYieldSupplyCryptoBalance).isEqualTo("10.500001 ETH")
+ }
+
+ @Test
+ fun `GIVEN yield inactive AND prev has yield balances WHEN transform THEN yield balances are cleared`() {
+ // GIVEN
+ val status = createStatus(loadedValue(yieldSupplyStatus = null))
+ val transformer = createTransformer(status)
+ val prev = contentWithYieldBalances(fiat = "$21,000.12", crypto = "10.500001 ETH")
+ val state = initialState().copy(balanceBlockUM = prev)
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content
+ assertThat(content.displayYieldSupplyFiatBalance).isNull()
+ assertThat(content.displayYieldSupplyCryptoBalance).isNull()
+ }
+
+ @Test
+ fun `GIVEN yield active AND prev is not Content WHEN transform THEN yield balances are null`() {
+ // GIVEN — prev is the default Loading block, so there are no yield balances to preserve yet
+ val status = createStatus(loadedValue(yieldSupplyStatus = activeYieldSupplyStatus()))
+ val transformer = createTransformer(status)
+
+ // WHEN
+ val result = transformer.transform(initialState())
+
+ // THEN
+ val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content
+ assertThat(content.displayYieldSupplyFiatBalance).isNull()
+ assertThat(content.displayYieldSupplyCryptoBalance).isNull()
+ }
+
+ // endregion
+
// region Unrelated fields preserved
@Test
@@ -473,6 +527,7 @@ class SetBalanceTransformerTest {
fiatAmount: BigDecimal = BigDecimal("21000"),
fiatRate: BigDecimal = BigDecimal("2000"),
stakingBalance: StakingBalance? = null,
+ yieldSupplyStatus: YieldSupplyStatus? = null,
sources: CryptoCurrencyStatus.Sources = CryptoCurrencyStatus.Sources(),
): CryptoCurrencyStatus.Loaded = CryptoCurrencyStatus.Loaded(
amount = amount,
@@ -480,13 +535,39 @@ class SetBalanceTransformerTest {
fiatRate = fiatRate,
priceChange = BigDecimal("2.5"),
stakingBalance = stakingBalance,
- yieldSupplyStatus = null,
+ yieldSupplyStatus = yieldSupplyStatus,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = mockk(relaxed = true),
sources = sources,
)
+ private fun activeYieldSupplyStatus(): YieldSupplyStatus = YieldSupplyStatus(
+ isActive = true,
+ isInitialized = true,
+ isAllowedToSpend = true,
+ effectiveProtocolBalance = null,
+ )
+
+ private fun contentWithYieldBalances(
+ fiat: String?,
+ crypto: String?,
+ ): TokenDetailsBalanceBlockUM.Content = TokenDetailsBalanceBlockUM.Content(
+ addFundsButton = placeholderButton(),
+ swapButton = placeholderButton(),
+ transferButton = placeholderButton(),
+ tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
+ currencyIconState = CurrencyIconState.Loading,
+ displayCryptoBalanceAll = stringReference(""),
+ displayFiatBalanceAll = stringReference(""),
+ displayCryptoBalanceAvailable = null,
+ displayFiatBalanceAvailable = null,
+ isBalanceFlickering = false,
+ isBalanceZero = false,
+ displayYieldSupplyFiatBalance = fiat,
+ displayYieldSupplyCryptoBalance = crypto,
+ )
+
private fun noQuoteValue(): CryptoCurrencyStatus.NoQuote = CryptoCurrencyStatus.NoQuote(
amount = BigDecimal("5.0"),
stakingBalance = null,
diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt
new file mode 100644
index 0000000000..c8e7c4a92a
--- /dev/null
+++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetYieldSupplyBalanceTransformerTest.kt
@@ -0,0 +1,146 @@
+package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
+
+import com.google.common.truth.Truth.assertThat
+import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
+import com.tangem.core.ui.components.currency.icon.CurrencyIconState
+import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
+import com.tangem.core.ui.ds.button.TangemButtonType
+import com.tangem.core.ui.ds.button.TangemButtonUM
+import com.tangem.core.ui.extensions.stringReference
+import com.tangem.domain.yield.supply.models.YieldSupplyRewardBalance
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.AddFundsUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.TransferUM
+import com.tangem.feature.tokendetails.presentation.tokendetails.state.ZeroBalanceActionsUM
+import io.mockk.mockk
+import kotlinx.collections.immutable.persistentListOf
+import org.junit.jupiter.api.Test
+
+class SetYieldSupplyBalanceTransformerTest {
+
+ @Test
+ fun `GIVEN Content state WHEN transform THEN yield balances are set from reward balance`() {
+ // GIVEN
+ val state = stateWith(contentBlock())
+ val transformer = SetYieldSupplyBalanceTransformer(
+ YieldSupplyRewardBalance(fiatBalance = "$21,000.12", cryptoBalance = "10.500001 ETH"),
+ )
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content
+ assertThat(content.displayYieldSupplyFiatBalance).isEqualTo("$21,000.12")
+ assertThat(content.displayYieldSupplyCryptoBalance).isEqualTo("10.500001 ETH")
+ }
+
+ @Test
+ fun `GIVEN empty reward balance WHEN transform on Content THEN yield balances are nulled`() {
+ // GIVEN
+ val state = stateWith(
+ contentBlock().copy(
+ displayYieldSupplyFiatBalance = "$21,000.12",
+ displayYieldSupplyCryptoBalance = "10.500001 ETH",
+ ),
+ )
+ val transformer = SetYieldSupplyBalanceTransformer(YieldSupplyRewardBalance.empty())
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ val content = result.balanceBlockUM as TokenDetailsBalanceBlockUM.Content
+ assertThat(content.displayYieldSupplyFiatBalance).isNull()
+ assertThat(content.displayYieldSupplyCryptoBalance).isNull()
+ }
+
+ @Test
+ fun `GIVEN Loading block WHEN transform THEN state is unchanged`() {
+ // GIVEN
+ val state = stateWith(loadingBlock())
+ val transformer = SetYieldSupplyBalanceTransformer(
+ YieldSupplyRewardBalance(fiatBalance = "$1", cryptoBalance = "1 ETH"),
+ )
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ assertThat(result).isSameInstanceAs(state)
+ }
+
+ @Test
+ fun `GIVEN Error block WHEN transform THEN state is unchanged`() {
+ // GIVEN
+ val state = stateWith(errorBlock())
+ val transformer = SetYieldSupplyBalanceTransformer(
+ YieldSupplyRewardBalance(fiatBalance = "$1", cryptoBalance = "1 ETH"),
+ )
+
+ // WHEN
+ val result = transformer.transform(state)
+
+ // THEN
+ assertThat(result).isSameInstanceAs(state)
+ }
+
+ private fun contentBlock(): TokenDetailsBalanceBlockUM.Content = TokenDetailsBalanceBlockUM.Content(
+ addFundsButton = placeholderButton(),
+ swapButton = placeholderButton(),
+ transferButton = placeholderButton(),
+ tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
+ currencyIconState = CurrencyIconState.Loading,
+ displayCryptoBalanceAll = stringReference("10.5 ETH"),
+ displayFiatBalanceAll = stringReference("$21,000.00"),
+ displayCryptoBalanceAvailable = null,
+ displayFiatBalanceAvailable = null,
+ isBalanceFlickering = false,
+ isBalanceZero = false,
+ )
+
+ private fun loadingBlock(): TokenDetailsBalanceBlockUM.Loading = TokenDetailsBalanceBlockUM.Loading(
+ addFundsButton = placeholderButton(),
+ swapButton = placeholderButton(),
+ transferButton = placeholderButton(),
+ tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
+ currencyIconState = CurrencyIconState.Loading,
+ )
+
+ private fun errorBlock(): TokenDetailsBalanceBlockUM.Error = TokenDetailsBalanceBlockUM.Error(
+ addFundsButton = placeholderButton(),
+ swapButton = placeholderButton(),
+ transferButton = placeholderButton(),
+ tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
+ currencyIconState = CurrencyIconState.Loading,
+ )
+
+ private fun stateWith(balanceBlockUM: TokenDetailsBalanceBlockUM): TokenDetailsUM = TokenDetailsUM(
+ topAppBarUM = TokenDetailsTopAppBarUM(
+ titleState = TitleState.Simple(tokenName = ""),
+ subtitle = stringReference(""),
+ onBackClick = {},
+ menuItems = persistentListOf(),
+ ),
+ balanceBlockUM = balanceBlockUM,
+ notifications = persistentListOf(),
+ marketPriceBlockState = mockk(relaxed = true),
+ earnBlockState = null,
+ pullToRefreshConfig = mockk(relaxed = true),
+ isBalanceHidden = false,
+ isMarketPriceAvailable = false,
+ addFundsUM = AddFundsUM.Loading,
+ transferUM = TransferUM.Loading,
+ zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
+ )
+
+ private fun placeholderButton(): TangemButtonUM = TangemButtonUM(
+ text = stringReference(""),
+ type = TangemButtonType.Secondary,
+ onClick = {},
+ )
+}
\ No newline at end of file
diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts
index 073dc92b0f..58b07a4853 100644
--- a/features/wallet-settings/impl/build.gradle.kts
+++ b/features/wallet-settings/impl/build.gradle.kts
@@ -19,6 +19,7 @@ dependencies {
implementation(projects.features.nft.api)
implementation(projects.features.onboardingV2.api)
implementation(projects.features.pushNotifications.api)
+ implementation(projects.features.pushNotificationSettings.api)
implementation(projects.features.hotWallet.api)
implementation(projects.features.wallet.api)
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
index 4fa7a53f60..23e9499009 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/preview/PreviewWalletSettingsComponent.kt
@@ -63,6 +63,8 @@ internal class PreviewWalletSettingsComponent : WalletSettingsComponent {
isNotificationsEnabled = true,
onCheckedNotificationsChanged = {},
onNotificationsDescriptionClick = {},
+ isPushNotificationSettingsEnabled = false,
+ onNotificationSettingsClick = {},
isNotificationsPermissionGranted = false,
onAccessCodeClick = {},
onBackupClick = {},
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
index 8f236ca63a..6741173d06 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt
@@ -51,6 +51,7 @@ import com.tangem.feature.walletsettings.utils.ItemsBuilder
import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
+import com.tangem.features.pushnotificationsettings.PushNotificationSettingsFeatureToggles
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.PersistentList
@@ -92,6 +93,7 @@ internal class WalletSettingsModel @Inject constructor(
private val accountListSortingSaver: AccountListSortingSaver,
private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
+ private val pushNotificationSettingsFeatureToggles: PushNotificationSettingsFeatureToggles,
) : Model() {
val params: WalletSettingsComponent.Params = paramsContainer.require()
@@ -203,6 +205,8 @@ internal class WalletSettingsModel @Inject constructor(
is UserWallet.Cold -> userWallet.isMultiCurrency
is UserWallet.Hot -> true
}
+ val isPushNotificationSettingsEnabled =
+ pushNotificationSettingsFeatureToggles.isPushNotificationSettingsEnabled
return itemsBuilder.buildItems(
userWallet = userWallet,
cardItem = cardItem,
@@ -245,6 +249,8 @@ internal class WalletSettingsModel @Inject constructor(
isNotificationsPermissionGranted = isNotificationsPermissionGranted,
onCheckedNotificationsChanged = ::onCheckedNotificationsChange,
onNotificationsDescriptionClick = ::onNotificationsDescriptionClick,
+ isPushNotificationSettingsEnabled = isPushNotificationSettingsEnabled,
+ onNotificationSettingsClick = ::onNotificationSettingsClick,
onAccessCodeClick = { onAccessCodeClick(userWallet) },
onBackupClick = ::onBackupClick,
onCardSettingsClick = ::onCardSettingsClick,
@@ -252,6 +258,10 @@ internal class WalletSettingsModel @Inject constructor(
)
}
+ private fun onNotificationSettingsClick() {
+ router.push(AppRoute.PushNotificationSettings(userWalletId = params.userWalletId))
+ }
+
private fun forgetWallet() = modelScope.launch {
val userWallet = getUserWalletUseCase(params.userWalletId)
.getOrNull()
diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
index 8bde456990..21dfa146a5 100644
--- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
+++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt
@@ -33,6 +33,8 @@ internal class ItemsBuilder @Inject constructor() {
isNotificationsPermissionGranted: Boolean,
onCheckedNotificationsChanged: (Boolean) -> Unit,
onNotificationsDescriptionClick: () -> Unit,
+ isPushNotificationSettingsEnabled: Boolean,
+ onNotificationSettingsClick: () -> Unit,
forgetWallet: () -> Unit,
onLinkMoreCardsClick: () -> Unit,
onReferralClick: () -> Unit,
@@ -50,20 +52,26 @@ internal class ItemsBuilder @Inject constructor() {
isLinkMoreCardsAvailable = isLinkMoreCardsAvailable,
isReferralAvailable = isReferralAvailable,
isManageTokensAvailable = isManageTokensAvailable,
+ isPushNotificationSettingsEnabled = isPushNotificationSettingsEnabled,
onLinkMoreCardsClick = onLinkMoreCardsClick,
onReferralClick = onReferralClick,
onManageTokensClick = onManageTokensClick,
onBackupClick = onBackupClick,
onCardSettingsClick = onCardSettingsClick,
+ onNotificationSettingsClick = onNotificationSettingsClick,
),
)
.addAll(
- buildNotificationItems(
- isNotificationsPermissionGranted = isNotificationsPermissionGranted,
- isNotificationsEnabled = isNotificationsEnabled,
- onCheckedNotificationsChanged = onCheckedNotificationsChanged,
- onNotificationsDescriptionClick = onNotificationsDescriptionClick,
- ),
+ if (isPushNotificationSettingsEnabled) {
+ emptyList()
+ } else {
+ buildNotificationItems(
+ isNotificationsPermissionGranted = isNotificationsPermissionGranted,
+ isNotificationsEnabled = isNotificationsEnabled,
+ onCheckedNotificationsChanged = onCheckedNotificationsChanged,
+ onNotificationsDescriptionClick = onNotificationsDescriptionClick,
+ )
+ },
)
.addAll(
buildNFTItems(
@@ -137,11 +145,13 @@ internal class ItemsBuilder @Inject constructor() {
isLinkMoreCardsAvailable: Boolean,
isReferralAvailable: Boolean,
isManageTokensAvailable: Boolean,
+ isPushNotificationSettingsEnabled: Boolean,
onLinkMoreCardsClick: () -> Unit,
onReferralClick: () -> Unit,
onManageTokensClick: () -> Unit,
onBackupClick: () -> Unit,
onCardSettingsClick: () -> Unit,
+ onNotificationSettingsClick: () -> Unit,
) = WalletSettingsItemUM.WithItems(
id = "card",
description = null,
@@ -207,6 +217,16 @@ internal class ItemsBuilder @Inject constructor() {
add(referralBlock)
}
+
+ if (isPushNotificationSettingsEnabled) {
+ val notificationSettingsBlock = BlockUM(
+ text = resourceReference(R.string.push_notification_settings_title),
+ iconRes = R.drawable.ic_push_notification_settings_24,
+ onClick = onNotificationSettingsClick,
+ )
+
+ add(notificationSettingsBlock)
+ }
}.toImmutableList(),
)
diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt
index c771ddd2c9..773354e3fe 100644
--- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt
+++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt
@@ -58,8 +58,8 @@ internal class TangemPayMainBlockConverter(
subtitle = TextReference.Res(R.string.tangempay_status_deactivated),
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
- currencyCode = statusValue.fiatBalance.currency,
- balance = statusValue.fiatBalance.availableBalance,
+ currencyCode = statusValue.balance.fiatBalance.currency,
+ balance = statusValue.balance.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,
@@ -75,8 +75,8 @@ internal class TangemPayMainBlockConverter(
},
isBalanceFlickering = statusValue.source == StatusSource.CACHE,
balance = getBalanceText(
- currencyCode = statusValue.currencyCode,
- balance = statusValue.fiatBalance.availableBalance,
+ currencyCode = statusValue.balance.fiatBalance.currency,
+ balance = statusValue.balance.fiatBalance.availableBalance,
),
balanceSubtitle = stringReference(statusValue.cryptoCurrency.symbol),
shouldShowOnlyCacheWarning = statusValue.source == StatusSource.ONLY_CACHE,