diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a41fbd9545..4e84fb633b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + @@ -25,6 +25,13 @@ + + + + + + diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index 18f3cff503..33d7f81b88 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -30,7 +30,7 @@ internal class LockUserWalletsTimer( lifecycle.addObserver(this) } - override fun onResume(owner: LifecycleOwner) { + override fun onStart(owner: LifecycleOwner) { owner.lifecycleScope.launch { val wasApplicationStopped = settingsRepository.wasApplicationStopped() val shouldOpenWelcomeScreenOnResume = settingsRepository.shouldOpenWelcomeScreenOnResume() @@ -45,8 +45,6 @@ internal class LockUserWalletsTimer( settingsRepository.setWasApplicationStopped(value = false) - start() - if (shouldOpenWelcomeScreenOnResume) { store.dispatchNavigationAction { replaceAll(AppRoute.Welcome()) } settingsRepository.setShouldOpenWelcomeScreenOnResume(value = false) @@ -54,6 +52,10 @@ internal class LockUserWalletsTimer( } } + override fun onResume(owner: LifecycleOwner) { + start() + } + override fun onStop(owner: LifecycleOwner) { Timber.i("Owner stopped") diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 6964261453..94cc27578a 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -253,9 +253,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - window.setHideOverlayWindows(true) - } + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + // window.setHideOverlayWindows(true) + // } splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } @@ -279,6 +279,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac if (intent != null) { deepLinksRegistry.launch(intent) } + + lifecycle.addObserver(WindowObscurationObserver) } private fun setRootContent() { @@ -550,20 +552,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } override fun dispatchTouchEvent(event: MotionEvent): Boolean { - val isPartiallyObscured = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0 - } else { - false - } + val result = WindowObscurationObserver.dispatchTouchEvent(event, analyticsEventsHandler) - val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 - - if (isPartiallyObscured || isFullyObscured) { - Timber.e("Window is partially or fully obscured") - return false - } - - return super.dispatchTouchEvent(event) + return if (result) super.dispatchTouchEvent(event) else false } private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) { diff --git a/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt new file mode 100644 index 0000000000..57f5a3fdec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/WindowObscurationObserver.kt @@ -0,0 +1,68 @@ +package com.tangem.tap + +import android.os.Build +import android.view.MotionEvent +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.TechAnalyticsEvent +import com.tangem.core.analytics.models.event.TechAnalyticsEvent.WindowObscured.ObscuredState +import timber.log.Timber + +internal object WindowObscurationObserver : DefaultLifecycleObserver { + + private var isWindowPartiallyObscuredAlreadySent: Boolean = false + private var isWindowFullyObscuredAlreadySent: Boolean = false + + private var isReadyToProxy = false + + override fun onResume(owner: LifecycleOwner) { + super.onResume(owner) + isReadyToProxy = true + } + + override fun onPause(owner: LifecycleOwner) { + super.onPause(owner) + isReadyToProxy = false + } + + fun dispatchTouchEvent(event: MotionEvent, analyticsEventHandler: AnalyticsEventHandler): Boolean { + if (!isReadyToProxy) return true + + val isPartiallyObscured = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0 + } else { + false + } + + if (isPartiallyObscured) { + Timber.d("Window is partially obscured") + + if (!isWindowPartiallyObscuredAlreadySent) { + analyticsEventHandler.send( + event = TechAnalyticsEvent.WindowObscured(state = ObscuredState.PARTIALLY), + ) + + isWindowPartiallyObscuredAlreadySent = true + } + } + + val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 + + if (isFullyObscured) { + Timber.d("Window is partially or fully obscured") + + if (!isWindowFullyObscuredAlreadySent) { + analyticsEventHandler.send( + event = TechAnalyticsEvent.WindowObscured(state = ObscuredState.FULLY), + ) + + isWindowFullyObscuredAlreadySent = true + } + + return false + } + + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt index 7fe3ef0081..965d6caacd 100644 --- a/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt +++ b/app/src/main/java/com/tangem/tap/common/url/CustomTabsUrlOpener.kt @@ -1,8 +1,11 @@ package com.tangem.tap.common.url import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_DARK import androidx.browser.customtabs.CustomTabsIntent.COLOR_SCHEME_LIGHT @@ -12,6 +15,7 @@ import com.tangem.tap.common.extensions.getColorCompat import com.tangem.tap.foregroundActivityObserver import com.tangem.tap.withForegroundActivity import com.tangem.wallet.R +import timber.log.Timber internal class CustomTabsUrlOpener : UrlOpener { @@ -34,6 +38,37 @@ internal class CustomTabsUrlOpener : UrlOpener { ) .build() - customTabsIntent.launchUrl(context, Uri.parse(url)) + customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + runCatching { + if (checkCustomTabsAvailability(context, browserIntent)) { + context.startActivity(browserIntent) + } else { + customTabsIntent.launchUrl(context, Uri.parse(url)) + } + }.onFailure { + Timber.e(it.message) + } + } + + /** + * Custom Tabs compatibility check. Returns flag whether custom tabs are supported + * @see "https://developer.chrome.com/docs/android/custom-tabs/howto-custom-tab-check" + */ + private fun checkCustomTabsAvailability(context: Context, browserIntent: Intent): Boolean { + // Get all apps that can handle VIEW intents and Custom Tab service connections. + val resolveInfos = context.packageManager.queryIntentActivities(browserIntent, PackageManager.MATCH_ALL) + + // Extract package names from ResolveInfo objects + val packageNames = mutableListOf() + for (info in resolveInfos) { + packageNames.add(info.activityInfo.packageName) + } + + // Get a package that supports Custom Tabs + val packageName = CustomTabsClient.getPackageName(context, packageNames, true) + + return packageName == null // Custom Tabs are not supported by any browser on the device } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index bde2638529..1bc9c4dd87 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -208,6 +208,7 @@ internal class MainViewModel @Inject constructor( override fun onDismissBottomSheet() { listenToFlipsUseCase.changeUpdateEnabled(true) + router.pop() stateHolder.updateWithoutModalNotification() stateHolder.updateWithHiddenBalancesToast(true) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index c109672c4b..392f1aacd9 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -46,8 +46,6 @@ object TradeCryptoMiddleware { is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) is TradeCryptoAction.Buy -> proceedBuyAction(action) is TradeCryptoAction.Sell -> proceedSellAction(action) - is TradeCryptoAction.SendToken -> handleNewSendToken(action = action) - is TradeCryptoAction.SendCoin -> handleNewSendCoin(action = action) } } @@ -158,37 +156,4 @@ object TradeCryptoMiddleware { transactionId = transactionId, )?.let { store.dispatchOpenUrl(it) } } - - private fun handleNewSendToken(action: TradeCryptoAction.SendToken) { - handleNewSend( - userWalletId = action.userWallet.walletId, - txInfo = action.transactionInfo, - currency = action.tokenCurrency, - ) - } - - private fun handleNewSendCoin(action: TradeCryptoAction.SendCoin) { - handleNewSend( - userWalletId = action.userWallet.walletId, - txInfo = action.transactionInfo, - currency = action.coinStatus.currency, - ) - } - - private fun handleNewSend( - userWalletId: UserWalletId, - txInfo: TradeCryptoAction.TransactionInfo?, - currency: CryptoCurrency, - ) { - val route = AppRoute.Send( - currency = currency, - userWalletId = userWalletId, - transactionId = txInfo?.transactionId, - destinationAddress = txInfo?.destinationAddress, - amount = txInfo?.amount, - tag = txInfo?.tag, - ) - - store.dispatchNavigationAction { push(route) } - } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml index c9fb79355a..98ad59717a 100644 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ b/app/src/main/res/layout/fragment_onboarding_wallet.xml @@ -178,9 +178,9 @@ android:id="@+id/tv_body" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/home_bottom_tv_body_margin_start" + android:layout_marginStart="32dp" android:layout_marginTop="@dimen/home_bottom_tv_body_margin_top" - android:layout_marginEnd="@dimen/home_bottom_tv_body_margin_end" + android:layout_marginEnd="32dp" android:textAlignment="center" android:textColor="@color/text_secondary" android:textSize="16sp" diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 315574334e..28e0387959 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) implementation(deps.compose.coil) + implementation(deps.compose.constraintLayout) /** Deps */ implementation(deps.kotlin.immutable.collections) @@ -34,6 +35,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) + implementation(projects.domain.onramp.models) implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt similarity index 93% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt index cea568e0d5..2703481292 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressEstimate.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressEstimate.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express +package com.tangem.common.ui.expressStatus import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -7,17 +7,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import com.tangem.common.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.inputrow.InputRowApprox import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.tokendetails.impl.R @Suppress("LongParameterList") @Composable -internal fun ExpressEstimate( +fun ExpressEstimate( timestamp: TextReference, fromTokenIconState: CurrencyIconState, toTokenIconState: CurrencyIconState, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt similarity index 97% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt index ad15e423ed..1f8f4f8eb3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressProvider.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressProvider.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express +package com.tangem.common.ui.expressStatus import android.content.res.Configuration import android.widget.Toast @@ -29,7 +29,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @Composable -internal fun ExpressProvider( +fun ExpressProvider( providerName: TextReference, providerType: TextReference, providerTxId: String?, diff --git a/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt new file mode 100644 index 0000000000..98c0bfce00 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusBottomSheet.kt @@ -0,0 +1,24 @@ +package com.tangem.common.ui.expressStatus + +import androidx.compose.runtime.Composable +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.res.TangemTheme + +data class ExpressStatusBottomSheetConfig( + val value: ExpressTransactionStateUM, +) : TangemBottomSheetConfigContent + +@Composable +fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.tertiary, + ) { content: ExpressStatusBottomSheetConfig -> + when (val state = content.value) { + is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItem.kt similarity index 98% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItem.kt index 698c5aa938..25b2c3389d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItem.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express +package com.tangem.common.ui.expressStatus import android.content.res.Configuration import androidx.annotation.DrawableRes @@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.constraintlayout.compose.* +import com.tangem.common.ui.R import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.CurrencyIcon @@ -27,7 +28,6 @@ import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.tokendetails.impl.R @Suppress("DestructuringDeclarationWithTooManyEntries", "LongMethod", "LongParameterList") @Composable diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItems.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItems.kt similarity index 78% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItems.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItems.kt index ffd74a9e24..1424805a32 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusItems.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/ExpressStatusItems.kt @@ -1,14 +1,14 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express +package com.tangem.common.ui.expressStatus import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.ui.Modifier +import com.tangem.common.ui.R +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM -import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.PersistentList -internal fun LazyListScope.expressTransactionsItems( +fun LazyListScope.expressTransactionsItems( expressTxs: PersistentList, modifier: Modifier = Modifier, ) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheetContent.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt similarity index 79% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheetContent.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt index 94d3245146..35905de8f7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/onramp/OnrampStatusBottomSheetContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/OnrampStatusBottomSheetContent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp +package com.tangem.common.ui.expressStatus import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -7,8 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign -import com.tangem.common.ui.expressStatus.ExpressStatusBlock -import com.tangem.common.ui.expressStatus.ExpressStatusNotificationBlock +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -17,12 +16,9 @@ import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider @Composable -internal fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) { +fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) { Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt similarity index 53% rename from features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt rename to common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt index bb94a805d3..45138854f8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExpressTransactionStateUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/expressStatus/state/ExpressTransactionStateUM.kt @@ -1,31 +1,13 @@ -package com.tangem.feature.tokendetails.presentation.tokendetails.state.express +package com.tangem.common.ui.expressStatus.state -import com.tangem.common.ui.expressStatus.state.ExpressStatusUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampStatus -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.feature.swap.domain.models.domain.ExchangeStatus -import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications -import kotlinx.collections.immutable.ImmutableList -internal sealed class ExpressTransactionStateUM { +interface ExpressTransactionStateUM { - abstract val info: ExpressTransactionStateInfoUM - - data class ExchangeUM( - override val info: ExpressTransactionStateInfoUM, - val provider: SwapProvider, - val activeStatus: ExchangeStatus?, - val statuses: ImmutableList, - val notification: ExchangeStatusNotifications? = null, - val showProviderLink: Boolean, - val isRefundTerminalStatus: Boolean, - val fromCryptoCurrency: CryptoCurrency, - val toCryptoCurrency: CryptoCurrency, - ) : ExpressTransactionStateUM() + val info: ExpressTransactionStateInfoUM data class OnrampUM( override val info: ExpressTransactionStateInfoUM, @@ -34,10 +16,10 @@ internal sealed class ExpressTransactionStateUM { val providerType: String, // todo onramp fix after SwapProvider moved to own module val activeStatus: OnrampStatus.Status, val fromCurrencyCode: String, - ) : ExpressTransactionStateUM() + ) : ExpressTransactionStateUM } -internal data class ExpressTransactionStateInfoUM( +data class ExpressTransactionStateInfoUM( val title: TextReference, val status: ExpressStatusUM, val notification: NotificationUM?, @@ -49,7 +31,6 @@ internal data class ExpressTransactionStateInfoUM( val onGoToProviderClick: (String) -> Unit, val onClick: () -> Unit, val iconState: ExpressTransactionStateIconUM, - val toAmount: TextReference, val toFiatAmount: TextReference?, val toAmountSymbol: String, @@ -61,7 +42,7 @@ internal data class ExpressTransactionStateInfoUM( val fromCurrencyIcon: CurrencyIconState, ) -internal enum class ExpressTransactionStateIconUM { +enum class ExpressTransactionStateIconUM { Warning, Error, None, diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt new file mode 100644 index 0000000000..7d7460a633 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TechAnalyticsEvent.kt @@ -0,0 +1,26 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent + +/** + * Tech analytics event + * + * @param event event name + * @param params params + */ +sealed class TechAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = "Tech", event = event, params = params) { + + class WindowObscured(state: ObscuredState) : TechAnalyticsEvent( + event = "Window Obscured", + params = mapOf("State" to state.name), + ) { + + enum class ObscuredState { + PARTIALLY, + FULLY, + } + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json index bb3b50f661..b8b96b7123 100644 --- a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json +++ b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json @@ -8,7 +8,7 @@ "version": "undefined" }, { - "name": "clore-ai", + "name": "NEXA/test", "version": "undefined" }, { diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index c7dc28c8b7..60580e40b7 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -13,7 +13,7 @@ }, { "name": "ONRAMP_ENABLED", - "version": "5.19.0" + "version": "undefined" }, { "name": "MAIN_ACTION_BUTTONS_ENABLED", diff --git a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt index 8c208bae34..0a260230e9 100644 --- a/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt +++ b/core/config-toggles/src/main/kotlin/com/tangem/core/configtoggle/blockchain/impl/DefaultExcludedBlockchainsManager.kt @@ -6,8 +6,8 @@ import com.tangem.core.configtoggle.utils.associateToggles import com.tangem.core.configtoggle.version.VersionProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getSyncOrDefault -import com.tangem.datasource.local.preferences.utils.store +import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.datasource.local.preferences.utils.storeObjectMap internal class DefaultExcludedBlockchainsManager( private val localTogglesStorage: TogglesStorage, @@ -17,62 +17,64 @@ internal class DefaultExcludedBlockchainsManager( private var isInitialized: Boolean = false - private lateinit var currentExcludedBlockchainsIds: Set - private lateinit var localExcludedBlockchainsIds: Set + private lateinit var currentExcludedBlockchains: MutableMap + private lateinit var localExcludedBlockchains: Map override val excludedBlockchainsIds: Set get() { if (!isInitialized) error("ExcludedBlockchainsManager is not initialized") - return currentExcludedBlockchainsIds + return currentExcludedBlockchains + .filterValues { it } + .keys } override suspend fun init() { localTogglesStorage.populate(path = "configs/excluded_blockchains_config") - val storedExcludedBlockchainsIds = appPreferencesStore.getSyncOrDefault( + val storedExcludedBlockchainsIds = appPreferencesStore.getObjectMapSync( key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, - default = emptySet(), ) - localExcludedBlockchainsIds = localTogglesStorage.toggles + localExcludedBlockchains = localTogglesStorage.toggles .associateToggles(currentVersion = versionProvider.get().orEmpty()) - .filterValues { isIncluded -> !isIncluded } - .keys + .mapValues { (_, isIncluded) -> !isIncluded } - if (storedExcludedBlockchainsIds.isEmpty()) { - recoverLocalConfig() - } else { - currentExcludedBlockchainsIds = storedExcludedBlockchainsIds - } + currentExcludedBlockchains = (localExcludedBlockchains.keys + storedExcludedBlockchainsIds.keys) + .fold(mutableMapOf()) { acc, blockchainId -> + val isExcluded = storedExcludedBlockchainsIds[blockchainId] ?: localExcludedBlockchains[blockchainId] + + requireNotNull(isExcluded) { + "Unable to find $blockchainId in local or stored excluded blockchains" + } + + acc[blockchainId] = isExcluded + acc + } isInitialized = true } override suspend fun excludeBlockchain(mainnetId: String, isExcluded: Boolean) { - if (isExcluded) { - currentExcludedBlockchainsIds += mainnetId - } else { - currentExcludedBlockchainsIds -= mainnetId - } + currentExcludedBlockchains[mainnetId] = isExcluded storeCurrent() } override fun isMatchLocalConfig(): Boolean { - return currentExcludedBlockchainsIds == localExcludedBlockchainsIds + return currentExcludedBlockchains == localExcludedBlockchains } override suspend fun recoverLocalConfig() { - currentExcludedBlockchainsIds = localExcludedBlockchainsIds + currentExcludedBlockchains = localExcludedBlockchains.toMutableMap() storeCurrent() } private suspend fun storeCurrent() { - appPreferencesStore.store( - PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, - currentExcludedBlockchainsIds, + appPreferencesStore.storeObjectMap( + key = PreferencesKeys.EXCLUDED_BLOCKCHAINS_KEY, + value = currentExcludedBlockchains, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt index 7dbb49bbdf..a2a62175ec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/model/OnrampCurrencyDTO.kt @@ -12,7 +12,7 @@ data class OnrampCurrencyDTO( val code: String, @Json(name = "image") - val image: String, + val image: String?, @Json(name = "precision") val precision: Int, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index df2f985c7d..0e9a1885e9 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -55,7 +55,7 @@ object PreferencesKeys { val FEATURE_TOGGLES_KEY by lazy { stringPreferencesKey(name = "featureToggles") } - val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringSetPreferencesKey(name = "excludedBlockchains") } + val EXCLUDED_BLOCKCHAINS_KEY by lazy { stringPreferencesKey(name = "excludedBlockchainsV2") } val WAS_TWINS_ONBOARDING_SHOWN by lazy { booleanPreferencesKey(name = "twinsOnboardingShown") } diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt index b9427aee10..259ffd2cbb 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -3,15 +3,10 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink class BuyCurrencyDeepLink( - isOnrampFeatureEnabled: Boolean, val onReceive: (externalTxId: String) -> Unit, ) : DeepLink { - override val uri: String = if (isOnrampFeatureEnabled) { - ONRAMP_REDIRECT_DEEPLINK - } else { - BUY_REDIRECT_DEEPLINK - } + override val uri = BUY_REDIRECT_DEEPLINK override fun onReceive(params: Map) { onReceive( @@ -20,7 +15,7 @@ class BuyCurrencyDeepLink( } companion object { - const val ONRAMP_REDIRECT_DEEPLINK = "tangem://onramp-success?" + const val ONRAMP_REDIRECT_DEEPLINK = "https://tangem.com/success?action=dismissBrowser" private const val BUY_REDIRECT_DEEPLINK = "tangem://redirect?action=dismissBrowser" } } \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 3cc2368a82..aa37b93bdc 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -583,6 +583,8 @@ Der Kauf von Kryptowährungen bei Tangem wird von Drittanbietern zu deren Bedingungen durchgeführt. Suche nach Land Nicht verfügbar + Andere Währungen + Beliebte Fiats Suche nach Währung Durch die Nutzung der Onramp-Funktionalität stimmst Du den %1$s und %2$s des Anbieters zu. Der Kaufbetrag sollte nicht höher sein als %s diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 683983950c..1bd3de114c 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -6,10 +6,15 @@ Esto puede tardar unos segundos. Inténtelo de nuevo más tarde. Los datos aún no se han cargado. La acción no está disponible actualmente. Inténtalo de nuevo más tarde o actualiza los datos deslizando el dedo hacia abajo en la pantalla. + No disponible Elige el token - ¿No encuentras el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio + ¿No encuentra el token en tu billetera? Consulte los mercados para encontrarlo y agregarlo al intercambio No hay tokens disponibles para intercambiar con el token seleccionado. Por favor elige otro. No hay par disponible + Para utilizar la función de intercambio, su billetera debe contener al menos 2 tokens + Añada tokens + Sólo tienes 1 token añadido a tu portafolio. Para utilizar la función de intercambio, necesita añadir al menos 2 tokens + Añada tokens Seleccione el token que desea recibir Seleccione el token que desea intercambiar Elige red @@ -25,7 +30,7 @@ El seleccionado no admite la red %1$s Para activar el %1$s cifrado criptográfico de la blockchain, necesitarás reiniciar la billetera a los ajustes de fábrica. Por favor, retire sus fondos antes de hacerlo para asegurarte de no perderlos, y luego complete el proceso de reinicio. El acceso a la billetera actual no será posible después del reinicio. Esta tarjeta no admite %1$s tokens de red debido a una limitación del firmware. - ¿Tiene dificultades para escanear su tarjeta? + ¿Tiene dificultades para escanear su tarjeta/anillo? Esta tarjeta no está diseñada para funcionar con Tangem Tarifa por defecto Habilite las Tarifas predeterminadas para establecer automáticamente las tarifas de transacción y omitir la página de Tarifas al enviar fondos. Siempre puede volver a esta página si es necesario. @@ -65,10 +70,10 @@ El código de acceso se cambiará solo en esta tarjeta o este anillo Todas las tarjetas de la billetera seleccionada se han restablecido a la configuración de fábrica. Ahora puedes crear una nueva billetera. Reinicio completado - ¿Quiere restablecer la siguiente tarjeta en esta billetera? + ¿Quiere restablecer el siguiente dispositivo en esta billetera? Reinicio de tarjeta Recomendamos completar el proceso de reinicio para todas las tarjetas en esta billetera. - No ha reseteado todas sus tarjetas + No ha reseteado todas sus dispositivos Tangem Restablecer a ajustes de fábrica Modo de seguridad Ajustes del dispositivo @@ -266,6 +271,9 @@ El monto fue reembolsado en %1$s (red %2$s) Visite el sitio web del proveedor para la verificación Verificación KYC requerida por el proveedor + Comprado + Comprando + Comprando... Cancelado Confirmado Esperando confirmación @@ -305,15 +313,15 @@ No se encontraron fichas. Por favor intenta con otra solicitud ID : %s ID de transacción copiado - Convierta una de sus monedas a otra + Convierta una de sus activos de su portafolio por este token La siguiente información es opcional. Puede borrarla si no quiere compartirla. Cuéntenos qué funciones echa de menos y trataremos de ayudarle. - Por favor, díganos qué tarjeta tiene + Por favor, díganos qué tarjeta o anillo tiene Hola equipo de soporte, Por favor, cuéntenos más sobre tu problema. Cada pequeño detalle puede ayudar. Billetera previamente activada Mis recomendaciones - No se puede escanear una tarjeta + No se puede escanear una tarjeta/anillo Comentarios Comentarios en Tangem No se puede completar una transacción @@ -329,14 +337,14 @@ Ordenar Escanee Esta información fue generada con IA.\nPulse aquí si encuentra algún error. - Toque para cambiar la contraseña + Para cambiar el código de acceso coloque la tarjeta o el anillo como se muestra arriba y no lo retire hasta el fin de la operación Toque para cambiar la contraseña - Para crear la billetera, toque la tarjeta como se muestra arriba y no la retire hasta el final de la operación + Para crear la billetera, coloque la tarjeta o el anillo como se muestra arriba y no la retire hasta el fin de la operación Para crear la billetera, ponga el anillo como se muestra arriba y manténgala hasta que termine la operación. Toque la tarjeta no. %s de la billetera Coloque para escanear Toque para firmar - Coloque la tarjeta + Coloque la tarjeta o el anillo Ha actualizado la biometría, escanee su tarjeta o anillo para entrar Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia Saldo insuficiente @@ -379,7 +387,7 @@ Este activo no está disponible en la billetera Este activo no está disponible para esta billetera Añadir al portafolio - Agregue un token + Añadir Redes disponibles Mi portafolio Mercado @@ -391,7 +399,7 @@ Acciones rápidas Busque en el mercado Resultado - Ver tokens con una capitalización de mercado de menos de $100,000 + Ver tokens con una capitalización de mercado inferior a 100,000$ Mostrar tokens Sin resultado Seleccione una red @@ -478,9 +486,9 @@ NFC no está disponible en su dispositivo Deba configurar un único código de acceso para proteger todss sus dispositivos. Proteger - Puede configurar un código de acceso individual en cada tarjeta más tarde + Puede configurar un código de acceso individual en cada tarjeta más adelante Personalizar - El código de acceso se puede recuperar con una tarjeta o un anillo vinculada(o). No guarde todas las tarjetas en un mismo lugar. + El código de acceso se puede recuperar con una tarjeta o un anillo vinculada(o). No guarde todos los dispositivos en el mismo lugar. Restaurar Elige cualquier palabra, frase o número que quieras como su código de acceso Crear un código de acceso @@ -491,12 +499,12 @@ Por favor repita la operación. La tarjeta se restablecerá a la configuración de fábrica. Error de activación Agregar tokens - Ha agregado una tarjeta de backup. Cuando el proceso de backup finalice, no podrá añadir más tarjetas de backup. Si tiene otra tarjeta, agréguela al backup. ¿Quiere continuar el proceso de backup? + Ha agregado una tarjeta/anillo de backup. Cuando el proceso de backup finalice, no podrá añadir más dispositivos de backup. Si tiene otra tarjeta o anillo, agréguela al backup. ¿Quiere continuar el proceso de backup? El proceso de backup está parcialmente completo. No puede salir ahora. La frase de contraseña es una característica de seguridad avanzada utilizada por las billeteras criptográficas. Agrega una palabra o frase adicional de su elección a su frase de recuperación ya existente para desbloquear un conjunto completamente nuevo de direcciones. Agregar una tarjeta o un anillo de backup Escanee la tarjeta - Escanee la tarjeta no. %d + Escanee la tarjeta núm. %d Hacer backup ahora Escanee la tarjeta principal Escanee el anillo @@ -506,7 +514,7 @@ Escanee la tarjeta principal Finalizar más tarde ¿Cómo funciona? - Vamos a generar todas las claves en su tarjeta y crear una billetera segura + Vamos a generar todas las claves en su tarjeta o anillos y crear una billetera segura Crear una billetera Crear una billetera Otras opciones @@ -539,23 +547,23 @@ Legado Para comprobar si has escrito correctamente su seed phrase, por favor introduce las palabras 2ª, 7ª y 11ª Bien, vamos a comprobar - Para iniciar el proceso de backup, agregue hasta dos tarjetas de backup. - Puede agregar una tarjeta más o finalizar el proceso de backup + Para iniciar el proceso de backup, agregue hasta dos tarjetas/anillos de backup. + Puede agregar una tarjeta/anillo más o finalizar el proceso de backup Prepare su tarjeta de backup con el no.%s Escanee la tarjeta principal o el anillo para iniciar el proceso de backup. Prepare la tarjeta principal con el número %s Prepare el anillo y presione el botón de escaneo a continuación - Su tarjeta billetera está configurada y lista para usar. - Número máximo de tarjetas añadido. Finalice el proceso de backup. - Activando tarjeta + Su billetera está configurada y lista para usar. + Número máximo de dispositivos añadido. Finalice el proceso de backup. + Activando billetera Tarjeta de backup Tarjeta de backup no.%d Anillo de backup Ningunos dispositivos de backup Notificaciones - Una tarjeta de backup agregada + Un dispositivo de backup agregado Prepare su tarjeta - Dos tarjetas de backup agregadas + Dos dispositivos de backup agregados Para empezar, simplemente recargue la billetera con cualquier cantidad Para empezar, simplemente recargue la billetera con más de %1$s %2$s Comprar cripto @@ -563,7 +571,7 @@ Activar una billetera El proceso de emparejamiento está parcialmente completo. No puede salir ahora. Si el proceso de creación del wallet se interrumpe de alguna manera, tendrá que empezar de nuevo - Puede hacer backup de sus claves en hasta dos tarjetas Tangem Wallet en blanco. + Puede hacer backup de sus claves en hasta dos tarjetas o anillos Tangem no activadas. El código de acceso se puede restaurar con una de las tarjetas de backup. Todas las tarjetas de backup se pueden usar como funcionales con las claves idénticas. Podrá establecer un código de acceso para proteger sus billeteras. @@ -572,8 +580,11 @@ Tarjetas idénticas Código de acceso Disponible con %s + La compra de criptomonedas en Tangem se lleva a cabo utilizando proveedores externos usando sus propios términos. Buscar por país Indisponible + Otras monedas + Monedas Fiats Populares Buscar por moneda Al utilizar la funcionalidad onramp, acepta %1$s y %2$s del proveedor. El monto de la compra no debe ser mayor a %s @@ -604,7 +615,8 @@ %1$s (%2$s) en la red %3$s El envío de cualquier otra moneda supondrá una pérdida irreversible. Envíe solo %s a esta dirección. Enviar cualquier otra moneda resultará en su pérdida irreversible. - Transfiera fondos desde otra billetera o intercambio + Envíe solo %1$s en la red %2$s + Transfiera fondos desde otra billetera o exchange Participar Error al cargar la información sobre el programa de referidos. Por favor, inténtelo de nuevo más tarde. Error al cargar la información sobre el programa de referidos. Código de error: %s. Por favor, inténtelo de nuevo más tarde. @@ -806,17 +818,17 @@ Revotar Auto Manual - Bloquear - Día + Por Bloque + Diario Cada %s Cada día - Cada %s + Cada %s días Cada - Época + Por Época Era - Hora - Mes - Semana + Cada Hora + Mensual + Semanal Recompensas El stake está bloqueado Hacer más staking @@ -843,7 +855,7 @@ Votar Retirar Sus stakes - Almacene sus activos crypto de forma segura manteniendo las claves privadas contenidas en su tarjeta + Almacene sus criptomonedas de forma segura manteniendo las claves privadas almacenadas en su tarjeta Billetera de hardware revolucionaria Hasta 3 tarjetas físicas o anillos por billetera Backup ultra seguro @@ -857,7 +869,7 @@ La red cobrará una tarifa de aprobación de token para verificar que está autorizando el uso de su token para el swap. Intercambie más tokens a mejores tasas directamente en su billetera. ¡Nuevo proveedor de intercambio disponible! - El monto incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. + La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. El monto incluye:\n• tarifas del proveedor de servicios\n• tarifas de red por enviar %1$s desde el intercambio a la dirección del usuario. \n\nEl slippage del proveedor puede alcanzar el %2$s El importe incluye los honorarios del proveedor de servicios. El importe incluye las tarifas del proveedor de servicios. \n\nEl slippage del proveedor puede alcanzar el %s @@ -920,7 +932,7 @@ Ha escaneado la tarjeta gemela incorrecta. Por favor, intente con otra Esta que tiene en sus manos y la otra con el número %s.\n\nAmbas tarjetas pueden usarse para extraer fondos de esta billetera. Una billetera. Dos tarjetas. - Escanee la tarjeta no. %s + Escanee la tarjeta núm. %s Creando la billetera Escanear tarjeta gemela no. %s Preparando la tarjeta diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 55d428f3d3..e6647acb4d 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -580,8 +580,11 @@ Cartes identiques Code d\'accès Disponible avec %s + L\'achat de crypto-monnaies sur Tangem est assuré par des fournisseurs tiers selon leurs conditions. Recherche par pays Indisponible + Autres devises + Devises Fiats Populaires Recherche par devise En utilisant la fonctionnalité onramp, vous acceptez %1$s et %2$s du fournisseur Le montant de l\'achat ne doit pas dépasser %s diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index bd018bcf92..66cd763ddb 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -575,6 +575,8 @@ Tangemでの暗号通貨の買付は、サードパーティプロバイダーの条件に基づいて行われます。 国で検索 利用不可 + その他の通貨 + 人気の法定通貨 通貨で検索 オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします 買付金額は%s以下にしてください diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 92629c43f6..f63d0d3e28 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -5,7 +5,7 @@ Продать Это может занять несколько секунд. Пожалуйста, попробуйте позже. Данные ещё не загрузились - Это действие в данный момент недоступно, пожалуйста попробуйте позже или обновите данные, сделав свайп экрана вниз. + Это действие в данный момент недоступно, пожалуйста, попробуйте позже или обновите данные, сделав свайп экрана вниз. Кнопка недоступна Выберите токен Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для обмена. @@ -51,6 +51,8 @@ Больше не показывать Понятно Балансы скрыты + Согласно информации от разработчиков сети, токены Kaspa находятся в режиме бета. Следите за обновлениями! + Бета режим Пожалуйста, отсканируйте карту или кольцо Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту или кольцо Слишком много попыток @@ -227,7 +229,7 @@ Не выбрано Сеть Сеть - Вы можете добавить токен в ручную, если он не поддерживается Tangem + Вы можете добавить токен вручную, если он не поддерживается Tangem Например, USDC Символ Символ токена @@ -597,6 +599,8 @@ Tangem предоставляет доступ к покупке через сторонних провайдеров в соответствии с их правилами Поиск по стране Недоступно + Другие валюты + Популярные фиаты Поиск по валюте Пользуясь сервисом покупки, вы соглашаетесь с %1$s и %2$s Сумма покупки не может быть больше, чем %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 7a2ba44136..9b615a149f 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -12,6 +12,7 @@ Немає доступних токенів для обміну з обраним токеном. Будь ласка, оберіть інший. Немає вільної пари Щоб скористатися функцією обміну, у вашому портфелі має бути щонайменше 2 токени. + Додайте токени У вас додано лише 1 токен. Щоб скористатися функцією обміну, потрібно додати мінімум 2 токени. Додайте токени Оберіть токен для отримання @@ -324,6 +325,7 @@ Розкажіть, будь ласка, яку картку або кільце ви маєте? Привіт, команда підтримки, Будь ласка, розкажіть нам більше про вашу проблему. Кожна дрібниця може допомогти. + Раніше активований гаманець Мої пропозиції Не вдається відсканувати картку/кільце Звернення в підтримку @@ -597,7 +599,10 @@ Tangem забезпечує доступ к покупці через сторонніх провайдерів згідно з їхніми умовами Пошук за країною Недоступно + Інші валюти + Популярні фіати Пошук по валюті + Використовуючи сервіс покупки, ви погоджуєтесь з %1$s та %2$s Сума покупки не може бути більше ніж %s Сума покупки повинна бути не менше %s Для данної валюти немає доступних провайдерів diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 12d15e5487..99759792da 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -51,6 +51,8 @@ Don\'t show again Got it Balances are hidden + According to the blockchain developers, Kaspa tokens are currently in beta. Stay tuned for updates! + Beta Mode Please scan the card or ring Please try again in 30 seconds or scan the card or ring Too many attempts @@ -583,6 +585,8 @@ Buying crypto in Tangem is powered by third-party providers on their terms. Search by country Unavailable + Other currencies + Popular Fiats Search by currency By using onramp functionality, you agree with provider’s %1$s and %2$s The purchase amount should be no more than %s diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index ca78dbeca6..578611fb04 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -32,10 +32,8 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject -import com.tangem.datasource.local.preferences.utils.getObjectSyncOrDefault import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject -import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError @@ -251,6 +249,7 @@ internal class DefaultOnrampRepository( maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), paymentMethod = paymentMethod, provider = provider, + countryCode = response.countryCode, ) }, onError = { error -> @@ -259,6 +258,7 @@ internal class DefaultOnrampRepository( paymentMethod = paymentMethod, provider = provider, fromOnrampAmount = fromOnrampAmount, + countryCode = country.code, ) }, ) @@ -281,6 +281,7 @@ internal class DefaultOnrampRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, + isDarkTheme: Boolean, ): OnrampTransaction = withContext(dispatchers.io) { try { val address = requireNotNull( @@ -306,7 +307,7 @@ internal class DefaultOnrampRepository( toAddress = address, redirectUrl = BuyCurrencyDeepLink.ONRAMP_REDIRECT_DEEPLINK, language = null, - theme = getTheme(), + theme = getTheme(isDarkTheme), requestId = requestId, ).bind() }, @@ -415,7 +416,7 @@ internal class DefaultOnrampRepository( fromCurrency = currency, toAmount = quote.toAmount.value, toCurrencyId = cryptoCurrency.id.value, - status = OnrampStatus.Status.Expired, + status = OnrampStatus.Status.Created, externalTxUrl = onrampDataJson.externalTxUrl, externalTxId = onrampDataJson.externalTxId, timestamp = DateTime.now().millis, @@ -442,17 +443,10 @@ internal class DefaultOnrampRepository( ) } - private suspend fun getTheme(): String { - val appTheme = appPreferencesStore.getObjectSyncOrDefault( - key = PreferencesKeys.APP_THEME_MODE_KEY, - default = AppThemeMode.DEFAULT, - ) - return when (appTheme) { - AppThemeMode.FORCE_DARK -> PROVIDER_THEME_DARK - AppThemeMode.FORCE_LIGHT, - AppThemeMode.FOLLOW_SYSTEM, - -> PROVIDER_THEME_LIGHT - } + private fun getTheme(isDarkTheme: Boolean): String = if (isDarkTheme) { + PROVIDER_THEME_DARK + } else { + PROVIDER_THEME_LIGHT } private fun List.removeApplePay(): List = filterNot { it.id == "apple-pay" } @@ -462,18 +456,26 @@ internal class DefaultOnrampRepository( paymentMethod: OnrampPaymentMethod, provider: OnrampProvider, fromOnrampAmount: OnrampAmount, + countryCode: String, ) = if (error is ApiResponseError.HttpException) { val onrampError = onrampErrorConverter.convert(value = error.errorBody.orEmpty()) if (onrampError is OnrampError.AmountError) { + OnrampQuote.AmountError( + paymentMethod = paymentMethod, + provider = provider, + fromAmount = fromOnrampAmount, + error = onrampError, + countryCode = countryCode, + ) + } else { + Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") OnrampQuote.Error( paymentMethod = paymentMethod, provider = provider, fromAmount = fromOnrampAmount, error = onrampError, + countryCode = countryCode, ) - } else { - Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") - null } } else { Timber.w(error, "Unable to fetch onramp quotes for ${provider.id}. $error") diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index ea747277bc..4fb9ef47d2 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -138,32 +138,32 @@ internal class DefaultCurrenciesRepository( } private fun populateCurrenciesWithMissedCoins(currencies: List): List { - val networkToCoin = mutableMapOf() - val networkToTokens = mutableMapOf>() + val currenciesSequence = currencies.asSequence() - currencies.forEach { currency -> - when (currency) { - is CryptoCurrency.Coin -> { - networkToCoin[currency.network] = currency - } - is CryptoCurrency.Token -> { - val tokens = networkToTokens.getOrElse(currency.network, ::emptyList) - networkToTokens[currency.network] = tokens + currency - } - } - } + val networksWithTokens = currenciesSequence + .filterIsInstance() + .map { it.network } + .distinct() + + val networksWithCoins = currenciesSequence + .filterIsInstance() + .map { it.network } + .distinct() + + val networksNeedingCoins = (networksWithTokens - networksWithCoins.toSet()).toMutableList() + + if (networksNeedingCoins.isEmpty()) return currencies return buildList { - if (networkToTokens.isEmpty()) { - // add only coin here - addAll(networkToCoin.values) - } else { - networkToTokens.forEach { (network, networkTokens) -> - val networkCoin = networkToCoin[network] ?: cryptoCurrencyFactory.createCoin(network) + currencies.forEach { currency -> + if (currency is CryptoCurrency.Token && currency.network in networksNeedingCoins) { + val coin = cryptoCurrencyFactory.createCoin(currency.network) + add(coin) - add(networkCoin) - addAll(networkTokens) + networksNeedingCoins.remove(currency.network) } + + add(currency) } } } diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt index 20f225f582..8b5bdbf34b 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampCurrency.kt @@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable data class OnrampCurrency( val name: String, val code: String, - val image: String, + val image: String?, val precision: Int, val unit: String, ) \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt index f0d51802d6..2891d6072d 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProviderWithQuote.kt @@ -23,7 +23,7 @@ sealed interface OnrampProviderWithQuote { data class Error( override val provider: OnrampProvider, - val quoteError: OnrampQuote.Error, + val quoteError: OnrampQuote.AmountError, ) : Unavailable } } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt index cc6a4586c1..dde5f19064 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampQuote.kt @@ -6,20 +6,32 @@ sealed class OnrampQuote { abstract val paymentMethod: OnrampPaymentMethod abstract val provider: OnrampProvider + abstract val fromAmount: OnrampAmount + abstract val countryCode: String data class Data( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val fromAmount: OnrampAmount, + override val fromAmount: OnrampAmount, + override val countryCode: String, val toAmount: OnrampAmount, val minFromAmount: OnrampAmount, val maxFromAmount: OnrampAmount, ) : OnrampQuote() + data class AmountError( + override val paymentMethod: OnrampPaymentMethod, + override val provider: OnrampProvider, + override val fromAmount: OnrampAmount, + override val countryCode: String, + val error: OnrampError.AmountError, + ) : OnrampQuote() + data class Error( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val fromAmount: OnrampAmount, - val error: OnrampError.AmountError, + override val fromAmount: OnrampAmount, + override val countryCode: String, + val error: OnrampError, ) : OnrampQuote() } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt index 22238ab748..1d6c6700d6 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampProviderWithQuoteUseCase.kt @@ -31,8 +31,8 @@ class GetOnrampProviderWithQuoteUseCase( private fun List.quoteWithProvider( provider: OnrampProvider, selectedPaymentMethod: OnrampPaymentMethod, - ): OnrampProviderWithQuote { - val matchedQuote = firstOrNull { it.paymentMethod == selectedPaymentMethod } + ): OnrampProviderWithQuote? { + val matchedQuote = firstOrNull { it.paymentMethod.id == selectedPaymentMethod.id } return when (matchedQuote) { is OnrampQuote.Data -> OnrampProviderWithQuote.Data( provider = matchedQuote.provider, @@ -40,14 +40,23 @@ class GetOnrampProviderWithQuoteUseCase( toAmount = matchedQuote.toAmount, fromAmount = matchedQuote.fromAmount, ) - is OnrampQuote.Error -> Unavailable.Error( + is OnrampQuote.AmountError -> Unavailable.Error( provider = matchedQuote.provider, quoteError = matchedQuote, ) - null -> Unavailable.NotSupportedPaymentMethod( - provider = provider, - availablePaymentMethods = provider.paymentMethods, - ) + is OnrampQuote.Error -> null + null -> { + val availablePaymentMethods = getAvailablePaymentMethods(provider) + Unavailable.NotSupportedPaymentMethod( + provider = provider, + availablePaymentMethods = availablePaymentMethods, + ).takeIf { availablePaymentMethods.isNotEmpty() } + } } } + + private fun List.getAvailablePaymentMethods(provider: OnrampProvider) = provider.paymentMethods + .filter { pm -> + filter { it !is OnrampQuote.Error }.any { it.paymentMethod.id == pm.id } + } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt index 1da7243f67..24b27d4860 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampQuotesUseCase.kt @@ -47,9 +47,9 @@ class GetOnrampQuotesUseCase( grouped.value.sortedByDescending { when (it) { is OnrampQuote.Data -> it.toAmount.value - + is OnrampQuote.Error -> null // negative difference to sort both when data and unavailable is present - is OnrampQuote.Error -> { + is OnrampQuote.AmountError -> { when (val error = it.error) { is OnrampError.AmountError.TooSmallError -> it.fromAmount.value - error.requiredAmount is OnrampError.AmountError.TooBigError -> error.requiredAmount - it.fromAmount.value diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt index 34bb9f5202..55ba74a5bf 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt @@ -19,12 +19,14 @@ class GetOnrampRedirectUrlUseCase( userWalletId: UserWalletId, quote: OnrampProviderWithQuote.Data, cryptoCurrency: CryptoCurrency, + isDarkTheme: Boolean, ): Either { return Either.catch { val transaction = repository.getOnrampData( userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, quote = quote, + isDarkTheme = isDarkTheme, ) transactionRepository.storeTransaction(transaction) transaction.redirectUrl diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt index a79f332c23..20effbbeeb 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt @@ -11,9 +11,9 @@ class GetOnrampStatusUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(externalTxId: String): Either { + suspend operator fun invoke(txId: String): Either { return Either.catch { - onrampRepository.getStatus(externalTxId) + onrampRepository.getStatus(txId) }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index e577385a98..cf54baee5b 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -1,7 +1,6 @@ package com.tangem.domain.onramp.analytics import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam.Key.CURRENCY import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.PAYMENT_METHOD @@ -37,7 +36,7 @@ sealed class OnrampAnalyticsEvent( private val currency: String, ) : OnrampAnalyticsEvent( event = "Currency Chosen", - params = mapOf(CURRENCY to currency), + params = mapOf("Currency Type" to currency), ) data object CloseOnramp : OnrampAnalyticsEvent(event = "Button - Close") @@ -114,7 +113,7 @@ sealed class OnrampAnalyticsEvent( event = "Button - Buy", params = mapOf( PROVIDER to providerName, - CURRENCY to currency, + "Currency Type" to currency, TOKEN_PARAM to tokenSymbol, ), ) @@ -131,7 +130,7 @@ sealed class OnrampAnalyticsEvent( TOKEN_PARAM to tokenSymbol, PROVIDER to providerName, RESIDENCE to residence, - CURRENCY to currency, + "Currency Type" to currency, PAYMENT_METHOD to paymentMethod, ), ) @@ -143,11 +142,13 @@ sealed class OnrampAnalyticsEvent( private val tokenSymbol: String, private val errorCode: String, private val providerName: String?, + private val paymentMethod: String?, ) : OnrampAnalyticsEvent( event = "Errors", params = buildMap { put(TOKEN_PARAM, tokenSymbol) providerName?.let { put(PROVIDER, providerName) } + paymentMethod?.let { put(PAYMENT_METHOD, paymentMethod) } put(ERROR_CODE, errorCode) }, ) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index 87e9bff955..302a563b12 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -24,6 +24,7 @@ interface OnrampRepository { userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, + isDarkTheme: Boolean, ): OnrampTransaction // cache diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index d42f2f31b5..804f5d06c5 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -74,6 +74,16 @@ sealed class TokenScreenAnalyticsEvent( params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), ) + class RevealTryAgain(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent( + event = "Button - Reveal Try Again", + params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), + ) + + class RevealCancel(tokenSymbol: String, blockchain: String) : TokenScreenAnalyticsEvent( + event = "Button - Reveal Cancel", + params = mapOf("Token" to tokenSymbol, "Blockchain" to blockchain), + ) + data class StakingClicked(val token: String) : TokenScreenAnalyticsEvent( event = "Staking Clicked", params = mapOf("Token" to token), diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index 4c4473250c..ccd2264205 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -57,4 +57,6 @@ sealed class CryptoCurrencyWarning { val amount: BigDecimal, val maxAmount: BigDecimal?, ) : CryptoCurrencyWarning() + + data object TokensInBetaWarning : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 3e54e19951..e6975190ee 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -173,6 +173,7 @@ class GetCurrencyWarningsUseCase( when { tokenStatus != null && coinStatus != null -> { buildList { + getIsBetaTokensWarning(tokenStatus.currency)?.let(::add) getFeeWarning( userWalletId = userWalletId, coinStatus = coinStatus, @@ -219,6 +220,15 @@ class GetCurrencyWarningsUseCase( } } + private fun getIsBetaTokensWarning(currency: CryptoCurrency): CryptoCurrencyWarning? { + val isTokenBetaFunctionality = BlockchainUtils.isTokenBetaFunctionality(currency.network.id.value) + return if (currency is CryptoCurrency.Token && isTokenBetaFunctionality) { + CryptoCurrencyWarning.TokensInBetaWarning + } else { + null + } + } + private suspend fun constructTokenBalanceNotEnoughWarning( userWalletId: UserWalletId, tokenStatus: CryptoCurrencyStatus, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt index a9ffeda4c8..58b0a13e7f 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt @@ -1,11 +1,9 @@ package com.tangem.domain.tokens.legacy import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import org.rekotlin.Action -import java.math.BigDecimal sealed class TradeCryptoAction : Action { @@ -23,29 +21,4 @@ sealed class TradeCryptoAction : Action { val cryptoCurrencyStatus: CryptoCurrencyStatus, val appCurrencyCode: String, ) : TradeCryptoAction() - - @Deprecated("Use AppRoute instead") - data class SendToken( - val userWallet: UserWallet, - val tokenCurrency: CryptoCurrency.Token, - val tokenFiatRate: BigDecimal?, - val coinFiatRate: BigDecimal?, - val feeCurrencyStatus: CryptoCurrencyStatus?, - val transactionInfo: TransactionInfo? = null, - ) : TradeCryptoAction() - - @Deprecated("Use AppRoute instead") - data class SendCoin( - val userWallet: UserWallet, - val coinStatus: CryptoCurrencyStatus, - val feeCurrencyStatus: CryptoCurrencyStatus?, - val transactionInfo: TransactionInfo? = null, - ) : TradeCryptoAction() - - data class TransactionInfo( - val transactionId: String, - val destinationAddress: String, - val amount: String, - val tag: String? = null, - ) } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index 87bd32ab50..55dbe5ca34 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -47,11 +47,16 @@ import java.nio.charset.StandardCharsets @Composable internal fun DisclaimerScreen(state: DisclaimerUM) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val bottomPadding = bottomBarHeight + TangemTheme.dimens.size16 - + val bottomPadding = if (state.isTosAccepted) { + bottomBarHeight + TangemTheme.dimens.size16 + } else { + bottomBarHeight + TangemTheme.dimens.size64 + } + val backgroundColor = TangemTheme.colors.background.primary + val (textColor, iconColor) = TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1 Box( modifier = Modifier - .background(TangemTheme.colors.background.primary) + .background(backgroundColor) .statusBarsPadding() .testTag(DISCLAIMER_SCREEN_CONTAINER), ) { @@ -67,6 +72,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, titleAlignment = Alignment.CenterHorizontally, + textColor = textColor, + iconTint = iconColor, ) DisclaimerContent(state.url) } @@ -74,7 +81,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { if (!state.isTosAccepted) { BottomFade( modifier = Modifier.align(Alignment.BottomCenter), - backgroundColor = TangemTheme.colors.background.primary, + backgroundColor = backgroundColor, ) DisclaimerButton(state.onAccept) } else { @@ -85,6 +92,8 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { @Composable private fun DisclaimerContent(url: String) { + val backgroundColor = TangemTheme.colors.background.primary + val webViewStateUrl = rememberWebViewState(url) val webViewStateData = rememberWebViewStateWithHTMLData( data = localTermsOfServices, @@ -119,12 +128,12 @@ private fun DisclaimerContent(url: String) { exit = fadeOut(), modifier = Modifier .fillMaxSize() - .background(TangemTheme.colors.background.primary), + .background(backgroundColor), ) { Box( modifier = Modifier .fillMaxSize() - .background(TangemTheme.colors.background.primary), + .background(backgroundColor), ) { CircularProgressIndicator( color = TangemTheme.colors.icon.informative, diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 4f73123e32..87db7e37d6 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -14,6 +14,7 @@ android { dependencies { /* Project - API */ api(projects.features.markets.api) + api(projects.features.onramp.api) implementation(projects.core.navigation) /* Domain */ diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 70e107356a..9868bc2b34 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -23,6 +23,7 @@ import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -41,12 +42,15 @@ internal class TokenActionsHandler @AssistedInject constructor( @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, + private val onrampFeatureToggles: OnrampFeatureToggles, ) { - private val disabledActionsInDemoMode = setOf( - TokenActionsBSContentUM.Action.Buy, - TokenActionsBSContentUM.Action.Sell, - ) + private val disabledActionsInDemoMode = buildSet { + if (!onrampFeatureToggles.isFeatureEnabled) { + add(TokenActionsBSContentUM.Action.Buy) + } + add(TokenActionsBSContentUM.Action.Sell) + } fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { onHandleQuickAction( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt index feb6cd9c68..7dda243f37 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt @@ -10,7 +10,7 @@ internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent { data class Params( val cryptoCurrency: CryptoCurrency, val country: OnrampCountry, - val onDismiss: (OnrampCountry) -> Unit, + val onDismiss: () -> Unit, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt index 62dd39d0dd..0a88231cc3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -66,11 +66,17 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor( config: ConfirmResidencyBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { - ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create( + is ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create( context = childByContext(componentContext), params = SelectCountryComponent.Params( cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, + onDismiss = { + // Dismiss country select sheet + model.bottomSheetNavigation.dismiss() + + // Dismiss confirm residency sheet + config.onDismiss() + }, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt index baacb6b92a..0900dfebc2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/entity/ConfirmResidencyBottomSheetConfig.kt @@ -6,5 +6,5 @@ import kotlinx.serialization.Serializable internal sealed class ConfirmResidencyBottomSheetConfig { @Serializable - data object SelectCountry : ConfirmResidencyBottomSheetConfig() + data class SelectCountry(val onDismiss: () -> Unit) : ConfirmResidencyBottomSheetConfig() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index 5237af3c2b..270c3e6f73 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM @@ -35,7 +36,7 @@ internal class ConfirmResidencyModel @Inject constructor( country = params.country.name, countryFlagUrl = params.country.image, isCountrySupported = params.country.onrampAvailable, - primaryButtonConfig = getPrimaryButtonConfig(), + primaryButtonConfig = getPrimaryButtonConfig(params.country), secondaryButtonConfig = ConfirmResidencyUM.ActionButtonConfig( onClick = ::onChangeClick, text = resourceReference(R.string.common_change), @@ -47,11 +48,14 @@ internal class ConfirmResidencyModel @Inject constructor( analyticsEventHandler.send(OnrampAnalyticsEvent.ResidenceConfirmScreenOpened(params.country.name)) } - private fun getPrimaryButtonConfig() = if (params.country.onrampAvailable) { + private fun getPrimaryButtonConfig(country: OnrampCountry) = if (country.onrampAvailable) { ConfirmResidencyUM.ActionButtonConfig( onClick = { - analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(params.country.name)) - modelScope.launch { saveDefaultCountryUseCase.invoke(params.country) } + analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceConfirm(country.name)) + modelScope.launch { + saveDefaultCountryUseCase.invoke(country) + params.onDismiss() + } }, text = resourceReference(R.string.common_confirm), ) @@ -67,6 +71,6 @@ internal class ConfirmResidencyModel @Inject constructor( private fun onChangeClick() { analyticsEventHandler.send(OnrampAnalyticsEvent.OnResidenceChange) - bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry) + bottomSheetNavigation.activate(ConfirmResidencyBottomSheetConfig.SelectCountry(params.onDismiss)) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 739d1a4d6e..5164c91d5b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -58,7 +58,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( country = config.country, onDismiss = { model.bottomSheetNavigation.dismiss() - model.handleOnrampAvailable(it.defaultCurrency) + model.handleOnrampAvailable() }, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt index d0cfbe8515..c56ecedb67 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt @@ -10,7 +10,12 @@ internal data class OnrampAmountBlockUM( val secondaryFieldModel: OnrampAmountSecondaryFieldUM, ) -internal data class OnrampCurrencyUM(val code: String, val iconUrl: String, val precision: Int, val onClick: () -> Unit) +internal data class OnrampCurrencyUM( + val code: String, + val iconUrl: String?, + val precision: Int, + val onClick: () -> Unit, +) @Immutable internal sealed interface OnrampAmountSecondaryFieldUM { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt new file mode 100644 index 0000000000..bc6113303c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt @@ -0,0 +1,8 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.domain.onramp.model.OnrampAmount + +data class OnrampLastUpdate( + val lastAmount: OnrampAmount, + val lastCountryString: String, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 136405af33..18a7bb1727 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -49,8 +49,11 @@ internal class OnrampStateFactory( fun getOnrampErrorState(onrampError: OnrampError): OnrampMainComponentUM { return when (onrampError) { OnrampError.PairsNotFound -> getNoPairsErrorState() - is OnrampError.DataError -> getErrorState(onrampError.code) - is OnrampError.DomainError -> getErrorState() + is OnrampError.DataError -> getErrorState( + errorCode = onrampError.code, + onRefresh = onrampIntents::onRefresh, + ) + is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh) is OnrampError.AmountError.TooBigError, is OnrampError.AmountError.TooSmallError, OnrampError.RedirectError.VerificationFailed, @@ -74,7 +77,7 @@ internal class OnrampStateFactory( ) } - fun getErrorState(errorCode: String? = null): OnrampMainComponentUM { + fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() val endButton = state.topBarConfig.endButtonUM.copy(enabled = true) @@ -89,13 +92,13 @@ internal class OnrampStateFactory( providerBlockState = OnrampProviderBlockUM.Empty, errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, - onRefresh = onrampIntents::onRefresh, + onRefresh = onRefresh, ), ) is OnrampMainComponentUM.InitialLoading -> state.copy( errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, - onRefresh = onrampIntents::onRefresh, + onRefresh = onRefresh, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index 5d85cb0561..2a443efd36 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -14,15 +14,18 @@ import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.model.AmountType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.* import com.tangem.features.onramp.providers.entity.SelectProviderResult import com.tangem.utils.Provider +import com.tangem.utils.extensions.isSingleItem internal class OnrampAmountStateFactory( private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, private val onrampIntents: OnrampIntents, + private val cryptoCurrency: CryptoCurrency, ) { private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( @@ -62,11 +65,12 @@ internal class OnrampAmountStateFactory( return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), + providerBlockState = OnrampProviderBlockUM.Loading, buyButtonConfig = currentState.buyButtonConfig.copy(enabled = false), ) } - fun getAmountSecondaryUpdatedState(quote: OnrampQuote, isBestRate: Boolean): OnrampMainComponentUM { + fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM { val currentState = currentStateProvider() if (currentState !is OnrampMainComponentUM.Content) return currentState @@ -75,9 +79,9 @@ internal class OnrampAmountStateFactory( return currentState.copy( amountBlockState = amountState.copy( - secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState), + amountFieldModel = amountState.amountFieldModel.copy(isError = false), + secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, ), - providerBlockState = quote.toProviderBlockState(isBestRate), buyButtonConfig = currentState.buyButtonConfig.copy( enabled = quote is OnrampQuote.Data, onClick = { @@ -96,6 +100,32 @@ internal class OnrampAmountStateFactory( ) } + fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List): OnrampMainComponentUM { + val currentState = currentStateProvider() + if (currentState !is OnrampMainComponentUM.Content) return currentState + + analyticsEventHandler.send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = selectedQuote.provider.info.name, + tokenSymbol = cryptoCurrency.symbol, + paymentMethod = selectedQuote.paymentMethod.name, + ), + ) + + val bestProvider = selectedQuote as? OnrampQuote.Data + val isMultipleQuotes = !quotes.isSingleItem() + val isOtherQuotesHasData = quotes + .filter { it.paymentMethod == selectedQuote.paymentMethod } + .filterNot { it == bestProvider } + .any { it is OnrampQuote.Data } + + val isBestProvider = selectedQuote == bestProvider && isMultipleQuotes && isOtherQuotesHasData + + return currentState.copy( + providerBlockState = selectedQuote.toProviderBlockState(isBestProvider), + ) + } + fun getAmountSecondaryUpdatedState( providerResult: SelectProviderResult, isBestRate: Boolean, @@ -175,19 +205,20 @@ internal class OnrampAmountStateFactory( ) } - private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM { + private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? { return when (this) { + is OnrampQuote.Error -> null is OnrampQuote.Data -> { val amount = toAmount.value.format { crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) } OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) } - is OnrampQuote.Error -> this.toSecondaryFieldUiModel(amountState) + is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) } } - private fun OnrampQuote.Error.toSecondaryFieldUiModel( + private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( amountState: OnrampAmountBlockUM, ): OnrampAmountSecondaryFieldUM.Error { val amount = error.requiredAmount.format { @@ -203,7 +234,7 @@ internal class OnrampAmountStateFactory( R.string.onramp_max_amount_restriction } is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError) + analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError) R.string.onramp_min_amount_restriction } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 514f0a3316..5a09dc2d37 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -19,17 +19,13 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.OnrampMainComponent -import com.tangem.features.onramp.main.entity.OnrampIntents -import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM +import com.tangem.features.onramp.main.entity.* import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory import com.tangem.features.onramp.providers.entity.SelectProviderResult @@ -39,15 +35,13 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler -import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class OnrampMainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -76,19 +70,20 @@ internal class OnrampMainComponentModel @Inject constructor( currentStateProvider = Provider { _state.value }, analyticsEventHandler = analyticsEventHandler, onrampIntents = this, + cryptoCurrency = params.cryptoCurrency, ) private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val _state: MutableStateFlow = MutableStateFlow( value = stateFactory.getInitialState( currency = params.cryptoCurrency.name, - onClose = router::pop, + onClose = ::onCloseClick, ), ) private val quotesTaskScheduler = SingleTaskScheduler() val state: StateFlow get() = _state.asStateFlow() val bottomSheetNavigation: SlotNavigation = SlotNavigation() - private val lastAmount = mutableStateOf(BigDecimal.ZERO) + private val lastUpdateState = mutableStateOf(null) init { sendScreenOpenAnalytics() @@ -105,8 +100,7 @@ internal class OnrampMainComponentModel @Inject constructor( ) } - fun handleOnrampAvailable(currency: OnrampCurrency) { - _state.update { stateFactory.getReadyState(currency) } + fun handleOnrampAvailable() { subscribeToCountryAndCurrencyUpdates() subscribeToQuotesUpdate() } @@ -125,7 +119,7 @@ internal class OnrampMainComponentModel @Inject constructor( private fun handleOnrampAvailability(availability: OnrampAvailability) { when (availability) { - is OnrampAvailability.Available -> handleOnrampAvailable(availability.currency) + is OnrampAvailability.Available -> handleOnrampAvailable() is OnrampAvailability.ConfirmResidency, is OnrampAvailability.NotSupported, -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) @@ -139,7 +133,13 @@ internal class OnrampMainComponentModel @Inject constructor( ifLeft = ::handleOnrampError, ifRight = { country -> if (country == null) return@onEach - _state.update { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) } + _state.update { + if (it is OnrampMainComponentUM.InitialLoading) { + stateFactory.getReadyState(country.defaultCurrency) + } else { + amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + } + } updatePairsAndQuotes() }, ) @@ -157,7 +157,11 @@ internal class OnrampMainComponentModel @Inject constructor( } private suspend fun updatePairsAndQuotes() { - _state.update { amountStateFactory.getAmountSecondaryLoadingState() } + val state = state.value as? OnrampMainComponentUM.Content + + if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) { + _state.update { amountStateFactory.getAmountSecondaryLoadingState() } + } fetchPairsUseCase.invoke(params.cryptoCurrency).fold( ifLeft = ::handleOnrampError, ifRight = { _state.update { amountStateFactory.getAmountSecondaryResetState() } }, @@ -196,6 +200,7 @@ internal class OnrampMainComponentModel @Inject constructor( private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() + .conflate() .onEach { maybeQuotes -> maybeQuotes.fold( ifLeft = ::handleOnrampError, @@ -268,44 +273,75 @@ internal class OnrampMainComponentModel @Inject constructor( super.onDestroy() } - private fun handleQuoteResult(quotes: List) { - quotes.filterIsInstance().forEach { errorState -> - sendOnrampErrorAnalytic(errorState.error) - } + private fun onCloseClick() { + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp) + router.pop() + } - val quote = quotes.firstOrNull() + private fun handleQuoteResult(quotes: List) { + sendOnrampQuotesErrorAnalytic(quotes) + + val quote = selectOrUpdateQuote(quotes) if (quote == null) { - _state.update { stateFactory.getErrorState() } + _state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + lastUpdateState.value = null return } - val bestProvider = quote as? OnrampQuote.Data - val isMultipleQuotes = !quotes.isSingleItem() - val isOtherQuotesHasData = quotes - .filter { it.paymentMethod == quote.paymentMethod } - .filterNot { it == bestProvider } - .any { it is OnrampQuote.Data } - val hasBestProvider = isMultipleQuotes && isOtherQuotesHasData - - val isBestProvider = quote == bestProvider && hasBestProvider - - if (quote is OnrampQuote.Data && lastAmount.value != quote.fromAmount.value) { - lastAmount.value = quote.fromAmount.value - analyticsEventHandler.send( - OnrampAnalyticsEvent.ProviderCalculated( - providerName = quote.provider.info.name, - tokenSymbol = params.cryptoCurrency.symbol, - paymentMethod = quote.paymentMethod.name, - ), + if (checkLastInputState(quote)) { + lastUpdateState.value = OnrampLastUpdate( + quote.fromAmount, + quote.countryCode, ) + + _state.update { + amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) + } } + _state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } + } + + /** + * !!! Important quote selection logic !!! + * Selects or updated quote based on input data (amount, country, currency). + * If input data has changed select new best quote, otherwise last selected quote. + * If last selected quote on same input data is in an error state, select next best quote + * If new best quote or next best quote does not exist (i.e. Error state) select nothing. + */ + private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { + val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } + + // Check if amount, country or currency has changed + return if (checkLastInputState(quoteToCheck)) { + quoteToCheck + } else { + val state = state.value as? OnrampMainComponentUM.Content + val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content + + // Get current selected quote to update + val lastSelectedQuote = quotes.firstOrNull { + it.provider.id == providerState?.providerId && + it.paymentMethod.id == providerState.paymentMethod.id + } + + // Check if selected updated quote is not error + if (lastSelectedQuote is OnrampQuote.Error) { + quoteToCheck + } else { + lastSelectedQuote + } + } + } + + private fun onRetryQuotes() { _state.update { - amountStateFactory.getAmountSecondaryUpdatedState( - quote = quote, - isBestRate = isBestProvider, - ) + (it as? OnrampMainComponentUM.Content)?.copy( + errorNotification = null, + providerBlockState = OnrampProviderBlockUM.Loading, + ) ?: it } + startLoadingQuotes() } private fun showDemoWarning() { @@ -344,9 +380,35 @@ internal class OnrampMainComponentModel @Inject constructor( error = error, tokenSymbol = params.cryptoCurrency.symbol, providerName = providerContent?.providerName, + paymentMethod = providerContent?.paymentMethod?.name, ) } + private fun sendOnrampQuotesErrorAnalytic(quotes: List) { + quotes.forEach { errorState -> + when (errorState) { + is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent( + error = errorState.error, + tokenSymbol = params.cryptoCurrency.symbol, + providerName = errorState.provider.info.name, + paymentMethod = errorState.paymentMethod.name, + ) + is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent( + error = errorState.error, + tokenSymbol = params.cryptoCurrency.symbol, + providerName = errorState.provider.info.name, + paymentMethod = errorState.paymentMethod.name, + ) + else -> { /* no-op */ } + } + } + } + + private fun checkLastInputState(quote: OnrampQuote?): Boolean { + return lastUpdateState.value?.lastAmount != quote?.fromAmount || + lastUpdateState.value?.lastCountryString != quote?.countryCode + } + private companion object { const val UPDATE_DELAY = 10_000L } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt index 0e6727be5b..f3c9d5341b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListItemUM.kt @@ -6,30 +6,42 @@ internal sealed interface ProviderListItemUM { val providerId: String val name: String + val imageUrl: String - data class Available( - override val providerId: String, - override val name: String, - val imageUrl: String, - val rate: String, - val isBestRate: Boolean, - val diffRate: TextReference?, - val isSelected: Boolean, - val onClick: () -> Unit, - ) : ProviderListItemUM + sealed interface Available : ProviderListItemUM { + val onClick: () -> Unit + val providerResult: SelectProviderResult + val isBestRate: Boolean + val isSelected: Boolean - data class AvailableWithError( - override val providerId: String, - override val name: String, - val imageUrl: String, - val subtitle: TextReference, - val onClick: () -> Unit, - ) : ProviderListItemUM + data class Content( + override val providerId: String, + override val name: String, + override val onClick: () -> Unit, + override val imageUrl: String, + override val providerResult: SelectProviderResult, + override val isBestRate: Boolean, + override val isSelected: Boolean, + val rate: String, + val diffRate: TextReference?, + ) : Available + + data class WithError( + override val providerId: String, + override val name: String, + override val onClick: () -> Unit, + override val imageUrl: String, + override val providerResult: SelectProviderResult, + override val isBestRate: Boolean, + override val isSelected: Boolean, + val subtitle: TextReference, + ) : Available + } data class Unavailable( override val providerId: String, override val name: String, - val imageUrl: String, + override val imageUrl: String, val subtitle: TextReference, ) : ProviderListItemUM } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt index 28350458da..b4f1435144 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/ProviderListUM.kt @@ -4,13 +4,14 @@ import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.collections.immutable.ImmutableList internal data class SelectPaymentAndProviderUM( - val paymentMethods: ImmutableList, - val selectedPaymentMethod: SelectProviderUM, + val paymentMethods: ImmutableList, + val selectedPaymentMethod: PaymentProviderUM, + val selectedProviderId: String, val isPaymentMethodClickEnabled: Boolean, val onPaymentMethodClick: () -> Unit, ) -internal data class SelectProviderUM( +internal data class PaymentProviderUM( val paymentMethod: OnrampPaymentMethod, val providers: ImmutableList, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt index 434138e8ab..3f1364105d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/entity/SelectProviderResult.kt @@ -20,6 +20,6 @@ sealed class SelectProviderResult { data class ProviderWithError( override val paymentMethod: OnrampPaymentMethod, override val provider: OnrampProvider, - val quoteError: OnrampQuote.Error, + val quoteError: OnrampQuote.AmountError, ) : SelectProviderResult() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt index e4b6bdb60b..e85759d361 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/SelectProviderModel.kt @@ -72,9 +72,21 @@ internal class SelectProviderModel @Inject constructor( }, ifRight = { it }, ) + + val filteredEmptyMethods = methods.mapNotNull { method -> + val providers = getOnrampProviderWithQuoteUseCase(method).getOrNull() + if (!providers.isNullOrEmpty()) { + PaymentProviderUM( + paymentMethod = method, + providers = providers.toProvidersListItems(), + ) + } else { + null + } + } _state.value = state.value.copy( - paymentMethods = methods.toPersistentList(), - isPaymentMethodClickEnabled = methods.isNotEmpty(), + paymentMethods = filteredEmptyMethods.toPersistentList(), + isPaymentMethodClickEnabled = filteredEmptyMethods.isNotEmpty(), ) } } @@ -111,13 +123,19 @@ internal class SelectProviderModel @Inject constructor( private fun getInitialState(): SelectPaymentAndProviderUM { return SelectPaymentAndProviderUM( - paymentMethods = persistentListOf(params.selectedPaymentMethod), + paymentMethods = persistentListOf( + PaymentProviderUM( + paymentMethod = params.selectedPaymentMethod, + providers = emptyList().toImmutableList(), + ), + ), isPaymentMethodClickEnabled = false, onPaymentMethodClick = ::openPaymentMethods, - selectedPaymentMethod = SelectProviderUM( + selectedPaymentMethod = PaymentProviderUM( paymentMethod = params.selectedPaymentMethod, providers = emptyList().toImmutableList(), ), + selectedProviderId = params.selectedProviderId, ) } @@ -131,16 +149,18 @@ internal class SelectProviderModel @Inject constructor( ) } - private fun ImmutableList.toPaymentUMList(): ImmutableList = map { method -> + private fun ImmutableList.toPaymentUMList(): ImmutableList = map { method -> PaymentMethodUM( - id = method.id, - imageUrl = method.imageUrl, - name = method.name, + id = method.paymentMethod.id, + imageUrl = method.paymentMethod.imageUrl, + name = method.paymentMethod.name, onSelect = { onPaymentMethodSelected(method) }, ) }.toPersistentList() - private fun onPaymentMethodSelected(paymentMethod: OnrampPaymentMethod) { + private fun onPaymentMethodSelected(methodContainer: PaymentProviderUM) { + val paymentMethod = methodContainer.paymentMethod + val firstProvider = methodContainer.providers.firstOrNull() analyticsEventHandler.send(OnrampAnalyticsEvent.OnPaymentMethodChosen(paymentMethod = paymentMethod.name)) modelScope.launch { saveSelectedPaymentMethod.invoke(paymentMethod) @@ -151,6 +171,10 @@ internal class SelectProviderModel @Inject constructor( ), ) } + if (firstProvider is ProviderListItemUM.Available) { + onProviderSelected(firstProvider.providerResult, firstProvider.isBestRate) + } + bottomSheetNavigation.dismiss() } } @@ -165,6 +189,8 @@ internal class SelectProviderModel @Inject constructor( val hasBestProvider = isMultipleQuotes && isOtherQuotesHasData return sorted.map { quote -> + val isSelectedProvider = state.value.selectedProviderId == quote.provider.id + when (quote) { is OnrampProviderWithQuote.Data -> { val rate = quote.toAmount.value.format { @@ -173,28 +199,29 @@ internal class SelectProviderModel @Inject constructor( val rateDiff = bestProvider?.toAmount?.value?.let { bestRate -> BigDecimal.ONE - quote.toAmount.value / bestRate } - val isSelectedProvider = quote.provider.id == params.selectedProviderId - val isSelectedPayment = quote.paymentMethod.id == params.selectedPaymentMethod.id val isBestProvider = quote == bestProvider && hasBestProvider - ProviderListItemUM.Available( + val providerResult = SelectProviderResult.ProviderWithQuote( + paymentMethod = quote.paymentMethod, + provider = quote.provider, + fromAmount = quote.fromAmount, + toAmount = quote.toAmount, + ) + ProviderListItemUM.Available.Content( providerId = quote.provider.id, imageUrl = quote.provider.info.imageLarge, name = quote.provider.info.name, rate = rate, - isSelected = isSelectedProvider && isSelectedPayment, isBestRate = isBestProvider, diffRate = stringReference("$MINUS${rateDiff.format { percent() }}") .takeIf { hasBestProvider }, + providerResult = providerResult, + isSelected = isSelectedProvider, onClick = { onProviderSelected( - result = SelectProviderResult.ProviderWithQuote( - paymentMethod = quote.paymentMethod, - provider = quote.provider, - fromAmount = quote.fromAmount, - toAmount = quote.toAmount, - ), + result = providerResult, isBestRate = isBestProvider, ) + params.onDismiss() }, ) } @@ -207,21 +234,25 @@ internal class SelectProviderModel @Inject constructor( is OnrampError.AmountError.TooBigError -> R.string.express_provider_max_amount is OnrampError.AmountError.TooSmallError -> R.string.express_provider_min_amount } - - ProviderListItemUM.AvailableWithError( + val providerResult = SelectProviderResult.ProviderWithError( + paymentMethod = quoteError.paymentMethod, + provider = quote.provider, + quoteError = quote.quoteError, + ) + ProviderListItemUM.Available.WithError( providerId = quote.provider.id, imageUrl = quote.provider.info.imageLarge, name = quote.provider.info.name, subtitle = resourceReference(errorSubtitleRes, wrappedList(amount)), + providerResult = providerResult, + isBestRate = false, + isSelected = isSelectedProvider, onClick = { onProviderSelected( - result = SelectProviderResult.ProviderWithError( - paymentMethod = quoteError.paymentMethod, - provider = quote.provider, - quoteError = quote.quoteError, - ), + result = providerResult, isBestRate = bestProvider == quote, ) + params.onDismiss() }, ) } @@ -247,8 +278,10 @@ internal class SelectProviderModel @Inject constructor( tokenSymbol = params.cryptoCurrency.symbol, ), ) + _state.update { + it.copy(selectedProviderId = result.provider.id) + } params.onProviderClick(result, isBestRate) - params.onDismiss() } private fun sendOnrampErrorEvent(error: OnrampError) { @@ -259,6 +292,7 @@ internal class SelectProviderModel @Inject constructor( error = error, tokenSymbol = params.cryptoCurrency.symbol, providerName = selectedProvider?.name, + paymentMethod = state.value.selectedPaymentMethod.paymentMethod.name, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/previewData/SelectProviderPreviewData.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/previewData/SelectProviderPreviewData.kt index 4110250555..f4a23e8734 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/previewData/SelectProviderPreviewData.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/model/previewData/SelectProviderPreviewData.kt @@ -2,59 +2,83 @@ package com.tangem.features.onramp.providers.model.previewData import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.onramp.model.OnrampPaymentMethod -import com.tangem.domain.onramp.model.PaymentMethodType +import com.tangem.domain.onramp.model.* +import com.tangem.features.onramp.providers.entity.PaymentProviderUM import com.tangem.features.onramp.providers.entity.ProviderListItemUM import com.tangem.features.onramp.providers.entity.SelectPaymentAndProviderUM -import com.tangem.features.onramp.providers.entity.SelectProviderUM +import com.tangem.features.onramp.providers.entity.SelectProviderResult import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal internal object SelectProviderPreviewData { - private val paymentMethods = persistentListOf( - OnrampPaymentMethod( - id = "card", - name = "Card", - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", - type = PaymentMethodType.CARD, - ), + private val paymentMethod = OnrampPaymentMethod( + id = "card", + name = "Card", + imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/PaymentMethods/visa-mc.png", + type = PaymentMethodType.CARD, ) - private val providerListUM = SelectProviderUM( - paymentMethod = paymentMethods.first(), + private val provider = OnrampProvider( + id = "mercuryo", + info = OnrampProviderInfo( + name = "Mercuryo", + imageLarge = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png", + termsOfUseLink = null, + privacyPolicyLink = null, + ), + paymentMethods = listOf(), + ) + + private val providerListUM = PaymentProviderUM( + paymentMethod = paymentMethod, providers = persistentListOf( - ProviderListItemUM.Available( - providerId = "mercuryo", - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png", - name = "Mercuryo", + ProviderListItemUM.Available.Content( + providerId = provider.id, + imageUrl = provider.info.imageLarge, + name = provider.info.name, rate = "1 BTC", isSelected = true, isBestRate = true, diffRate = TextReference.EMPTY, + providerResult = SelectProviderResult.ProviderWithQuote( + paymentMethod = paymentMethod, + provider = provider, + fromAmount = OnrampAmount(symbol = "$", value = BigDecimal.ONE, decimals = 2), + toAmount = OnrampAmount(symbol = "BTC", value = BigDecimal.ONE, decimals = 18), + + ), onClick = {}, ), - ProviderListItemUM.Available( - providerId = "mercuryo", - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png", - name = "Mercuryo", + ProviderListItemUM.Available.Content( + providerId = provider.id, + imageUrl = provider.info.imageLarge, + name = provider.info.name, rate = "1 BTC", isSelected = false, isBestRate = false, diffRate = stringReference("-0.03%"), + providerResult = SelectProviderResult.ProviderWithQuote( + paymentMethod = paymentMethod, + provider = provider, + fromAmount = OnrampAmount(symbol = "$", value = BigDecimal.ONE, decimals = 2), + toAmount = OnrampAmount(symbol = "BTC", value = BigDecimal.ONE, decimals = 18), + ), onClick = {}, ), ProviderListItemUM.Unavailable( - providerId = "mercuryo", - imageUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/express/mercuryo1024.png", - name = "Mercuryo", + providerId = provider.id, + imageUrl = provider.info.imageLarge, + name = provider.info.name, subtitle = stringReference("Avaiable from 0,00413 BTC"), ), ), ) val state = SelectPaymentAndProviderUM( - paymentMethods = paymentMethods, + paymentMethods = persistentListOf(providerListUM), selectedPaymentMethod = providerListUM, + selectedProviderId = provider.id, isPaymentMethodClickEnabled = false, onPaymentMethodClick = {}, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt index 29b255f412..39cc85083b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/providers/ui/SelectProviderBottomSheet.kt @@ -124,16 +124,17 @@ private fun PaymentMethodBlock( @Composable private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifier) { when (state) { - is ProviderListItemUM.Available -> AvailableProviderItem( + is ProviderListItemUM.Available.Content -> AvailableProviderItem( modifier = modifier, state = state, ) - is ProviderListItemUM.AvailableWithError -> { + is ProviderListItemUM.Available.WithError -> { UnavailableProviderItem( modifier = modifier.clickable(onClick = state.onClick), imageUrl = state.imageUrl, providerName = state.name, subtitle = state.subtitle, + isSelected = state.isSelected, ) } is ProviderListItemUM.Unavailable -> UnavailableProviderItem( @@ -141,12 +142,13 @@ private fun ProviderItem(state: ProviderListItemUM, modifier: Modifier = Modifie imageUrl = state.imageUrl, providerName = state.name, subtitle = state.subtitle, + isSelected = false, ) } } @Composable -private fun AvailableProviderItem(state: ProviderListItemUM.Available, modifier: Modifier = Modifier) { +private fun AvailableProviderItem(state: ProviderListItemUM.Available.Content, modifier: Modifier = Modifier) { Row( modifier = modifier .then( @@ -220,10 +222,19 @@ private fun UnavailableProviderItem( imageUrl: String, providerName: String, subtitle: TextReference, + isSelected: Boolean, modifier: Modifier = Modifier, ) { Row( - modifier = modifier.padding(all = TangemTheme.dimens.spacing12), + modifier = modifier + .then( + if (isSelected) { + Modifier.selectedBorder() + } else { + Modifier.clip(RoundedCornerShape(16.dp)) + }, + ) + .padding(all = TangemTheme.dimens.spacing12), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt index 71cedbb3a5..17904c58e6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/DefaultOnrampRedirectComponent.kt @@ -1,7 +1,9 @@ package com.tangem.features.onramp.redirect import androidx.activity.compose.BackHandler +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel @@ -22,6 +24,11 @@ internal class DefaultOnrampRedirectComponent @AssistedInject constructor( override fun Content(modifier: Modifier) { BackHandler(onBack = params.onBack) OnrampRedirectContent(modifier = modifier, state = model.state) + + val isDarkTheme = isSystemInDarkTheme() + LaunchedEffect(model.state) { + model.getRedirectUrl(isDarkTheme = isDarkTheme) + } } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index 2a78d7fded..df74ea2d77 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -58,16 +58,13 @@ internal class OnrampRedirectModel @Inject constructor( ), ) - init { - getRedirectUrl() - } - - private fun getRedirectUrl() { + fun getRedirectUrl(isDarkTheme: Boolean) { modelScope.launch { getOnrampRedirectUrlUseCase.invoke( userWalletId = params.userWalletId, quote = params.onrampProviderWithQuote, cryptoCurrency = params.cryptoCurrency, + isDarkTheme = isDarkTheme, ) .onLeft(::handleError) .onRight { @@ -83,6 +80,7 @@ internal class OnrampRedirectModel @Inject constructor( error = error, tokenSymbol = params.cryptoCurrency.symbol, providerName = params.onrampProviderWithQuote.provider.info.name, + paymentMethod = params.onrampProviderWithQuote.paymentMethod.name, ) val contentMessage = ContentMessage { onDismiss -> BasicDialog( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt index 24c8fbdcb3..224e1c4168 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/ui/SelectCountryBottomSheet.kt @@ -185,7 +185,7 @@ private fun UnavailableCountryItem(state: CountryItemState.WithContent.Unavailab color = TangemTheme.colors.text.disabled, ) Text( - text = "Unavailable", + text = stringResourceSafe(R.string.onramp_country_unavailable), maxLines = 1, style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt index e5e3328194..00e776e170 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsTransformer.kt @@ -1,10 +1,11 @@ package com.tangem.features.onramp.selectcurrency.entity.transformer import arrow.core.Either -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.onramp.model.OnrampCurrencies import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState @@ -28,7 +29,7 @@ internal class UpdateCurrencyItemsTransformer( if (populars.isNotEmpty()) { add( CurrenciesSection( - title = stringReference("Popular fiats"), + title = resourceReference(R.string.onramp_currency_popular), items = populars.map(::convertCurrencyToUiModel).toImmutableList(), ), ) @@ -36,7 +37,7 @@ internal class UpdateCurrencyItemsTransformer( if (others.isNotEmpty()) { add( CurrenciesSection( - title = stringReference("Other currencies"), + title = resourceReference(R.string.onramp_currency_other), items = others.map(::convertCurrencyToUiModel).toImmutableList(), ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt index 127d38cb94..a297ae455a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -6,7 +6,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.onramp.FetchOnrampCurrenciesUseCase import com.tangem.domain.onramp.GetOnrampCurrenciesUseCase import com.tangem.domain.onramp.OnrampSaveDefaultCurrencyUseCase @@ -136,8 +135,11 @@ internal class OnrampSelectCurrencyModel @Inject constructor( private companion object { val loadingSections = listOf( - CurrenciesSection(stringReference("Popular fiats"), items = createLoadingItems("popular")), - CurrenciesSection(stringReference("Other currencies"), items = createLoadingItems("other")), + CurrenciesSection( + resourceReference(R.string.onramp_currency_popular), + items = createLoadingItems("popular"), + ), + CurrenciesSection(resourceReference(R.string.onramp_currency_other), items = createLoadingItems("other")), ).toImmutableList() private fun createLoadingItems(prefix: String, size: Int = 5): ImmutableList = diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt index 71f80e2f05..cb5a0cd776 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/OnrampSuccessComponentUM.kt @@ -11,7 +11,7 @@ sealed class OnrampSuccessComponentUM { data class Content( val txId: String, val timestamp: Long, - val currencyImageUrl: String, + val currencyImageUrl: String?, val fromAmount: TextReference, val toAmount: TextReference, val statusBlock: ExpressStatusUM, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 348cbe0154..514a00dc19 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -1,19 +1,28 @@ package com.tangem.features.onramp.success.model +import androidx.compose.ui.res.stringResource import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +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.components.BasicDialog +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.ContentMessage import com.tangem.domain.onramp.GetOnrampStatusUseCase import com.tangem.domain.onramp.GetOnrampTransactionUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.onramp.component.OnrampSuccessComponent +import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.success.entity.OnrampSuccessClickIntents import com.tangem.features.onramp.success.entity.OnrampSuccessComponentUM import com.tangem.features.onramp.success.entity.conterter.SetOnrampSuccessContentConverter @@ -36,6 +45,8 @@ internal class OnrampSuccessComponentModel @Inject constructor( private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val messageSender: UiMessageSender, + private val router: Router, paramsContainer: ParamsContainer, ) : Model(), OnrampSuccessClickIntents { @@ -58,8 +69,9 @@ internal class OnrampSuccessComponentModel @Inject constructor( modelScope.launch { getOnrampTransactionUseCase(externalTxId = params.externalTxId) .fold( - ifLeft = { - Timber.e(it.toString()) + ifLeft = { error -> + Timber.e(error.toString()) + showErrorAlert(error) }, ifRight = { transaction -> loadTransactionStatus(transaction) @@ -74,15 +86,17 @@ internal class OnrampSuccessComponentModel @Inject constructor( transaction.toCurrencyId, ).getOrElse { error("Crypto currency not found") } - getOnrampStatusUseCase(externalTxId = params.externalTxId) + getOnrampStatusUseCase(txId = transaction.txId) .fold( ifLeft = { error -> analyticsEventHandler.sendOnrampErrorEvent( error = error, tokenSymbol = cryptoCurrency.symbol, providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, ) Timber.e(error.toString()) + showErrorAlert(error) }, ifRight = { status -> analyticsEventHandler.send( @@ -101,14 +115,46 @@ internal class OnrampSuccessComponentModel @Inject constructor( goToProviderClick = ::goToProviderClick, ).convert(status) } - removeTransactionIfTerminalStatus(cryptoCurrency, transaction.providerName, status) + removeTransactionIfTerminalStatus( + cryptoCurrency = cryptoCurrency, + providerName = transaction.providerName, + paymentMethod = transaction.paymentMethod, + status = status, + ) }, ) } + private fun showErrorAlert(error: OnrampError) { + val contentMessage = ContentMessage { onDismiss -> + val errorCode = (error as? OnrampError.DataError)?.code + val message = if (errorCode.isNullOrBlank()) { + stringResource(R.string.common_unknown_error) + } else { + stringResource(R.string.express_error_code, wrappedList(errorCode)) + } + BasicDialog( + message = message, + confirmButton = DialogButtonUM( + title = stringResource(id = R.string.common_ok), + onClick = { + router.pop() + onDismiss() + }, + ), + onDismissDialog = { + router.pop() + onDismiss() + }, + ) + } + messageSender.send(contentMessage) + } + private fun removeTransactionIfTerminalStatus( cryptoCurrency: CryptoCurrency, providerName: String, + paymentMethod: String, status: OnrampStatus, ) { modelScope.launch { @@ -118,6 +164,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( error = error, tokenSymbol = cryptoCurrency.symbol, providerName = providerName, + paymentMethod = paymentMethod, ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt index 5ca1b735b6..e9ec5c76c5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampErrorAnalyticsSender.kt @@ -8,11 +8,13 @@ internal fun AnalyticsEventHandler.sendOnrampErrorEvent( error: OnrampError, tokenSymbol: String, providerName: String? = null, + paymentMethod: String? = null, ) = when (error) { is OnrampError.DataError -> send( OnrampAnalyticsEvent.Errors( tokenSymbol = tokenSymbol, providerName = providerName, + paymentMethod = paymentMethod, errorCode = error.code, ), ) diff --git a/features/referral/presentation/build.gradle.kts b/features/referral/presentation/build.gradle.kts index d799b6e432..63f2a5e291 100644 --- a/features/referral/presentation/build.gradle.kts +++ b/features/referral/presentation/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(deps.androidx.appCompat) implementation(deps.androidx.fragment.ktx) implementation(deps.lifecycle.viewModel.ktx) + implementation(deps.androidx.activity.compose) /** Compose */ implementation(deps.compose.foundation) diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index 5bf12360cb..f5941be08d 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -2,6 +2,7 @@ package com.tangem.feature.referral.ui import android.content.res.Configuration import android.content.res.Resources +import androidx.activity.compose.BackHandler import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -51,6 +52,8 @@ import kotlinx.coroutines.launch internal fun ReferralScreen(stateHolder: ReferralStateHolder) { val snackbarHostState = remember(::SnackbarHostState) + BackHandler(onBack = stateHolder.headerState.onBackClicked) + Scaffold( topBar = { AppBarWithBackButton( diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index fdb05e838d..ec9e878040 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -380,10 +380,9 @@ internal class SendViewModel @Inject constructor( when { uiState.value.sendState?.isSuccess == true -> return transactionId != null && amount != null && destinationAddress != null -> { - loadFee() uiState.value = stateFactory.getReadyState(amount, destinationAddress, memo) stateRouter.showSend() - updateNotifications() + loadFee() } else -> { uiState.value = stateFactory.getReadyState() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt index 565d27a3b8..af04294b72 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/router/CustomTabsManager.kt @@ -8,6 +8,7 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import java.lang.ref.WeakReference +@Deprecated("Replace with CustomTabsUrlOpener") class CustomTabsManager(private val context: WeakReference) { fun openUrl(url: String) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index baf2ff720e..353348dd44 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -184,6 +184,14 @@ internal class SwapViewModel @Inject constructor( }.launchIn(viewModelScope) } + override fun onStart(owner: LifecycleOwner) { + startLoadingQuotesFromLastState(true) + } + + override fun onStop(owner: LifecycleOwner) { + singleTaskScheduler.cancelTask() + } + override fun onCleared() { singleTaskScheduler.cancelTask() super.onCleared() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index 52eae5de76..d8a3eb4733 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -50,6 +50,13 @@ internal class DefaultTokenDetailsRouter( } override fun openOnrampSuccess(externalTxId: String) { - router.push(AppRoute.OnrampSuccess(externalTxId)) + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = router.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + + replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + + router.replaceAll(*replaceOnrampScreens.toTypedArray()) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index 625959ab86..672da4f66c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -152,6 +152,7 @@ internal object TokenDetailsPreviewData { formatArgs = wrappedList("Solana"), ), iconState = iconState, + isEnabled = true, onStakeClicked = {}, ) @@ -308,6 +309,7 @@ internal object TokenDetailsPreviewData { dialogConfig = null, pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, @@ -338,6 +340,7 @@ internal object TokenDetailsPreviewData { dialogConfig = null, pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), pullToRefreshConfig = pullToRefreshConfig, bottomSheetConfig = null, isBalanceHidden = false, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsAnalyticsEvent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsAnalyticsEvent.kt index 8c5f6c268f..5362f40522 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsAnalyticsEvent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsAnalyticsEvent.kt @@ -23,5 +23,10 @@ internal open class TokenDetailsAnalyticsEvent( event = "Not Enough Fee", params = currency.toAnalyticsParams(), ) + + class Reveal(currency: CryptoCurrency) : Notice( + event = "Reveal Transaction", + params = currency.toAnalyticsParams(), + ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt index 836894e575..8e3ed14b1c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/TokenDetailsNotificationsAnalyticsSender.kt @@ -41,6 +41,9 @@ internal class TokenDetailsNotificationsAnalyticsSender( programName = TokenSwapPromoAnalyticsEvent.ProgramName.OKX, source = AnalyticsParam.ScreensSources.Token, ) + is TokenDetailsNotification.KaspaIncompleteTransactionWarning -> TokenDetailsAnalyticsEvent.Notice.Reveal( + currency = cryptoCurrency, + ) is TokenDetailsNotification.NetworksUnreachable, is TokenDetailsNotification.ExistentialDeposit, is TokenDetailsNotification.NetworksNoAccount, @@ -48,9 +51,9 @@ internal class TokenDetailsNotificationsAnalyticsSender( is TokenDetailsNotification.RentInfo, is TokenDetailsNotification.NetworkShutdown, is TokenDetailsNotification.HederaAssociateWarning, - is TokenDetailsNotification.KaspaIncompleteTransactionWarning, is TokenDetailsNotification.KoinosMana, is TokenDetailsNotification.MigrationMaticToPol, + is TokenDetailsNotification.TokensInBeta, -> null } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/utils/CryptoCurrencyMapper.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/utils/CryptoCurrencyMapper.kt index d63835def0..c01c196347 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/utils/CryptoCurrencyMapper.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/analytics/utils/CryptoCurrencyMapper.kt @@ -5,6 +5,6 @@ import com.tangem.domain.tokens.model.CryptoCurrency internal fun CryptoCurrency.toAnalyticsParams(): Map { return mapOf( "Token" to symbol, - "Blockchain" to network.currencySymbol, + "Blockchain" to network.name, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt index 0072e90707..789ddc5ec5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/StakingBlockUM.kt @@ -24,6 +24,7 @@ internal sealed interface StakingBlockUM { val iconState: IconState, val titleText: TextReference, val subtitleText: TextReference, + val isEnabled: Boolean, val onStakeClicked: () -> Unit, ) : StakingBlockUM } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 8ece0d02e9..06fda7a3b6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -9,7 +9,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.pullToRefresh.PullToRefreshConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList @@ -21,6 +21,7 @@ internal data class TokenDetailsState( val stakingBlocksState: StakingBlockUM?, val notifications: ImmutableList, val pendingTxs: PersistentList, + val expressTxsToDisplay: PersistentList, val expressTxs: PersistentList, val txHistoryState: TxHistoryState, val dialogConfig: TokenDetailsDialogConfig?, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 86db0a1bf3..abc78abbec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -229,4 +229,9 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { title = resourceReference(id = R.string.warning_matic_migration_title), subtitle = resourceReference(id = R.string.warning_matic_migration_message), ) + + data object TokensInBeta : Warning( + title = resourceReference(id = R.string.beta_mode_warning_title), + subtitle = resourceReference(id = R.string.beta_mode_warning_message), + ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt new file mode 100644 index 0000000000..4420f4be4a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/express/ExchangeUM.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.express + +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +import com.tangem.feature.swap.domain.models.domain.SwapProvider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications +import kotlinx.collections.immutable.ImmutableList + +internal data class ExchangeUM( + override val info: ExpressTransactionStateInfoUM, + val provider: SwapProvider, + val activeStatus: ExchangeStatus?, + val statuses: ImmutableList, + val notification: ExchangeStatusNotifications? = null, + val showProviderLink: Boolean, + val isRefundTerminalStatus: Boolean, + val fromCryptoCurrency: CryptoCurrency, + val toCryptoCurrency: CryptoCurrency, +) : ExpressTransactionStateUM \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index a8bf99a969..2a9be57347 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -158,7 +158,7 @@ internal class TokenDetailsLoadedBalanceConverter( return when { stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> { if (pendingBalances.isEmpty()) { - getStakeAvailableState(stakingEntryInfo, iconState) + getStakeAvailableState(stakingEntryInfo, iconState, isStakingButtonEnabled(status)) } else { getStakedBlockWithFiatAmount(status, pendingBalances.sumOf { it.amount }, null) } @@ -170,6 +170,12 @@ internal class TokenDetailsLoadedBalanceConverter( } } + private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean { + return status.value is CryptoCurrencyStatus.Loaded || + status.value is CryptoCurrencyStatus.NoQuote || + status.value is CryptoCurrencyStatus.Custom + } + private fun getStakedBlockWithFiatAmount( status: CryptoCurrencyStatus, stakingAmount: BigDecimal?, @@ -210,6 +216,7 @@ internal class TokenDetailsLoadedBalanceConverter( private fun getStakeAvailableState( stakingEntryInfo: StakingEntryInfo, iconState: IconState, + isEnabled: Boolean, ): StakingBlockUM.StakeAvailable { val apr = stakingEntryInfo.apr.format { percent() } return StakingBlockUM.StakeAvailable( @@ -222,6 +229,7 @@ internal class TokenDetailsLoadedBalanceConverter( formatArgs = wrappedList(stakingEntryInfo.tokenSymbol), ), iconState = iconState, + isEnabled = isEnabled, onStakeClicked = clickIntents::onStakeBannerClick, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index b1435ae301..1c0e9869ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -135,6 +135,7 @@ internal class TokenDetailsNotificationConverter( }, ) is CryptoCurrencyWarning.MigrationMaticToPol -> MigrationMaticToPol + is CryptoCurrencyWarning.TokensInBetaWarning -> TokensInBeta } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index dd8b9bc4e9..9fc0a051b5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -23,9 +23,9 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 07f98c6f10..f22431609c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -67,6 +67,7 @@ internal class TokenDetailsSkeletonStateConverter( notifications = persistentListOf(), pendingTxs = persistentListOf(), expressTxs = persistentListOf(), + expressTxsToDisplay = persistentListOf(), txHistoryState = TxHistoryState.Content( contentItems = MutableStateFlow( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index d4738e748b..18fe759bac 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -23,9 +23,9 @@ import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListMode import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeStatusState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateIconUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateInfoUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -45,20 +45,20 @@ internal class TokenDetailsSwapTransactionsStateConverter( private val cryptoCurrency: CryptoCurrency, private val analyticsEventsHandler: AnalyticsEventHandler, appCurrencyProvider: Provider, -) : Converter> { +) : Converter> { private val iconStateConverter = CryptoCurrencyToIconStateConverter() private val appCurrency = appCurrencyProvider() - override fun convert(value: Unit): PersistentList { + override fun convert(value: Unit): PersistentList { return persistentListOf() } fun convert( savedTransactions: List, quotes: Set, - ): PersistentList { - val result = mutableListOf() + ): PersistentList { + val result = mutableListOf() savedTransactions .forEach { swapTransaction -> @@ -84,7 +84,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( getNotification(transaction.status?.status, transaction.status?.txExternalUrl, null) val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( - ExpressTransactionStateUM.ExchangeUM( + ExchangeUM( provider = transaction.provider, statuses = getStatuses(transaction.status?.status), notification = notifications, @@ -108,11 +108,11 @@ internal class TokenDetailsSwapTransactionsStateConverter( } fun updateTxStatus( - tx: ExpressTransactionStateUM.ExchangeUM, + tx: ExchangeUM, statusModel: ExchangeStatusModel?, refundToken: CryptoCurrency?, isRefundTerminalStatus: Boolean, - ): ExpressTransactionStateUM.ExchangeUM { + ): ExchangeUM { if (statusModel == null || tx.activeStatus == statusModel.status) { Timber.e("UpdateTxStatus isn't required. Current status isn't changed") return tx diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 545ff398df..25d87bd106 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore @@ -15,9 +16,8 @@ import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import dagger.assisted.Assisted @@ -55,7 +55,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( ) } - suspend operator fun invoke(): Flow> { + suspend operator fun invoke(): Flow> { val selectedWallet = getSelectedWalletSyncUseCase().fold( ifLeft = { return emptyFlow() }, ifRight = { it }, @@ -82,7 +82,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( suspend fun removeTransactionOnBottomSheetClosed(isForceTerminal: Boolean = false) { val state = currentStateProvider() val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return - val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.ExchangeUM ?: return + val selectedTx = bottomSheetConfig.value as? ExchangeUM ?: return val shouldTerminate = selectedTx.activeStatus.isTerminal(selectedTx.isRefundTerminalStatus) || isForceTerminal if (shouldTerminate) { @@ -95,7 +95,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - suspend fun updateSwapTxStatus(swapTx: ExpressTransactionStateUM.ExchangeUM): ExpressTransactionStateUM.ExchangeUM { + suspend fun updateSwapTxStatus(swapTx: ExchangeUM): ExchangeUM { return if (swapTx.activeStatus.isTerminal(swapTx.isRefundTerminalStatus)) { swapTx } else { @@ -163,7 +163,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private fun getExchangeStatusState( savedTransactions: List?, quotes: Set, - ): PersistentList { + ): PersistentList { if (savedTransactions == null) { return persistentListOf() } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 92a6a2508c..7cfbd6eb7b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.domain.appcurrency.model.AppCurrency @@ -10,8 +12,7 @@ import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -79,11 +80,13 @@ internal class ExpressStatusFactory @AssistedInject constructor( expressTxs.map { tx -> async { when (tx) { - is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx) + is ExchangeUM -> exchangeStatusFactory.updateSwapTxStatus(tx) is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.updateOnrmapTxStatus(tx) + else -> null } } }.awaitAll() + .filterNotNull() .toPersistentList() } @@ -95,11 +98,18 @@ internal class ExpressStatusFactory @AssistedInject constructor( val config = state.bottomSheetConfig val expressBottomSheet = config?.content as? ExpressStatusBottomSheetConfig val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } - if (currentTx is ExpressTransactionStateUM.ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { + if (currentTx is ExchangeUM && currentTx.activeStatus == ExchangeStatus.Finished) { updateBalance(currentTx.toCryptoCurrency) } + val expressTxsToDisplay = expressTxs.filterNot { + when (it) { + is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden + else -> false + } + }.toPersistentList() return state.copy( expressTxs = expressTxs, + expressTxsToDisplay = expressTxsToDisplay, bottomSheetConfig = currentTx?.let( ::updateStateWithExpressStatusBottomSheet, ) ?: config, @@ -108,7 +118,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( fun getStateWithExpressStatusBottomSheet(expressState: ExpressTransactionStateUM): TokenDetailsState { val analyticEvent = when (expressState) { - is ExpressTransactionStateUM.ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened( + is ExchangeUM -> TokenExchangeAnalyticsEvent.CexTxStatusOpened( cryptoCurrency.symbol, ) is ExpressTransactionStateUM.OnrampUM -> TokenOnrampAnalyticsEvent.OnrampStatusOpened( @@ -116,6 +126,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( provider = expressState.providerName, fiatCurrency = expressState.fromCurrencyCode, ) + else -> return currentStateProvider() } analyticsEventsHandler.send(analyticEvent) @@ -149,9 +160,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( isForceTerminal: Boolean = false, ) { when (expressState) { - is ExpressTransactionStateUM.ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed( - isForceTerminal, - ) + is ExchangeUM -> exchangeStatusFactory.removeTransactionOnBottomSheetClosed(isForceTerminal) is ExpressTransactionStateUM.OnrampUM -> onrampStatusFactory.removeTransactionOnBottomSheetClosed() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index eca24c7c13..7ca14864da 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus import com.tangem.domain.appcurrency.model.AppCurrency @@ -14,9 +16,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.utils.Provider import dagger.assisted.Assisted @@ -61,7 +61,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( ifRight = { onrampTxs -> val transactions = onrampTransactionStateConverter.convertList(onrampTxs) transactions.clearHiddenTerminal() - transactions.filterNot { it.activeStatus.isHidden } + transactions }, ifLeft = { persistentListOf() }, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a22b97d642..87ec82270c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -22,6 +22,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.paging.compose.collectAsLazyPagingItems +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheet import com.tangem.core.ui.components.bottomsheets.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBottomSheet @@ -49,8 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.expressTransactionsItems import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock import com.tangem.features.markets.token.block.TokenMarketBlockComponent @@ -178,7 +178,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState, tokenMarketBlockCompon } expressTransactionsItems( - expressTxs = state.expressTxs, + expressTxs = state.expressTxsToDisplay, modifier = itemModifier, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt index c37f0c020a..8eec7243af 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheet.kt @@ -1,17 +1,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express import androidx.compose.runtime.Composable +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.OnrampStatusBottomSheetContent +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.exchange.ExchangeStatusBottomSheetContent -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.onramp.OnrampStatusBottomSheetContent - -internal data class ExpressStatusBottomSheetConfig( - val value: ExpressTransactionStateUM, -) : TangemBottomSheetConfigContent @Composable internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) { @@ -21,7 +18,7 @@ internal fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) { ) { content: ExpressStatusBottomSheetConfig -> when (val state = content.value) { is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state) - is ExpressTransactionStateUM.ExchangeUM -> ExchangeStatusBottomSheetContent(state) + is ExchangeUM -> ExchangeStatusBottomSheetContent(state) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt index 2f2420d3a9..3c969fa8d2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/exchange/ExchangeStatusBottomSheetContent.kt @@ -8,6 +8,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import com.tangem.common.ui.expressStatus.ExpressEstimate +import com.tangem.common.ui.expressStatus.ExpressProvider import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -19,12 +21,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressEstimate -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressProvider +import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM @Composable -internal fun ExchangeStatusBottomSheetContent(state: ExpressTransactionStateUM.ExchangeUM) { +internal fun ExchangeStatusBottomSheetContent(state: ExchangeUM) { Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { SpacerH10() Text( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt index 5cd7f1b7c2..807664c039 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/staking/TokenStakingBlock.kt @@ -114,6 +114,7 @@ private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifi SecondaryButton( modifier = Modifier.fillMaxWidth(), text = stringResourceSafe(id = R.string.common_stake), + enabled = state.isEnabled, onClick = state.onStakeClicked, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b10537af0c..3280261140 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -41,7 +41,6 @@ import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.NetworkAddress @@ -68,10 +67,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExpressTransactionStateUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.ExpressStatusFactory -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -84,7 +83,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.math.BigDecimal import javax.inject.Inject @Suppress("LongParameterList", "LargeClass", "TooManyFunctions") @@ -99,8 +97,6 @@ internal class TokenDetailsViewModel @Inject constructor( private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, - private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -199,7 +195,6 @@ internal class TokenDetailsViewModel @Inject constructor( viewModel = this, deepLinks = listOf( BuyCurrencyDeepLink( - isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled, onReceive = ::onBuyCurrencyDeepLink, ), ), @@ -337,6 +332,7 @@ internal class TokenDetailsViewModel @Inject constructor( expressTxStatusTaskScheduler.scheduleTask( viewModelScope, PeriodicTask( + isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runCatching { @@ -463,9 +459,8 @@ internal class TokenDetailsViewModel @Inject constructor( return } - showErrorIfDemoModeOrElse { - val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse - + val status = cryptoCurrencyStatus ?: return + if (onrampFeatureToggles.isFeatureEnabled) { viewModelScope.launch(dispatchers.main) { reduxStateHolder.dispatch( TradeCryptoAction.Buy( @@ -476,6 +471,19 @@ internal class TokenDetailsViewModel @Inject constructor( ), ) } + } else { + showErrorIfDemoModeOrElse { + viewModelScope.launch(dispatchers.main) { + reduxStateHolder.dispatch( + TradeCryptoAction.Buy( + userWallet = userWallet, + source = OnrampSource.TOKEN_DETAILS, + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ), + ) + } + } } } @@ -507,68 +515,16 @@ internal class TokenDetailsViewModel @Inject constructor( return } - sendCurrency(status = cryptoCurrencyStatus ?: return) + sendCurrency() } - private fun sendCurrency(status: CryptoCurrencyStatus, transactionInfo: TransactionInfo? = null) { - viewModelScope.launch(dispatchers.main) { - val maybeFeeCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + private fun sendCurrency() { + val route = AppRoute.Send( + currency = cryptoCurrency, + userWalletId = userWallet.walletId, + ) - when (val currency = status.currency) { - is CryptoCurrency.Coin -> { - reduxStateHolder.dispatch( - action = TradeCryptoAction.SendCoin( - userWallet = userWallet, - coinStatus = status, - feeCurrencyStatus = maybeFeeCurrencyStatus, - transactionInfo = transactionInfo, - ), - ) - } - is CryptoCurrency.Token -> { - sendToken( - tokenCurrency = currency, - tokenFiatRate = status.value.fiatRate, - feeCurrencyStatus = maybeFeeCurrencyStatus, - transactionInfo = transactionInfo, - ) - } - } - } - } - - private fun sendToken( - tokenCurrency: CryptoCurrency.Token, - tokenFiatRate: BigDecimal?, - feeCurrencyStatus: CryptoCurrencyStatus?, - transactionInfo: TransactionInfo?, - ) { - viewModelScope.launch(dispatchers.main) { - val maybeCoinStatus = getNetworkCoinStatusUseCase( - userWalletId = userWalletId, - networkId = tokenCurrency.network.id, - derivationPath = tokenCurrency.network.derivationPath, - isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - .conflate() - .distinctUntilChanged() - .firstOrNull() - - reduxStateHolder.dispatchWithMain( - action = TradeCryptoAction.SendToken( - userWallet = userWallet, - tokenCurrency = tokenCurrency, - tokenFiatRate = tokenFiatRate, - coinFiatRate = maybeCoinStatus?.fold( - ifLeft = { null }, - ifRight = { it.value.fiatRate }, - ), - feeCurrencyStatus = feeCurrencyStatus, - transactionInfo = transactionInfo, - ), - ) - } + appRouter.push(route) } override fun onReceiveClick(unavailabilityReason: ScenarioUnavailabilityReason) { @@ -808,7 +764,7 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxs.first { it.info.txId == txId } + val expressTxState = internalUiState.value.expressTxsToDisplay.first { it.info.txId == txId } internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } @@ -873,6 +829,12 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onRetryIncompleteTransactionClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.RevealTryAgain( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) viewModelScope.launch { retryIncompleteTransactionUseCase( userWalletId = userWalletId, @@ -908,6 +870,12 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onDismissIncompleteTransactionClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.RevealCancel( + tokenSymbol = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + ), + ) viewModelScope.launch { internalUiState.value = stateFactory.getStateWithDismissIncompleteTransactionConfirmDialog() } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index c673d76a65..36690c35a6 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -52,19 +52,19 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.analytics) implementation(projects.core.analytics.models) - implementation(projects.common.routing) implementation(projects.core.deepLinks) implementation(projects.core.deepLinks.global) implementation(projects.core.decompose) + implementation(projects.core.datasource) implementation(projects.core.res) implementation(projects.libs.crypto) + implementation(projects.libs.blockchainSdk) /** Domain modules */ implementation(projects.domain.card) implementation(projects.domain.demo) implementation(projects.domain.legacy) - implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) implementation(projects.domain.settings) implementation(projects.domain.tokens) @@ -83,6 +83,7 @@ dependencies { implementation(projects.domain.markets.models) implementation(projects.domain.feedback) implementation(projects.domain.onramp.models) + implementation(projects.domain.onramp) //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] implementation(projects.features.onboarding) @@ -101,6 +102,7 @@ dependencies { /** Common modules */ implementation(projects.common.ui) + implementation(projects.common.routing) /** Test libraries */ implementation(deps.test.junit) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt index c9abd72ef6..d58c9a80ae 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/deeplink/WalletDeepLinksHandler.kt @@ -2,25 +2,22 @@ package com.tangem.feature.wallet.presentation.deeplink import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.deeplink.DeepLink import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.deeplink.global.BuyCurrencyDeepLink import com.tangem.core.deeplink.global.SellCurrencyDeepLink -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.legacy.TradeCryptoAction.TransactionInfo -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents import com.tangem.features.onramp.OnrampFeatureToggles import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -28,13 +25,9 @@ internal class WalletDeepLinksHandler @Inject constructor( private val deepLinksRegistry: DeepLinksRegistry, private val analyticsEventHandler: AnalyticsEventHandler, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, - private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase, - private val getCryptoCurrencyStatusesSyncUseCase: GetCryptoCurrencyStatusesSyncUseCase, - private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, - private val reduxStateHolder: ReduxStateHolder, private val clickIntents: WalletClickIntents, private val onrampFeatureToggles: OnrampFeatureToggles, + private val router: AppRouter, ) { private var deepLinksMap = mutableMapOf>() @@ -65,7 +58,6 @@ internal class WalletDeepLinksHandler @Inject constructor( if (onrampFeatureToggles.isFeatureEnabled || !userWallet.isMultiCurrency) { add( BuyCurrencyDeepLink( - isOnrampFeatureEnabled = onrampFeatureToggles.isFeatureEnabled, onReceive = { externalTxId -> scope.launch { onBuyCurrencyDeepLink(externalTxId, userWallet) } }, @@ -76,35 +68,23 @@ internal class WalletDeepLinksHandler @Inject constructor( } private suspend fun onSellCurrencyDeepLink(userWallet: UserWallet, data: SellCurrencyDeepLink.Data) { - val cryptoCurrencyStatus = findCryptoCurrencyStatus(userWallet, data.currencyId) ?: return - val feeCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWallet.walletId, - cryptoCurrencyStatus, - ).getOrNull() + val cryptoCurrency = getCryptoCurrencyUseCase(userWallet.walletId, data.currencyId).getOrNull() - val transactionInfo = data.let { - TransactionInfo( - amount = it.baseCurrencyAmount, - destinationAddress = it.depositWalletAddress, - transactionId = it.transactionId, - tag = it.depositWalletAddressTag, - ) + if (cryptoCurrency == null) { + Timber.e("onSellCurrencyDeepLink cryptoCurrency is null") + return } - when (cryptoCurrencyStatus.currency) { - is CryptoCurrency.Coin -> sendCoin( - userWallet = userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - transactionInfo = transactionInfo, - ) - is CryptoCurrency.Token -> sendToken( - userWallet = userWallet, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - transactionInfo = transactionInfo, - ) - } + val route = AppRoute.Send( + currency = cryptoCurrency, + userWalletId = userWallet.walletId, + transactionId = data.transactionId, + destinationAddress = data.depositWalletAddress, + amount = data.baseCurrencyAmount, + tag = data.depositWalletAddressTag, + ) + + router.push(route) } private suspend fun onBuyCurrencyDeepLink(externalTxId: String, userWallet: UserWallet) { @@ -115,62 +95,4 @@ internal class WalletDeepLinksHandler @Inject constructor( analyticsEventHandler.send(TokenScreenAnalyticsEvent.Bought(cryptoCurrency.symbol)) } } - - private suspend fun findCryptoCurrencyStatus( - userWallet: UserWallet, - currencyIdValue: String, - ): CryptoCurrencyStatus? { - return if (userWallet.isMultiCurrency) { - getCryptoCurrencyStatusesSyncUseCase(userWallet.walletId).getOrNull()?.let { currencies -> - currencies.find { currencyIdValue == it.currency.id.value } - } - } else { - getCryptoCurrencyStatusSyncUseCase(userWallet.walletId).getOrNull() - } - } - - private fun sendCoin( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus?, - transactionInfo: TransactionInfo, - ) { - reduxStateHolder.dispatch( - action = TradeCryptoAction.SendCoin( - userWallet = userWallet, - coinStatus = cryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - transactionInfo = transactionInfo, - ), - ) - } - - private suspend fun sendToken( - userWallet: UserWallet, - cryptoCurrencyStatus: CryptoCurrencyStatus, - feeCurrencyStatus: CryptoCurrencyStatus?, - transactionInfo: TransactionInfo, - ) { - val cryptoCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return - val coinStatus = getNetworkCoinStatusUseCase( - userWalletId = userWallet.walletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = false, - ) - .firstOrNull() - ?.getOrNull() - ?: return - - reduxStateHolder.dispatch( - action = TradeCryptoAction.SendToken( - userWallet = userWallet, - tokenCurrency = cryptoCurrency, - tokenFiatRate = cryptoCurrencyStatus.value.fiatRate, - coinFiatRate = coinStatus.value.fiatRate, - feeCurrencyStatus = feeCurrencyStatus, - transactionInfo = transactionInfo, - ), - ) - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 4d2df2bd9f..6780b00e47 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -130,7 +130,14 @@ internal class DefaultWalletRouter( } override fun openOnrampSuccessScreen(externalTxId: String) { - router.push(AppRoute.OnrampSuccess(externalTxId)) + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = router.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + + replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + + router.replaceAll(*replaceOnrampScreens.toTypedArray()) } override fun openUrl(url: String) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt new file mode 100644 index 0000000000..8387de10c1 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -0,0 +1,99 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.datasource.local.swaptx.ExpressAnalyticsStatus +import com.tangem.domain.onramp.GetOnrampStatusUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase +import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.OnrampStatus.Status.* +import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.hilt.android.scopes.ViewModelScoped +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import timber.log.Timber +import javax.inject.Inject + +@ViewModelScoped +internal class OnrampStatusFactory @Inject constructor( + private val stateHolder: WalletStateController, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val getOnrampStatusUseCase: GetOnrampStatusUseCase, + private val onrampUpdateTransactionStatusUseCase: OnrampUpdateTransactionStatusUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, + private val dispatchers: CoroutineDispatcherProvider, +) { + + suspend fun removeTransactionOnBottomSheetClosed() { + val state = stateHolder.getSelectedWallet() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig ?: return + val selectedTx = bottomSheetConfig.value as? ExpressTransactionStateUM.OnrampUM ?: return + + if (selectedTx.activeStatus.isTerminal) { + onrampRemoveTransactionUseCase(externalTxId = selectedTx.info.txExternalId) + } + } + + suspend fun updateOnrmapTransactionStatuses() = withContext(dispatchers.io) { + val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content + ?: return@withContext + + singleWalletState.expressTxs.map { tx -> + async { + if (tx is ExpressTransactionStateUM.OnrampUM) { + updateOnrampTxStatus(tx) + } + } + }.awaitAll() + } + + private suspend fun updateOnrampTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM) { + if (!onrampTx.activeStatus.isTerminal) { + getOnrampStatusUseCase(onrampTx.info.txId).fold( + ifLeft = { + Timber.e("Couldn't update onramp status. $it") + }, + ifRight = { statusModel -> + val externalTxId = statusModel.externalTxId + val status = toAnalyticStatus(statusModel.status) ?: return + + if (statusModel.status != onrampTx.activeStatus) { + analyticsEventHandler.send( + TokenOnrampAnalyticsEvent.OnrampStatusChanged( + tokenSymbol = onrampTx.info.toAmountSymbol, + status = status.name, + provider = onrampTx.providerName, + fiatCurrency = onrampTx.fromCurrencyCode, + ), + ) + onrampUpdateTransactionStatusUseCase(externalTxId = externalTxId, statusModel.status) + } + }, + ) + } + } + + private fun toAnalyticStatus(status: OnrampStatus.Status?): ExpressAnalyticsStatus? { + return when (status) { + Expired, + Paused, + -> ExpressAnalyticsStatus.Cancelled + Created, + WaitingForPayment, + PaymentProcessing, + Paid, + Sending, + -> ExpressAnalyticsStatus.InProgress + Verifying -> ExpressAnalyticsStatus.KYC + Failed -> ExpressAnalyticsStatus.Fail + Finished -> ExpressAnalyticsStatus.Done + null -> null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 033f180725..80663dd7de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -161,7 +161,7 @@ internal enum class Wallet2CobrandImage( Pastel( cards2ResId = R.drawable.ill_pastel_cards3_120_106, cards3ResId = R.drawable.ill_pastel_cards3_120_106, - batchIds = setOf("AF43", "AF44", "AF45"), + batchIds = setOf("AF43", "AF44", "AF45", "AF78", "AF79", "AF80"), ), RedPanda( @@ -204,7 +204,7 @@ internal enum class Wallet2CobrandImage( Vivid( cards2ResId = R.drawable.ill_vivid_cards3_120_106, cards3ResId = R.drawable.ill_vivid_cards3_120_106, - batchIds = setOf("AF40", "AF41", "AF42"), + batchIds = setOf("AF40", "AF41", "AF42", "AF75", "AF76", "AF77"), ), VoltInu( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index b4c0f3252e..0d96fb7855 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.onramp.GetOnrampTransactionsUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase @@ -27,6 +29,8 @@ internal class SingleWalletContentLoader( private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, ) : WalletContentLoader(id = userWallet.walletId) { @@ -55,6 +59,16 @@ internal class SingleWalletContentLoader( getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, ), + SingleWalletExpressStatusesSubscriber( + userWallet = userWallet, + stateHolder = stateHolder, + clickIntents = clickIntents, + getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, + analyticsEventHandler = analyticsEventHandler, + getPrimaryCurrencyStatusUpdatesUseCase = getPrimaryCurrencyStatusUpdatesUseCase, + getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, + onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, + ), TxHistorySubscriber( userWallet = userWallet, isRefresh = isRefresh, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt index c6be994278..aa40c79e7f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.onramp.GetOnrampTransactionsUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase @@ -26,6 +28,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, ) { @@ -45,6 +49,8 @@ internal class SingleWalletContentLoaderFactory @Inject constructor( getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, analyticsEventHandler = analyticsEventHandler, walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, + getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, + onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 6a23ccc059..4f86e6685c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState @@ -59,6 +60,8 @@ internal sealed interface WalletState : WalletStateHolder { override val buttons: PersistentList, override val marketPriceBlockState: MarketPriceBlockState, override val txHistoryState: TxHistoryState, + val expressTxsToDisplay: PersistentList, + val expressTxs: PersistentList, ) : SingleCurrency() data class Locked( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt new file mode 100644 index 0000000000..98058d6c2e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -0,0 +1,76 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.collections.immutable.toPersistentList +import timber.log.Timber + +internal class SetExpressStatusesTransformer( + userWalletId: UserWalletId, + private val onrampTxs: List, + private val clickIntents: WalletClickIntents, + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val analyticsEventHandler: AnalyticsEventHandler, +) : WalletStateTransformer(userWalletId) { + override fun transform(prevState: WalletState): WalletState { + return when (prevState) { + is WalletState.SingleCurrency.Content -> { + val expressTxs = SingleWalletOnrampTransactionConverter( + clickIntents = clickIntents, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrency = appCurrency, + analyticsEventHandler = analyticsEventHandler, + ).convertList(onrampTxs).toPersistentList() + + val expressTxsToDisplay = expressTxs.filterNot { + it.activeStatus.isHidden + }.toPersistentList() + + val expressBottomSheet = prevState.bottomSheetConfig?.content as? ExpressStatusBottomSheetConfig + val currentTx = expressTxs.firstOrNull { it.info.txId == expressBottomSheet?.value?.info?.txId } + + prevState.copy( + expressTxs = expressTxs, + expressTxsToDisplay = expressTxsToDisplay, + bottomSheetConfig = prevState.bottomSheetConfig?.updateStateWithExpressStatusBottomSheet(currentTx), + ) + } + is WalletState.SingleCurrency.Locked -> { + Timber.w("Impossible to load express statuses for locked wallet") + prevState + } + is WalletState.Visa -> { + Timber.w("Impossible to load express statuses for visa wallet") + prevState + } + is WalletState.MultiCurrency -> { + Timber.w("Impossible to load express statuses for multi-currency wallet") + prevState + } + } + } + + private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet( + expressState: ExpressTransactionStateUM?, + ): TangemBottomSheetConfig { + val currentConfig = this.content as? ExpressStatusBottomSheetConfig ?: return this + if (expressState == null) return this + return copy( + content = if (currentConfig.value != expressState) { + ExpressStatusBottomSheetConfig(expressState) + } else { + currentConfig + }, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt new file mode 100644 index 0000000000..f6443b6ed5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -0,0 +1,279 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.expressStatus.state.ExpressLinkUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState +import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM +import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.common.ui.notifications.ExpressNotificationsUM +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday +import com.tangem.core.ui.utils.toTimeFormat +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.onramp.model.OnrampStatus +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.feature.wallet.impl.R +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM +import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class SingleWalletOnrampTransactionConverter( + private val clickIntents: WalletClickIntents, + cryptoCurrencyStatus: CryptoCurrencyStatus, + private val appCurrency: AppCurrency, + private val analyticsEventHandler: AnalyticsEventHandler, +) : Converter { + + private val iconStateConverter = CryptoCurrencyToIconStateConverter() + + private val currency = cryptoCurrencyStatus.currency + private val status = cryptoCurrencyStatus.value + + override fun convert(value: OnrampTransaction): ExpressTransactionStateUM.OnrampUM { + return ExpressTransactionStateUM.OnrampUM( + info = ExpressTransactionStateInfoUM( + title = resourceReference( + id = R.string.express_status_buying, + wrappedList(currency.name), + ), + status = convertStatuses(value.status, value.externalTxUrl), + notification = getNotification(value.status, value.externalTxUrl, value.providerName), + txId = value.txId, + txExternalId = value.externalTxId, + txExternalUrl = value.externalTxUrl, + timestamp = value.timestamp, + timestampFormatted = resourceReference( + R.string.send_date_format, + wrappedList( + value.timestamp.toDateFormatWithTodayYesterday(), + value.timestamp.toTimeFormat(), + ), + ), + toAmount = stringReference( + value.toAmount.format { crypto(currency) }, + ), + toFiatAmount = stringReference( + status.fiatRate?.multiply(value.toAmount).format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + toAmountSymbol = currency.symbol, + toCurrencyIcon = iconStateConverter.convert(currency), + fromAmount = stringReference( + value.fromAmount.format { + fiat( + fiatCurrencyCode = value.fromCurrency.name, + fiatCurrencySymbol = value.fromCurrency.code, + ) + }, + ), + fromFiatAmount = null, + fromAmountSymbol = value.fromCurrency.code, + fromCurrencyIcon = CurrencyIconState.FiatIcon( + url = value.fromCurrency.image, + fallbackResId = R.drawable.ic_currency_24, + ), + iconState = getIconState(value.status), + onGoToProviderClick = { + analyticsEventHandler.send(TokenOnrampAnalyticsEvent.GoToProvider) + clickIntents.onGoToProviderClick(it) + }, + onClick = { + val analyticEvent = TokenOnrampAnalyticsEvent.OnrampStatusOpened( + tokenSymbol = currency.symbol, + provider = value.providerName, + fiatCurrency = value.fromCurrency.code, + ) + analyticsEventHandler.send(analyticEvent) + clickIntents.onExpressTransactionClick(value.txId) + }, + ), + providerName = value.providerName, + providerImageUrl = value.providerImageUrl, + providerType = value.providerType, + activeStatus = value.status, + fromCurrencyCode = value.fromCurrency.code, + ) + } + + private fun getNotification( + status: OnrampStatus.Status, + externalTxUrl: String?, + providerName: String, + ): NotificationUM? { + if (externalTxUrl == null) return null + return when (status) { + OnrampStatus.Status.Verifying -> { + analyticsEventHandler.send( + TokenOnrampAnalyticsEvent.NoticeKYC(currency.symbol, providerName), + ) + ExpressNotificationsUM.NeedVerification { + clickIntents.onGoToProviderClick(externalTxUrl) + } + } + OnrampStatus.Status.Failed -> { + ExpressNotificationsUM.FailedByProvider { + clickIntents.onGoToProviderClick(externalTxUrl) + } + } + else -> null + } + } + + private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { + return when (status) { + OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning + OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error + else -> ExpressTransactionStateIconUM.None + } + } + + private fun convertStatuses(status: OnrampStatus.Status, externalTxUrl: String?): ExpressStatusUM { + val statuses = with(status) { + persistentListOf( + getAwaitingDepositItem(), + getPaymentProcessingItem(), + getBuyingItem(), + getSendingItem(), + ) + } + + return ExpressStatusUM( + title = resourceReference(R.string.common_transaction_status), + link = getStatusLink(status, externalTxUrl), + statuses = statuses, + ) + } + + private fun OnrampStatus.Status.getAwaitingDepositItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.WaitingForPayment.order -> { + resourceReference(R.string.express_exchange_status_receiving) + } + this == OnrampStatus.Status.WaitingForPayment -> { + resourceReference(R.string.express_exchange_status_receiving_active) + } + else -> { + resourceReference(R.string.express_exchange_status_received) + } + }, + state = getStatusState(OnrampStatus.Status.WaitingForPayment), + ) + + private fun OnrampStatus.Status.getPaymentProcessingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.PaymentProcessing.order -> { + resourceReference(R.string.express_exchange_status_confirming) + } + this == OnrampStatus.Status.PaymentProcessing -> { + resourceReference(R.string.express_exchange_status_confirming_active) + } + this == OnrampStatus.Status.Verifying -> { + resourceReference(R.string.express_exchange_status_verifying) + } + this == OnrampStatus.Status.Failed -> { + resourceReference(R.string.express_exchange_status_failed) + } + else -> resourceReference(R.string.express_exchange_status_confirmed) + }, + state = when { + order < OnrampStatus.Status.PaymentProcessing.order -> { + ExpressStatusItemState.Default + } + this == OnrampStatus.Status.PaymentProcessing -> { + ExpressStatusItemState.Active + } + this == OnrampStatus.Status.Verifying -> { + ExpressStatusItemState.Warning + } + this == OnrampStatus.Status.Failed -> { + ExpressStatusItemState.Error + } + else -> { + ExpressStatusItemState.Done + } + }, + ) + + private fun OnrampStatus.Status.getBuyingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.Paid.order -> { + resourceReference(R.string.express_status_buying, wrappedList(currency.name)) + } + this == OnrampStatus.Status.Paid -> { + resourceReference( + R.string.express_status_buying_active, + wrappedList(currency.name), + ) + } + else -> { + resourceReference(R.string.express_status_bought, wrappedList(currency.name)) + } + }, + state = getStatusState(OnrampStatus.Status.Paid), + ) + + private fun OnrampStatus.Status.getSendingItem() = ExpressStatusItemUM( + text = when { + order < OnrampStatus.Status.Sending.order -> { + resourceReference( + R.string.express_exchange_status_sending, + wrappedList(currency.name), + ) + } + this == OnrampStatus.Status.Sending -> { + resourceReference( + R.string.express_exchange_status_sending_active, + wrappedList(currency.name), + ) + } + else -> { + resourceReference( + R.string.express_exchange_status_sent, + wrappedList(currency.name), + ) + } + }, + state = getStatusState(OnrampStatus.Status.Sending), + ) + + private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { + if (externalTxUrl == null) return ExpressLinkUM.Empty + return when (status) { + OnrampStatus.Status.Verifying, + OnrampStatus.Status.Failed, + -> { + ExpressLinkUM.Content( + icon = R.drawable.ic_arrow_top_right_24, + text = resourceReference(R.string.common_go_to_provider), + onClick = { + clickIntents.onGoToProviderClick(externalTxUrl) + }, + ) + } + else -> ExpressLinkUM.Empty + } + } + + private fun OnrampStatus.Status.getStatusState(targetState: OnrampStatus.Status) = when { + order < targetState.order -> ExpressStatusItemState.Default + this == targetState -> ExpressStatusItemState.Active + else -> ExpressStatusItemState.Done + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index aa619fe1e2..45b1b98237 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -58,6 +58,8 @@ internal class WalletLoadingStateFactory( value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick), ), ), + expressTxsToDisplay = persistentListOf(), + expressTxs = persistentListOf(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt new file mode 100644 index 0000000000..37eb922c67 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -0,0 +1,92 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import arrow.core.Either +import arrow.core.getOrElse +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.onramp.GetOnrampTransactionsUseCase +import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase +import com.tangem.domain.onramp.model.cache.OnrampTransaction +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.error.CurrencyStatusError +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer +import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.WalletClickIntents +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import timber.log.Timber + +@Suppress("LongParameterList") +internal class SingleWalletExpressStatusesSubscriber( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val clickIntents: WalletClickIntents, + private val analyticsEventHandler: AnalyticsEventHandler, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, +) : WalletSubscriber() { + + override fun create( + coroutineScope: CoroutineScope, + ): Flow, AppCurrency>> { + return combine( + flow = getPrimaryCurrencyStatusUpdatesUseCase(userWalletId = userWallet.walletId) + .conflate() + .distinctUntilChanged(), + flow2 = getSelectedAppCurrencyUseCase() + .conflate() + .distinctUntilChanged() + .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, + transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, + ).onEach { maybeCurrencyStatusAndAppCurrency -> + val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { + Timber.e("Unable to get primary currency status: $it") + return@onEach + } + + getOnrampTransactionsUseCase( + userWalletId = userWallet.walletId, + cryptoCurrencyId = status.currency.id, + ).onEach { maybeTransaction -> + maybeTransaction.fold( + ifRight = { onrampTxs -> + onrampTxs.clearHiddenTerminal() + stateHolder.update( + SetExpressStatusesTransformer( + userWalletId = userWallet.walletId, + onrampTxs = onrampTxs, + clickIntents = clickIntents, + cryptoCurrencyStatus = status, + appCurrency = maybeCurrencyStatusAndAppCurrency.second, + analyticsEventHandler = analyticsEventHandler, + ), + ) + }, + ifLeft = { + stateHolder.update( + SetExpressStatusesTransformer( + userWalletId = userWallet.walletId, + onrampTxs = listOf(), + clickIntents = clickIntents, + cryptoCurrencyStatus = status, + appCurrency = maybeCurrencyStatusAndAppCurrency.second, + analyticsEventHandler = analyticsEventHandler, + ), + ) + }, + ) + } + .launchIn(coroutineScope) + } + } + + private suspend fun List.clearHiddenTerminal() { + this.filter { it.status.isHidden && it.status.isTerminal } + .forEach { onrampRemoveTransactionUseCase(externalTxId = it.externalTxId) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 17da55077b..f9cee1b0a5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -16,6 +16,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.ui.utils.ReviewManagerRequester import com.tangem.feature.wallet.presentation.wallet.ui.utils.animateScrollByIndex import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolling +import kotlinx.coroutines.delay @Suppress("LongParameterList") @Composable @@ -35,6 +36,7 @@ internal fun WalletEventEffect( onTrigger = { value -> when (value) { is WalletEvent.ChangeWallet -> { + delay(timeMillis = 1000) // to let compose time to start drawing otherwise animation doesnt work onAutoScrollSet() walletsListState.animateScrollByIndex(prevIndex = selectedWalletIndex, newIndex = value.index) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 061f3c506b..6a8f8eb2bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -44,6 +44,9 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.google.accompanist.systemuicontroller.rememberSystemUiController +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheet +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig +import com.tangem.common.ui.expressStatus.expressTransactionsItems import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -216,6 +219,12 @@ private fun WalletContent( walletState.marketPriceBlockState?.let { marketPriceBlockState -> marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) } + if (walletState is WalletState.SingleCurrency.Content) { + expressTransactionsItems( + expressTxs = walletState.expressTxsToDisplay, + modifier = itemModifier, + ) + } } (selectedWallet as? WalletState.Visa.Content)?.let { @@ -716,6 +725,7 @@ private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { is BalancesAndLimitsBottomSheetConfig -> BalancesAndLimitsBottomSheet(config = bottomSheetConfig) is VisaTxDetailsBottomSheetConfig -> VisaTxDetailsBottomSheet(config = bottomSheetConfig) is PushNotificationsBottomSheetConfig -> PushNotificationsBottomSheet(config = bottomSheetConfig) + is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a9af33b972..145ad36baa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -17,6 +17,7 @@ import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader @@ -33,9 +34,7 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.indexOfFirstOrNull import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay @@ -70,6 +69,7 @@ internal class WalletViewModel @Inject constructor( private val shouldAskPermissionUseCase: ShouldAskPermissionUseCase, private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, + private val onrampStatusFactory: OnrampStatusFactory, private val walletFeatureToggles: WalletFeatureToggles, analyticsEventsHandler: AnalyticsEventHandler, ) : ViewModel() { @@ -81,6 +81,8 @@ internal class WalletViewModel @Inject constructor( private val refreshWalletJobHolder = JobHolder() private var needToRefreshWallet = false + private var expressTxStatusTaskScheduler = SingleTaskScheduler() + init { analyticsEventsHandler.send(WalletScreenAnalyticsEvent.MainScreen.ScreenOpened) @@ -93,6 +95,7 @@ internal class WalletViewModel @Inject constructor( subscribeOnSelectedWalletFlow() subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() + subscribeOnExpressTransactionsUpdates() } private fun maybeMigrateNames() { @@ -239,6 +242,24 @@ internal class WalletViewModel @Inject constructor( .launchIn(viewModelScope) } + private fun subscribeOnExpressTransactionsUpdates() { + viewModelScope.launch(dispatchers.main) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + viewModelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { onrampStatusFactory.updateOnrmapTransactionStatuses() } + }, + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) + } + } + private fun needToRefreshTimer() { viewModelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) @@ -342,7 +363,7 @@ internal class WalletViewModel @Inject constructor( ) } - private suspend fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { + private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { walletScreenContentLoader.load( userWallet = action.selectedWallet, clickIntents = clickIntents, @@ -358,8 +379,6 @@ internal class WalletViewModel @Inject constructor( ), ) - withContext(dispatchers.io) { delay(timeMillis = 1000) } - scrollToWallet(index = action.selectedWalletIndex) } @@ -430,5 +449,6 @@ internal class WalletViewModel @Inject constructor( private companion object { const val REFRESH_WALLET_BACKGROUND_TIMER_MILLIS = 10000L + const val EXPRESS_STATUS_UPDATE_DELAY = 10000L } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt index 32132d1dfc..0b20b60c75 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletContentClickIntents.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.viewmodels.intents import arrow.core.getOrElse +import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase @@ -11,10 +12,14 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped @@ -43,6 +48,12 @@ internal interface WalletContentClickIntents { fun onTokenItemLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus) fun onTransactionClick(txHash: String) + + fun onDissmissBottomSheet() + + fun onGoToProviderClick(externalTxId: String) + + fun onExpressTransactionClick(txId: String) } @Suppress("LongParameterList") @@ -51,6 +62,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, private val currencyActionsClickIntents: WalletCurrencyActionsClickIntentsImplementor, private val walletWarningsClickIntents: WalletWarningsClickIntentsImplementor, + private val onrampStatusFactory: OnrampStatusFactory, private val getUserWalletUseCase: GetUserWalletUseCase, private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, @@ -164,4 +176,39 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( ) } } + + override fun onGoToProviderClick(externalTxUrl: String) { + router.openUrl(externalTxUrl) + } + + override fun onDissmissBottomSheet() { + val userWalletId = stateHolder.getSelectedWalletId() + if (stateHolder.getSelectedWallet().bottomSheetConfig?.content is ExpressStatusBottomSheetConfig) { + viewModelScope.launch(dispatchers.main) { + onrampStatusFactory.removeTransactionOnBottomSheetClosed() + } + } + stateHolder.update(CloseBottomSheetTransformer(userWalletId)) + } + + override fun onExpressTransactionClick(txId: String) { + viewModelScope.launch { + val userWalletId = stateHolder.getSelectedWalletId() + val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content + ?: return@launch + + val expressTransaction = singleWalletState.expressTxsToDisplay.firstOrNull { it.info.txId == txId } + ?: return@launch + + stateHolder.update( + OpenBottomSheetTransformer( + userWalletId = userWalletId, + content = ExpressStatusBottomSheetConfig( + value = expressTransaction, + ), + onDismissBottomSheet = ::onDissmissBottomSheet, + ), + ) + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index e720d18e1f..a2fa5ca344 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap -import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase @@ -33,13 +32,14 @@ import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.staking.model.stakekit.Yield -import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase +import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase +import com.tangem.domain.tokens.RemoveCurrencyUseCase import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -54,9 +54,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch import java.math.BigDecimal import javax.inject.Inject @@ -109,8 +107,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val getPrimaryCurrencyStatusUpdatesUseCase: GetPrimaryCurrencyStatusUpdatesUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, - private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val analyticsEventHandler: AnalyticsEventHandler, @@ -138,63 +134,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( if (handleUnavailabilityReason(unavailabilityReason)) return stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWallet.walletId)) - viewModelScope.launch(dispatchers.main) { - val maybeFeeCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWallet.walletId, cryptoCurrencyStatus).getOrNull() - when (val currency = cryptoCurrencyStatus.currency) { - is CryptoCurrency.Coin -> sendCoin(cryptoCurrencyStatus, userWallet, maybeFeeCurrencyStatus) - is CryptoCurrency.Token -> sendToken( - cryptoCurrency = currency, - cryptoCurrencyStatus = cryptoCurrencyStatus.value, - feeCurrencyStatus = maybeFeeCurrencyStatus, - userWallet = userWallet, - ) - } - } - } - - private fun sendCoin( - cryptoCurrencyStatus: CryptoCurrencyStatus, - userWallet: UserWallet, - feeCurrencyStatus: CryptoCurrencyStatus?, - ) { - reduxStateHolder.dispatch( - action = TradeCryptoAction.SendCoin( - userWallet = userWallet, - coinStatus = cryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - ), + val route = AppRoute.Send( + currency = cryptoCurrencyStatus.currency, + userWalletId = userWallet.walletId, ) - } - private fun sendToken( - cryptoCurrency: CryptoCurrency.Token, - cryptoCurrencyStatus: CryptoCurrencyStatus.Value, - feeCurrencyStatus: CryptoCurrencyStatus?, - userWallet: UserWallet, - ) { - viewModelScope.launch(dispatchers.main) { - getNetworkCoinStatusUseCase( - userWalletId = userWallet.walletId, - networkId = cryptoCurrency.network.id, - derivationPath = cryptoCurrency.network.derivationPath, - isSingleWalletWithTokens = userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - .take(count = 1) - .collectLatest { - it.onRight { coinStatus -> - reduxStateHolder.dispatch( - action = TradeCryptoAction.SendToken( - userWallet = userWallet, - tokenCurrency = cryptoCurrency, - tokenFiatRate = cryptoCurrencyStatus.fiatRate, - coinFiatRate = coinStatus.value.fiatRate, - feeCurrencyStatus = feeCurrencyStatus, - ), - ) - } - } - } + appRouter.push(route) } override fun onReceiveClick(cryptoCurrencyStatus: CryptoCurrencyStatus) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index b08fc610e1..45b1f33b00 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-889" +tangemBlockchainSdk = "develop-890" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-415" +tangemCardSdk = "develop-416" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem17" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 1fbc46c22c..ab076272bd 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -145,4 +145,9 @@ object BlockchainUtils { private fun getNetworkNameWithoutTestnet(blockchain: Blockchain): String { return blockchain.getNetworkName().replace(oldValue = " Testnet", newValue = "") } + + fun isTokenBetaFunctionality(blockchainId: String): Boolean { + val blockchain = Blockchain.fromId(blockchainId) + return blockchain == Blockchain.Kaspa + } } \ No newline at end of file