From bfdb3a29662515afd73a914b51b135161e6b1f9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Apr 2025 18:34:21 +0500 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 2 +- .../tangem/tap/features/main/MainViewModel.kt | 10 ++++ .../tangem/datasource/api/onramp/OnrampApi.kt | 2 +- .../data/onramp/DefaultOnrampRepository.kt | 13 ++++- .../onramp/GetOnrampRedirectUrlUseCase.kt | 5 +- features/onramp/api/build.gradle.kts | 1 + .../onramp/deeplink/OnrampDeepLink.kt | 12 +++++ features/onramp/impl/build.gradle.kts | 1 + .../onramp/deeplink/DefaultOnrampDeepLink.kt | 48 +++++++++++++++++++ .../onramp/deeplink/OnrampRedirectResult.kt | 14 ++++++ .../deeplink/di/OnrampDeeplinkModule.kt | 18 +++++++ .../redirect/model/OnrampRedirectModel.kt | 43 ++++++++++++++--- .../success/OnrampSuccessScreenTrigger.kt | 26 ++++++++++ .../di/OnrampSuccessComponentModule.kt | 11 +++++ 14 files changed, 194 insertions(+), 12 deletions(-) create mode 100644 features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampRedirectResult.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/OnrampSuccessScreenTrigger.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b9e46866c1..810024d550 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,7 +127,7 @@ 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 212e39e905..c00ae2518f 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 @@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.ui.R import com.tangem.core.ui.coil.ImagePreloader import com.tangem.core.ui.extensions.resourceReference @@ -31,6 +32,7 @@ import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.analytics.StoriesEvents +import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog @@ -63,6 +65,8 @@ internal class MainViewModel @Inject constructor( private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, private val onboardingRepository: OnboardingRepository, + private val deepLinksRegistry: DeepLinksRegistry, + private val onrampDeepLinkFactory: OnrampDeepLink.Factory, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -97,6 +101,8 @@ internal class MainViewModel @Inject constructor( sendKeyboardIdentifierEvent() preloadImages() + + initializeDeepLinks() } fun checkForUnfinishedBackup() { @@ -333,4 +339,8 @@ internal class MainViewModel @Inject constructor( ) } } + + private fun initializeDeepLinks() { + deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt index 433cf8b3eb..360e713a62 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt @@ -63,7 +63,7 @@ interface OnrampApi { @GET("onramp-data") suspend fun getData( @Header("user-id") userWalletId: String, - @Header("refcode") refCode: String, + @Header("refcode") refCode: String?, @Query("fromCurrencyCode") fromCurrencyCode: String, @Query("fromPrecision") fromPrecision: Int, @Query("toContractAddress") toContractAddress: String, 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 3001f28b09..b587d6f7e4 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 @@ -1,5 +1,6 @@ package com.tangem.data.onramp +import android.net.Uri import com.squareup.moshi.Moshi import com.tangem.blockchain.extensions.toBigDecimalOrDefault import com.tangem.data.common.api.safeApiCall @@ -349,6 +350,7 @@ internal class DefaultOnrampRepository( val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" } val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" } val requestId = UUID.randomUUID().toString() + val redirectUrl = quote.provider.toRedirectUrl() ?: error("Invalid onramp redirect url") val data = safeApiCall( call = { onrampApi.getData( @@ -362,7 +364,7 @@ internal class DefaultOnrampRepository( toDecimals = cryptoCurrency.decimals, providerId = quote.provider.id, toAddress = address, - redirectUrl = "", + redirectUrl = redirectUrl, language = null, theme = getTheme(isDarkTheme), requestId = requestId, @@ -370,7 +372,7 @@ internal class DefaultOnrampRepository( refCode = ExpressUtils.getRefCode( userWallet = userWallet, appPreferencesStore = appPreferencesStore, - ), + ).takeUnless { it.isEmpty() }, ).bind() }, onError = { e -> @@ -544,6 +546,11 @@ internal class DefaultOnrampRepository( null } + private fun OnrampProvider.toRedirectUrl() = Uri.Builder() + .path(REDIRECT_URL) + .appendPath(id) + .build().path + private companion object { const val PAYMENT_METHODS_KEY = "onramp_payment_methods" const val SELECTED_PAYMENT_METHOD_KEY = "onramp_selected_payment_method" @@ -553,5 +560,7 @@ internal class DefaultOnrampRepository( const val CURRENCIES_KEY = "onramp_currencies" const val PROVIDER_THEME_DARK = "dark" const val PROVIDER_THEME_LIGHT = "light" + + const val REDIRECT_URL = "https://tangem.com/onramp" } } \ No newline at end of file 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 dae9cfdb2a..d91f65451b 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 @@ -2,6 +2,7 @@ package com.tangem.domain.onramp import arrow.core.Either import com.tangem.domain.onramp.model.OnrampProviderWithQuote +import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository @@ -20,7 +21,7 @@ class GetOnrampRedirectUrlUseCase( quote: OnrampProviderWithQuote.Data, cryptoCurrency: CryptoCurrency, isDarkTheme: Boolean, - ): Either { + ): Either { return Either.catch { val transaction = repository.getOnrampData( userWallet = userWallet, @@ -29,7 +30,7 @@ class GetOnrampRedirectUrlUseCase( isDarkTheme = isDarkTheme, ) transactionRepository.storeTransaction(transaction) - transaction.redirectUrl + transaction }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/features/onramp/api/build.gradle.kts b/features/onramp/api/build.gradle.kts index ac5210f2a6..598b224b08 100644 --- a/features/onramp/api/build.gradle.kts +++ b/features/onramp/api/build.gradle.kts @@ -13,6 +13,7 @@ dependencies { /* Project - Core */ implementation(projects.core.decompose) implementation(projects.core.ui) + implementation(projects.core.deepLinks) /* Project - Domain */ implementation(projects.domain.onramp.models) diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt new file mode 100644 index 0000000000..1b2ae8c801 --- /dev/null +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt @@ -0,0 +1,12 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.core.deeplink.DeepLink +import kotlinx.coroutines.CoroutineScope + +abstract class OnrampDeepLink : DeepLink() { + override val uri = "tangem://onramp" + + interface Factory { + fun create(coroutineScope: CoroutineScope): OnrampDeepLink + } +} \ No newline at end of file diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index b32858e319..2307564258 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.configToggles) implementation(projects.core.decompose) + implementation(projects.core.deepLinks) implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt new file mode 100644 index 0000000000..a7edc4cbf5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLink.kt @@ -0,0 +1,48 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +internal class DefaultOnrampDeepLink @AssistedInject constructor( + private val appRouter: AppRouter, + private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, + @Assisted private val scope: CoroutineScope, +) : OnrampDeepLink() { + + override fun onReceive(params: Map) { + val txId = params[TX_ID_KEY] + val result = OnrampRedirectResult.getResult(params[RESULT_KEY]) + + when { + !txId.isNullOrEmpty() -> { + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = appRouter.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId)) + appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) + } + result != OnrampRedirectResult.Unknown -> { + scope.launch { + onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success) + } + } + } + } + + @AssistedFactory + interface Factory : OnrampDeepLink.Factory { + override fun create(coroutineScope: CoroutineScope): DefaultOnrampDeepLink + } + + private companion object { + const val TX_ID_KEY = "tx_id" + const val RESULT_KEY = "result" + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampRedirectResult.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampRedirectResult.kt new file mode 100644 index 0000000000..11dba8841e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampRedirectResult.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.deeplink + +enum class OnrampRedirectResult(val value: String) { + Success("success"), + Cancel("cancel"), + Unknown(""), + ; + + companion object { + fun getResult(result: String?): OnrampRedirectResult { + return entries.firstOrNull { it.value == result } ?: Unknown + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt new file mode 100644 index 0000000000..6663ec80d4 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -0,0 +1,18 @@ +package com.tangem.features.onramp.deeplink.di + +import com.tangem.features.onramp.deeplink.DefaultOnrampDeepLink +import com.tangem.features.onramp.deeplink.OnrampDeepLink +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface OnrampDeeplinkModule { + + @Binds + @Singleton + fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory +} \ No newline at end of file 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 7d8207b969..c9a11d1cb5 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 @@ -1,9 +1,10 @@ package com.tangem.features.onramp.redirect.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter 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.appbar.models.TopAppBarButtonUM @@ -13,14 +14,18 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase +import com.tangem.domain.onramp.model.cache.OnrampTransaction 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.redirect.OnrampRedirectComponent import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM import com.tangem.features.onramp.redirect.entity.OnrampRedirectUM +import com.tangem.features.onramp.success.OnrampSuccessScreenListener import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -30,16 +35,19 @@ internal class OnrampRedirectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val urlOpener: UrlOpener, private val getOnrampRedirectUrlUseCase: GetOnrampRedirectUrlUseCase, - private val getWalletsUseCase: GetWalletsUseCase, private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, - router: Router, + private val onrampSuccessScreenListener: OnrampSuccessScreenListener, + private val appRouter: AppRouter, paramsContainer: ParamsContainer, + getWalletsUseCase: GetWalletsUseCase, ) : Model() { private val params: OnrampRedirectComponent.Params = paramsContainer.require() private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + private var latestOnrampTransaction: OnrampTransaction? = null + val state = OnrampRedirectUM( topBarConfig = OnrampRedirectTopBarUM( title = combinedReference( @@ -48,7 +56,7 @@ internal class OnrampRedirectModel @Inject constructor( ), startButtonUM = TopAppBarButtonUM( iconRes = R.drawable.ic_close_24, - onIconClicked = router::pop, + onIconClicked = appRouter::pop, enabled = true, ), ), @@ -63,6 +71,10 @@ internal class OnrampRedirectModel @Inject constructor( ), ) + init { + subscribeToOnrampSuccessListener() + } + fun getRedirectUrl(isDarkTheme: Boolean) { modelScope.launch { getOnrampRedirectUrlUseCase.invoke( @@ -73,12 +85,31 @@ internal class OnrampRedirectModel @Inject constructor( ) .onLeft(::handleError) .onRight { - params.onBack() - urlOpener.openUrl(it) + latestOnrampTransaction = it + urlOpener.openUrl(it.redirectUrl) } } } + private fun subscribeToOnrampSuccessListener() { + onrampSuccessScreenListener.onrampSuccessTriggerFlow + .onEach { result -> + val latestTxId = latestOnrampTransaction?.txId + if (latestTxId != null && result) { + // Finish current onramp flow and show onramp success screen + val replaceOnrampScreens = appRouter.stack + .filterNot { it is AppRoute.Onramp } + .toMutableList() + replaceOnrampScreens.add(AppRoute.OnrampSuccess(latestTxId)) + appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) + } else { + // Close redirect screen and show last onramp state + params.onBack() + } + } + .launchIn(modelScope) + } + private fun handleError(error: OnrampError) { Timber.e(error.toString()) analyticsEventHandler.sendOnrampErrorEvent( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/OnrampSuccessScreenTrigger.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/OnrampSuccessScreenTrigger.kt new file mode 100644 index 0000000000..9dfa27bee6 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/OnrampSuccessScreenTrigger.kt @@ -0,0 +1,26 @@ +package com.tangem.features.onramp.success + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import javax.inject.Inject +import javax.inject.Singleton + +interface OnrampSuccessScreenListener { + val onrampSuccessTriggerFlow: Flow +} + +interface OnrampSuccessScreenTrigger { + suspend fun triggerOnrampSuccess(result: Boolean) +} + +@Singleton +internal class DefaultOnrampSuccessScreenTrigger @Inject constructor() : + OnrampSuccessScreenTrigger, + OnrampSuccessScreenListener { + + override val onrampSuccessTriggerFlow = MutableSharedFlow() + + override suspend fun triggerOnrampSuccess(result: Boolean) { + onrampSuccessTriggerFlow.emit(result) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt index fe2545877b..3a9cd54a03 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/di/OnrampSuccessComponentModule.kt @@ -2,6 +2,9 @@ package com.tangem.features.onramp.success.di import com.tangem.features.onramp.component.OnrampSuccessComponent import com.tangem.features.onramp.success.DefaultOnrampSuccessComponent +import com.tangem.features.onramp.success.DefaultOnrampSuccessScreenTrigger +import com.tangem.features.onramp.success.OnrampSuccessScreenListener +import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,4 +19,12 @@ internal interface OnrampSuccessComponentModule { fun bindOnrampSuccessComponentFactory( factory: DefaultOnrampSuccessComponent.Factory, ): OnrampSuccessComponent.Factory + + @Binds + @Singleton + fun bindOnrampSuccessScreenTrigger(impl: DefaultOnrampSuccessScreenTrigger): OnrampSuccessScreenTrigger + + @Binds + @Singleton + fun bindOnrampSuccessScreenListener(impl: DefaultOnrampSuccessScreenTrigger): OnrampSuccessScreenListener } \ No newline at end of file