From 0450ce990664c34667561c9764283664418ecaa1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 29 Jan 2024 10:44:28 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../main/java/com/tangem/tap/MainActivity.kt | 14 +- .../tap/proxy/redux/DaggerGraphState.kt | 2 +- core/deep-links/.gitignore | 1 + core/deep-links/build.gradle.kts | 23 +++ .../com/tangem/core/deeplink/DeepLink.kt | 36 ++++ .../tangem/core/deeplink/DeepLinksRegistry.kt | 63 +++++++ .../core/deeplink/di/DeepLinksModule.kt | 20 ++ .../deeplink/impl/DefaultDeepLinksRegistry.kt | 173 ++++++++++++++++++ .../utils/DeepLinksLifecycleObserver.kt | 20 ++ features/send/api/build.gradle.kts | 1 - settings.gradle.kts | 3 +- 12 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 core/deep-links/.gitignore create mode 100644 core/deep-links/build.gradle.kts create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt create mode 100644 core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85ed2fdfb3..834e2bdd48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,6 +62,7 @@ dependencies { implementation(projects.core.ui) implementation(projects.core.datasource) implementation(projects.core.utils) + implementation(projects.core.deepLinks) implementation(projects.libs.crypto) implementation(projects.libs.auth) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index c31c62fa66..74f347ee5f 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -23,9 +23,9 @@ import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.feature.qrscanning.QrScanningRouter import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.Snackbar +import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.AppScreen import com.tangem.core.navigation.NavigationAction import com.tangem.core.ui.event.StateEvent @@ -36,6 +36,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.tester.api.TesterRouter @@ -138,6 +139,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var qrScanningRouter: QrScanningRouter + @Inject + lateinit var deepLinksRegistry: DeepLinksRegistry + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -167,6 +171,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac checkForNotificationPermission() observeStateUpdates() + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } private fun observeStateUpdates() { @@ -332,6 +340,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycleScope.launch { intentProcessor.handleIntent(intent) } + + if (intent != null) { + deepLinksRegistry.launch(intent) + } } override fun showSnackbar( diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 6bd651f70c..b1839fae93 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.proxy.redux -import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.repository.AppThemeModeRepository @@ -13,6 +12,7 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.feature.qrscanning.QrScanningRouter import com.tangem.features.managetokens.featuretoggles.ManageTokensFeatureToggles import com.tangem.features.managetokens.navigation.ManageTokensRouter import com.tangem.features.send.api.featuretoggles.SendFeatureToggles diff --git a/core/deep-links/.gitignore b/core/deep-links/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/core/deep-links/.gitignore @@ -0,0 +1 @@ +/build diff --git a/core/deep-links/build.gradle.kts b/core/deep-links/build.gradle.kts new file mode 100644 index 0000000000..9d43ab04df --- /dev/null +++ b/core/deep-links/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.core.deeplink" +} + +dependencies { + + /* Libs - AndroidX */ + implementation(deps.lifecycle.runtime.ktx) + + /* Libs - Other */ + implementation(deps.timber) + + /* DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt new file mode 100644 index 0000000000..0b85327f28 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLink.kt @@ -0,0 +1,36 @@ +package com.tangem.core.deeplink + +/** + * Represents a deep link. + */ +interface DeepLink { + + /** + * ID of the deep link. + * + * By default, it is the same as the [uri]. + * */ + val id: String get() = uri + + /** + * URI of the deep link. + * + * **Note: Remember to add the URI in the AndroidManifest.xml file in the `app` module.** + * + * Query parameters will be received automatically. + * + * Path parameters can be added using the following syntax: + * ```kotlin + * "tangem://link" // Without parameters + * "tangem://link/{param1}/{param2}" // With path parameters + * ``` + * */ + val uri: String + + /** + * Method to be called when this deep link is received. + * + * @param params Map of parameters received from the deep link. + * */ + fun onReceive(params: Map) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt new file mode 100644 index 0000000000..beaeb5f0d2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt @@ -0,0 +1,63 @@ +package com.tangem.core.deeplink + +import android.content.Intent +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel + +// TODO: Add tests +/** + * Provides functionality to handle deep links. + * + * Allows deep links to be launched, registered, or unregistered. + */ +interface DeepLinksRegistry { + + /** + * Finds matches registered deep links for the given [intent] and launches them. + * + * @return `true` if any deep link was received, `false` otherwise. + */ + fun launch(intent: Intent): Boolean + + /** + * Registers the given [deepLink]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLink: DeepLink) + + /** + * Registers the given [deepLinks]. + * + * @see registerWithLifecycle + * @see registerWithViewModel + */ + fun register(deepLinks: Collection) + + /** + * Unregisters the given [deepLinks]. + */ + fun unregister(deepLinks: Collection) + + /** + * Unregisters the given [deepLink]. + */ + fun unregister(deepLink: DeepLink) + + /** + * Unregisters deep links with the given [ids]. + * */ + fun unregisterByIds(ids: Collection) + + /** + * Registers the [deepLinks] when the [owner] is resumed and ensures that they are unregistered when the [owner] is + * stopped. + */ + fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) + + /** + * Registers the [deepLinks] and ensures that they are unregistered when the [ViewModel] is closed. + */ + fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt new file mode 100644 index 0000000000..4398db12a2 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/di/DeepLinksModule.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.di + +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.impl.DefaultDeepLinksRegistry +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object DeepLinksModule { + + @Provides + @Singleton + fun provideDeepLinksRegistry(): DeepLinksRegistry { + return DefaultDeepLinksRegistry() + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt new file mode 100644 index 0000000000..bf84d3e382 --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/impl/DefaultDeepLinksRegistry.kt @@ -0,0 +1,173 @@ +package com.tangem.core.deeplink.impl + +import android.content.Intent +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ViewModel +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.utils.DeepLinksLifecycleObserver +import timber.log.Timber + +internal class DefaultDeepLinksRegistry : DeepLinksRegistry { + + private var registries: List = emptyList() + + override fun launch(intent: Intent): Boolean { + val received = intent.data ?: return false + var hasMatch = false + + Timber.d( + """ + Received deep link intent + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + registries.forEach { deepLink -> + val expected = deepLink.uri.toUri() + + if (!isMatches(expected, received)) return@forEach + hasMatch = true + + val params = getParams(expected, received) + + Timber.d( + """ + Matched deep link + |- Expected URI: $expected + |- Received URI: $received + |- Params: $params + """.trimIndent(), + ) + deepLink.onReceive(params) + } + + if (!hasMatch) { + Timber.d( + """ + No match found for deep link + |- Received URI: $received + |- Registries: $registries + """.trimIndent(), + ) + } + + return hasMatch + } + + override fun register(deepLinks: Collection) { + registries = (registries + deepLinks).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun register(deepLink: DeepLink) { + registries = (registries + deepLink).distinctBy(DeepLink::id) + + Timber.d( + """ + Registered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLinks: Collection) { + registries = registries.filter { it !in deepLinks } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregister(deepLink: DeepLink) { + registries = registries.filter { it.id != deepLink.id } + + Timber.d( + """ + Unregistered deep link + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun unregisterByIds(ids: Collection) { + registries = registries.filter { it.id !in ids } + + Timber.d( + """ + Unregistered deep links + |- Registries: $registries + """.trimIndent(), + ) + } + + override fun registerWithLifecycle(owner: LifecycleOwner, deepLinks: Collection) { + val observer = DeepLinksLifecycleObserver(deepLinksRegistry = this, deepLinks) + owner.lifecycle.addObserver(observer) + } + + override fun registerWithViewModel(viewModel: ViewModel, deepLinks: Collection) { + viewModel.addCloseable { + unregister(deepLinks) + } + + register(deepLinks) + } + + private fun isMatches(received: Uri, expected: Uri): Boolean { + if (received == expected) return true + if (received.authority != expected.authority || + received.pathSegments.size != expected.pathSegments.size + ) { + return false + } + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + !(receivedSegment.startsWith(prefix = "{") && receivedSegment.endsWith(suffix = "}")) + ) { + return false + } + } + + return true + } + + private fun getParams(received: Uri, expected: Uri): Map { + val params = mutableMapOf() + + received.pathSegments.forEachIndexed { index, receivedSegment -> + val expectedSegment = expected.pathSegments[index] + if (receivedSegment != expectedSegment && + receivedSegment.startsWith(prefix = "{") && + receivedSegment.endsWith(suffix = "}") + ) { + val path = receivedSegment + .replace(oldValue = "{", newValue = "") + .replace(oldValue = "}", newValue = "") + + params[path] = expectedSegment + } + } + + expected.queryParameterNames.forEach { paramName -> + expected.getQueryParameter(paramName)?.let { param -> + params[paramName] = param + } + } + + return params + } +} \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt new file mode 100644 index 0000000000..ae1580e17e --- /dev/null +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/utils/DeepLinksLifecycleObserver.kt @@ -0,0 +1,20 @@ +package com.tangem.core.deeplink.utils + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import com.tangem.core.deeplink.DeepLink +import com.tangem.core.deeplink.DeepLinksRegistry + +internal class DeepLinksLifecycleObserver( + private val deepLinksRegistry: DeepLinksRegistry, + private val deepLinks: Collection, +) : DefaultLifecycleObserver { + + override fun onResume(owner: LifecycleOwner) { + deepLinksRegistry.register(deepLinks) + } + + override fun onPause(owner: LifecycleOwner) { + deepLinksRegistry.unregister(deepLinks) + } +} \ No newline at end of file diff --git a/features/send/api/build.gradle.kts b/features/send/api/build.gradle.kts index 6b742b5542..eb561fcf20 100644 --- a/features/send/api/build.gradle.kts +++ b/features/send/api/build.gradle.kts @@ -10,7 +10,6 @@ android { } dependencies { - implementation(projects.domain.tokens.models) /** AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/settings.gradle.kts b/settings.gradle.kts index edf4844e3e..7060d410f2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,7 @@ include(":core:navigation") include(":core:res") include(":core:ui") include(":core:utils") +include(":core:deep-links") // endregion Core modules // region Libs modules @@ -148,4 +149,4 @@ include(":data:txhistory") include(":data:wallets") include(":data:analytics") include(":data:transaction") -// endregion Data modules +// endregion Data modules \ No newline at end of file