diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13f1801543..b61add5332 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -176,6 +176,16 @@ android:scheme="tangem" /> + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8a5266c470..0a6ed669e7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -76,7 +76,7 @@ import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import com.tangem.utils.extensions.validate +import com.tangem.utils.extensions.uriValidate import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -496,7 +496,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { receivedDeepLink != null -> { deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) } - webLink?.validate() == true -> { + webLink?.uriValidate() == true -> { urlOpener.openUrl(webLink) } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 7c69a01577..1e05612cee 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -8,11 +8,13 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.extensions.uriValidate import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,6 +34,7 @@ internal class DeepLinkFactory @Inject constructor( private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory, private val walletDeepLink: WalletDeepLinkHandler.Factory, private val tokenDetailsDeepLink: TokenDetailsDeepLinkHandler.Factory, + private val stakingDeepLink: StakingDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -101,6 +104,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Referral.host -> referralDeepLink.create() DeepLinkRoute.Wallet.host -> walletDeepLink.create() DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams) else -> { Timber.i( """ @@ -119,7 +123,7 @@ internal class DeepLinkFactory @Inject constructor( uri.queryParameterNames.forEach { paramName -> val paramValue = uri.getQueryParameter(paramName) - if (paramName.validate() && paramValue?.validate() == true) { + if (paramName.uriValidate() && paramValue?.uriValidate() == true) { params[paramName] = paramValue } } diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 32be06bd2f..f13bfa3ad8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -6,6 +6,7 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler @@ -45,6 +46,9 @@ class DeepLinkFactoryTest { private val tokenDetailsDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } + private val stakingDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } private val mockedUri = mockk(relaxed = true) @@ -59,6 +63,7 @@ class DeepLinkFactoryTest { walletConnectDeepLinkFactory, walletDeepLinkFactory, tokenDetailsDeepLinkFactory, + stakingDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -204,6 +209,12 @@ class DeepLinkFactoryTest { advanceUntilIdle() verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + // Test Staking + every { mockedUri.host } returns "staking" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + // Reset params every { mockedUri.queryParameterNames } returns emptySet() every { mockedUri.getQueryParameter(any()) } returns "" diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index c683a4136d..3d7be7ec68 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -27,6 +27,10 @@ sealed class DeepLinkRoute { data object TokenDetails : DeepLinkRoute() { override val host: String = "token" } + + data object Staking : DeepLinkRoute() { + override val host: String = "staking" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt index 07624b12c0..5b8f7e3ca4 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt @@ -5,7 +5,7 @@ private const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" /** * Check for malicious symbol in uri part */ -fun String.validate(): Boolean { +fun String.uriValidate(): Boolean { val regex = DEEPLINK_VALIDATION_REGEX.toRegex() return !regex.containsMatchIn(this) diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt new file mode 100644 index 0000000000..0453140239 --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.api.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface StakingDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, queryParams: Map): StakingDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt new file mode 100644 index 0000000000..00d124d530 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -0,0 +1,94 @@ +package com.tangem.features.staking.impl.deeplink + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getYieldUseCase: GetYieldUseCase, +) : StakingDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + val userWalletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) + ?: getSelectedWalletSyncUseCase().getOrNull()?.walletId + + if (userWalletId == null) { + Timber.e("Error on getting user wallet") + return + } + + val networkId = queryParams[NETWORK_ID_KEY] + val tokenId = queryParams[TOKEN_ID_KEY] + + scope.launch { + val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse { + Timber.e("Error on getting crypto currency list") + return@launch + }.firstOrNull { + it.id.rawNetworkId == networkId && it.id.rawCurrencyId?.value == tokenId + } + + if (cryptoCurrency == null) { + Timber.e( + """ + Could not get crypto currency for + |- $NETWORK_ID_KEY: $networkId + |- $TOKEN_ID_KEY: $tokenId + """.trimIndent(), + ) + return@launch + } + + val yield = getYieldUseCase.invoke( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ).getOrElse { + error("Staking is unavailable for ${cryptoCurrency.name}") + return@launch + } + + appRouter.push( + AppRoute.Staking( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + yieldId = yield.id, + ), + ) + } + } + + @AssistedFactory + interface Factory : StakingDeepLinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultStakingDeepLinkHandler + } + + private companion object { + const val WALLET_ID_KEY = "walletId" + const val NETWORK_ID_KEY = "network_id" + const val TOKEN_ID_KEY = "token_id" + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt new file mode 100644 index 0000000000..b4ca9dc5b9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.deeplink.di + +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler +import com.tangem.features.staking.impl.deeplink.DefaultStakingDeepLinkHandler +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 StakingDeepLinkModule { + @Binds + @Singleton + fun bindStakingDeepLinkHandlerFactory(impl: DefaultStakingDeepLinkHandler.Factory): StakingDeepLinkHandler.Factory +} \ No newline at end of file