diff --git a/.gitignore b/.gitignore index 74ed0fd34b..6106311a37 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ local.properties # Google services +app/src/debug/google-services.json +app/src/internal/google-services.json +app/src/external/google-services.json # Gradle generated files .gradle diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e666baaa4d..0c9d9ac315 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -111,6 +111,8 @@ dependencies { implementation(projects.domain.networks) implementation(projects.domain.quotes) implementation(projects.domain.notifications) + implementation(projects.domain.notifications.models) + implementation(projects.domain.notifications.toggles) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cac33d689c..13f1801543 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -128,7 +128,7 @@ @@ -150,7 +150,29 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index a20134ee4b..4ec5de6632 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit a20134ee4be4d7e1e34c6f44089b3cb0713eacc3 +Subproject commit 4ec5de66321afa82c04104746015ea9e6bc9fe64 diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index ee95e2ba40..52a6dc9646 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -15,6 +15,7 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage @@ -142,4 +143,6 @@ interface ApplicationEntryPoint { fun getOnlineCardVerifier(): OnlineCardVerifier fun getUserWalletBuilderFactory(): UserWalletBuilder.Factory + + fun getApiConfigsManager(): ApiConfigsManager } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 66df0f2d73..9e44bb3dc2 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,16 +19,19 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext +import com.tangem.core.deeplink.DEEPLINK_KEY import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.UiDependencies @@ -48,6 +51,7 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder @@ -67,6 +71,7 @@ import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent 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 dagger.hilt.android.AndroidEntryPoint @@ -171,6 +176,15 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var defaultDeviceFlipDetector: DefaultDeviceFlipDetector + @Inject + internal lateinit var routingFeatureToggle: RoutingFeatureToggle + + @Inject + internal lateinit var deeplinkFactory: DeepLinkFactory + + @Inject + internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -223,9 +237,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() - if (intent != null && savedInstanceState == null) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) { // handle intent only on start, not on recreate - deepLinksRegistry.launch(intent) + handleDeepLink(intent) } lifecycle.addObserver(WindowObscurationObserver) @@ -341,7 +355,10 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { val hasSavedWalletsProvider = { userWalletsListManager.hasUserWallets } intentProcessor.addHandler(OnPushClickedIntentHandler(analyticsEventsHandler)) intentProcessor.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider, lifecycleScope)) - intentProcessor.addHandler(WalletConnectLinkIntentHandler()) + + if (!walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { + intentProcessor.addHandler(WalletConnectLinkIntentHandler()) + } } private fun updateAppTheme(appThemeMode: AppThemeMode) { @@ -374,7 +391,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (intent != null) { - deepLinksRegistry.launch(intent) + handleDeepLink(intent) } } @@ -455,9 +472,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) { + handleDeepLink(intent) + } + viewModel.checkForUnfinishedBackup() } + private fun handleDeepLink(intent: Intent) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled) { + val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() + val receivedDeepLink = intent.data ?: deepLinkExtras ?: return + + deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + } else { + deepLinksRegistry.launch(intent) + } + } + private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index ed0018b6d9..03c39c9727 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -30,6 +30,7 @@ import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig @@ -228,6 +229,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat private val userWalletBuilderFactory: UserWalletBuilder.Factory get() = entryPoint.getUserWalletBuilderFactory() + private val apiConfigsManager: ApiConfigsManager + get() = entryPoint.getApiConfigsManager() + // endregion private val appScope = MainScope() @@ -275,6 +279,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat } fun init() { + apiConfigsManager.initialize() + store = createReduxStore() tangemAppLoggerInitializer.initialize() @@ -285,12 +291,8 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat // We need to initialize the toggles and excludedBlockchainsManager before the MainActivity starts using them. runBlocking { awaitAll( - async { - featureTogglesManager.init() - }, - async { - excludedBlockchainsManager.init() - }, + async { featureTogglesManager.init() }, + async { excludedBlockchainsManager.init() }, ) initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt index fe2add9b71..a90e193a92 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt @@ -104,7 +104,6 @@ sealed class AnalyticsParam { const val PERMISSION_TYPE = "Permission Type" const val PRODUCT_TYPE = "Product Type" const val FIRMWARE = "Firmware" - const val USER_WALLET_ID = "User Wallet ID" const val CURRENCY = "Currency" const val ERROR_DESCRIPTION = "Error Description" const val ERROR_CODE = "Error Code" diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt index 2aa36c5413..d28504b612 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeAnalyticsHandler.kt @@ -1,14 +1,23 @@ package com.tangem.tap.common.analytics.handlers.amplitude import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder class AmplitudeAnalyticsHandler( private val client: AmplitudeAnalyticsClient, -) : AnalyticsHandler { +) : AnalyticsHandler, AnalyticsUserIdHandler { override fun id(): String = ID + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + override fun send(eventId: String, params: Map) { client.logEvent(eventId, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt index 74cec2eaf2..a5ffec919c 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeClient.kt @@ -4,13 +4,14 @@ import android.app.Application import com.amplitude.api.Amplitude import com.amplitude.api.AmplitudeClient import com.tangem.core.analytics.api.EventLogger +import com.tangem.core.analytics.api.UserIdHolder import com.tangem.utils.converter.Converter import org.json.JSONObject /** [REDACTED_AUTHOR] */ -interface AmplitudeAnalyticsClient : EventLogger +interface AmplitudeAnalyticsClient : EventLogger, UserIdHolder internal class AmplitudeClient( application: Application, @@ -24,6 +25,14 @@ internal class AmplitudeClient( client.enableForegroundTracking(application) } + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.setUserId(null) + } + override fun logEvent(event: String, params: Map) { client.logEvent(event, ParamsToJSONObjectConverter().convert(params)) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt index f60d2edfcf..f24a5925a3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/amplitude/AmplitudeLogClient.kt @@ -12,6 +12,16 @@ internal class AmplitudeLogClient( private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(AmplitudeAnalyticsHandler.ID, jsonConverter) + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + } + + override fun clearUserId() { + this.userId = null + } + override fun logEvent(event: String, params: Map) { logger.logEvent(event, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt index 298874dcef..fbea91a9f4 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseAnalyticsHandler.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.handlers.firebase import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.ExceptionAnalyticsEvent import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -10,12 +11,20 @@ import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter class FirebaseAnalyticsHandler( private val client: FirebaseAnalyticsClient, -) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler { +) : AnalyticsHandler, AnalyticsErrorHandler, AnalyticsExceptionHandler, AnalyticsUserIdHandler { private val errorConverter = AnalyticsErrorConverter() override fun id(): String = ID + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + override fun send(eventId: String, params: Map) { client.logEvent(eventId, params) } diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt index 06ccf50709..6268ebf230 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseClient.kt @@ -8,11 +8,12 @@ import com.google.firebase.crashlytics.recordException import com.google.firebase.ktx.Firebase import com.tangem.core.analytics.api.ExceptionLogger import com.tangem.core.analytics.api.EventLogger +import com.tangem.core.analytics.api.UserIdHolder /** [REDACTED_AUTHOR] */ -interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger +interface FirebaseAnalyticsClient : EventLogger, ExceptionLogger, UserIdHolder internal class FirebaseClient : FirebaseAnalyticsClient { @@ -21,6 +22,14 @@ internal class FirebaseClient : FirebaseAnalyticsClient { private val eventConverter = FirebaseAnalyticsEventConverter() + override fun setUserId(userId: String) { + Firebase.analytics.setUserId(userId) + } + + override fun clearUserId() { + Firebase.analytics.setUserId(null) + } + override fun logEvent(event: String, params: Map) { fbAnalytics.logEvent( eventConverter.convertEventName(event), diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt index 4b818bdf51..391b7e7470 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/firebase/FirebaseLogClient.kt @@ -11,6 +11,15 @@ internal class FirebaseLogClient( ) : FirebaseAnalyticsClient { private val logger: AnalyticsEventsLogger = AnalyticsEventsLogger(FirebaseAnalyticsHandler.ID, jsonConverter) + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + } + + override fun clearUserId() { + this.userId = null + } override fun logEvent(event: String, params: Map) { logger.logEvent(event, params) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt index 42a7f46c75..f0f3326917 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt @@ -40,9 +40,6 @@ class CardContextInterceptor( params[AnalyticsParam.BATCH] = card.batchId params[AnalyticsParam.PRODUCT_TYPE] = getProductType() params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue - if (userWalletId != null) { - params[AnalyticsParam.USER_WALLET_ID] = userWalletId.stringValue - } ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let { params[AnalyticsParam.CURRENCY] = it.value diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt index 8e79d07381..d8dc74443d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt @@ -2,6 +2,7 @@ package com.tangem.tap.common.extensions import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor /** @@ -12,6 +13,11 @@ import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterc * Sets the new context */ fun Analytics.setContext(scanResponse: ScanResponse) { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + if (userWalletId != null) { + setUserId(userWalletId.stringValue) + } + addParamsInterceptor(LinkedCardContextInterceptor(scanResponse)) } @@ -19,6 +25,7 @@ fun Analytics.setContext(scanResponse: ScanResponse) { * Erases the context */ fun Analytics.eraseContext() { + clearUserId() removeParamsInterceptor(LinkedCardContextInterceptor.id()) } @@ -26,6 +33,11 @@ fun Analytics.eraseContext() { * Adds a new context and keeps a previous context as the parent of the new one */ fun Analytics.addContext(scanResponse: ScanResponse) { + val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() + if (userWalletId != null) { + setUserId(userWalletId.stringValue) + } + val currentContext = removeParamsInterceptor(LinkedCardContextInterceptor.id()) as? LinkedCardContextInterceptor val newContext = LinkedCardContextInterceptor(scanResponse, parent = currentContext) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt index 448ae3e36a..0396cf84fc 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/String.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -1,34 +1,5 @@ package com.tangem.tap.common.extensions -import android.graphics.Bitmap -import android.graphics.Color -import com.google.zxing.BarcodeFormat -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel -import java.util.Hashtable - -@Suppress("MagicNumber") -fun String.toQrCode(): Bitmap { - val hintMap = Hashtable() - hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage - hintMap[EncodeHintType.MARGIN] = 2 - - val qrCodeWriter = QRCodeWriter() - - val size = 256 - - val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) - for (x in 0 until width) { - for (y in 0 until width) { - bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) - } - } - return bmp -} - fun String.removePrefixOrNull(prefix: String): String? = when { startsWith(prefix) -> substring(prefix.length) else -> null diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index d16033568b..acf3104c3f 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.amountToCreateAccount import com.tangem.common.services.Result -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.tap.common.TestActions import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.domain.TapError diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt index 02a9b8be72..6167cea5a0 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -3,8 +3,9 @@ package com.tangem.tap.data import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await +import javax.inject.Inject -internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { +internal class FirebasePushNotificationsTokenProvider @Inject constructor() : PushNotificationsTokenProvider { override suspend fun getToken(): String { return FirebaseMessaging.getInstance().token.await() } diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index b55ef9a8f7..e32ac8b237 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -10,7 +10,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -70,7 +69,6 @@ internal object ManageTokensDomainModule { customTokensRepository: CustomTokensRepository, walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, derivationsRepository: DerivationsRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, @@ -83,7 +81,6 @@ internal object ManageTokensDomainModule { customTokensRepository = customTokensRepository, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, derivationsRepository = derivationsRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 4889626ff1..408e20a9b6 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -13,7 +13,6 @@ import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.legacy.UserWalletsListManager import dagger.Module @@ -72,7 +71,6 @@ object MarketsDomainModule { derivationsRepository: DerivationsRepository, marketsTokenRepository: MarketsTokenRepository, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -84,7 +82,6 @@ object MarketsDomainModule { derivationsRepository = derivationsRepository, marketsTokenRepository = marketsTokenRepository, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index 090a030029..07c16b3c3f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,9 +1,12 @@ package com.tangem.tap.di.domain +import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.quotes.single.SingleQuoteFetcher +import com.tangem.domain.quotes.single.SingleQuoteSupplier import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository +import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -72,10 +75,11 @@ internal object NFTDomainModule { @Provides @Singleton - fun providesGetNFTNetworkStatusUseCase(networksRepository: NetworksRepository): GetNFTNetworkStatusUseCase = - GetNFTNetworkStatusUseCase( - networksRepository = networksRepository, - ) + fun providesGetNFTNetworkStatusUseCase( + singleNetworkStatusSupplier: SingleNetworkStatusSupplier, + ): GetNFTNetworkStatusUseCase { + return GetNFTNetworkStatusUseCase(singleNetworkStatusSupplier = singleNetworkStatusSupplier) + } @Provides @Singleton @@ -83,4 +87,53 @@ internal object NFTDomainModule { GetNFTExploreUrlUseCase( nftRepository = nftRepository, ) + + @Provides + @Singleton + fun provideGetNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteSupplier: SingleQuoteSupplier, + ): GetNFTPriceUseCase { + return GetNFTPriceUseCase(nftRepository, singleQuoteSupplier) + } + + @Provides + @Singleton + fun provideFetchNFTPriceUseCase( + nftRepository: NFTRepository, + singleQuoteFetcher: SingleQuoteFetcher, + ): FetchNFTPriceUseCase { + return FetchNFTPriceUseCase(nftRepository, singleQuoteFetcher) + } + + @Provides + @Singleton + fun provideEnableWalletNFTUseCase(walletsRepository: WalletsRepository): EnableWalletNFTUseCase { + return EnableWalletNFTUseCase(walletsRepository) + } + + @Provides + @Singleton + fun provideDisableWalletNFTUseCase( + walletsRepository: WalletsRepository, + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): DisableWalletNFTUseCase { + return DisableWalletNFTUseCase(walletsRepository, nftRepository, currenciesRepository) + } + + @Provides + @Singleton + fun provideGetWalletNFTEnabledUseCase(walletsRepository: WalletsRepository): GetWalletNFTEnabledUseCase { + return GetWalletNFTEnabledUseCase(walletsRepository) + } + + @Provides + @Singleton + fun provideClearNFTCacheUseCase( + nftRepository: NFTRepository, + currenciesRepository: CurrenciesRepository, + ): ObserveAndClearNFTCacheIfNeedUseCase { + return ObserveAndClearNFTCacheIfNeedUseCase(nftRepository, currenciesRepository) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index c6f217e01c..14df43efb0 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -1,10 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.notifications.GetApplicationIdUseCase import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles +import com.tangem.tap.domain.notifications.DefaultNotificationsFeatureToggles import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.Module import dagger.Provides @@ -28,12 +31,10 @@ internal object NotificationsDomainModule { @Singleton fun providesSendPushTokenUseCase( notificationsRepository: NotificationsRepository, - getApplicationIdUseCase: GetApplicationIdUseCase, pushNotificationsTokenProvider: PushNotificationsTokenProvider, ): SendPushTokenUseCase { return SendPushTokenUseCase( notificationsRepository = notificationsRepository, - getApplicationIdUseCase = getApplicationIdUseCase, pushNotificationsTokenProvider = pushNotificationsTokenProvider, ) } @@ -57,4 +58,10 @@ internal object NotificationsDomainModule { notificationsRepository = notificationsRepository, ) } + + @Provides + @Singleton + fun provideNotificationsFeatureToggles(featureTogglesManager: FeatureTogglesManager): NotificationsFeatureToggles { + return DefaultNotificationsFeatureToggles(featureTogglesManager = featureTogglesManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ff23a2c9e2..c678251ad9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -4,6 +4,7 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.promo.PromoRepository @@ -18,7 +19,10 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.* +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.tokens.repository.PolkadotAccountHealthCheckRepository +import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.tap.domain.tokens.DefaultTokensFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -37,7 +41,6 @@ internal object TokensDomainModule { @Singleton fun provideAddCryptoCurrenciesUseCase( currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, @@ -47,7 +50,6 @@ internal object TokensDomainModule { ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, stakingRepository = stakingRepository, quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -62,7 +64,6 @@ internal object TokensDomainModule { fun provideFetchTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -71,7 +72,6 @@ internal object TokensDomainModule { ): FetchTokenListUseCase { return FetchTokenListUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -121,8 +121,8 @@ internal object TokensDomainModule { fun provideGetCurrencyUseCase( baseCurrencyStatusOperations: BaseCurrencyStatusOperations, dispatchers: CoroutineDispatcherProvider, - ): GetCurrencyStatusUpdatesUseCase { - return GetCurrencyStatusUpdatesUseCase( + ): GetSingleCryptoCurrencyStatusUseCase { + return GetSingleCryptoCurrencyStatusUseCase( currencyStatusOperations = baseCurrencyStatusOperations, dispatchers = dispatchers, ) @@ -147,7 +147,6 @@ internal object TokensDomainModule { fun provideGetCurrencyWarningsUseCase( walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, currencyChecksRepository: CurrencyChecksRepository, dispatchers: CoroutineDispatcherProvider, baseCurrencyStatusOperations: BaseCurrencyStatusOperations, @@ -155,31 +154,17 @@ internal object TokensDomainModule { return GetCurrencyWarningsUseCase( walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, + dispatchers = dispatchers, currencyChecksRepository = currencyChecksRepository, - dispatchers = dispatchers, currencyStatusOperations = baseCurrencyStatusOperations, ) } - @Provides - @Singleton - fun provideGetPrimaryCurrencyUseCase( - currencyStatusOperations: BaseCurrencyStatusOperations, - dispatchers: CoroutineDispatcherProvider, - ): GetPrimaryCurrencyStatusUpdatesUseCase { - return GetPrimaryCurrencyStatusUpdatesUseCase( - currencyStatusOperations = currencyStatusOperations, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideFetchCurrencyStatusUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -188,7 +173,6 @@ internal object TokensDomainModule { ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, @@ -203,7 +187,6 @@ internal object TokensDomainModule { fun provideFetchCardTokenListUseCase( currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, @@ -212,7 +195,6 @@ internal object TokensDomainModule { ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( currenciesRepository = currenciesRepository, - networksRepository = networksRepository, quotesRepository = quotesRepository, stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, @@ -222,14 +204,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun providesGetCryptoCurrencyStatusSyncUseCase( - currencyStatusOperations: BaseCurrencyStatusOperations, - ): GetCryptoCurrencyStatusSyncUseCase { - return GetCryptoCurrencyStatusSyncUseCase(currencyStatusOperations) - } - @Provides @Singleton fun provideGetCryptoCurrencyUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrencyUseCase { @@ -328,14 +302,10 @@ internal object TokensDomainModule { @Provides @Singleton fun provideUpdateDelayedCurrencyStatusUseCase( - networksRepository: NetworksRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): UpdateDelayedNetworkStatusUseCase { return UpdateDelayedNetworkStatusUseCase( - networksRepository = networksRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -413,7 +383,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, quotesRepositoryV2: QuotesRepositoryV2, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -427,7 +396,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, quotesRepositoryV2 = quotesRepositoryV2, - networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, @@ -447,7 +415,6 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, quotesRepositoryV2: QuotesRepositoryV2, - networksRepository: NetworksRepository, stakingRepository: StakingRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -461,7 +428,6 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, quotesRepositoryV2 = quotesRepositoryV2, - networksRepository = networksRepository, stakingRepository = stakingRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, multiNetworkStatusSupplier = multiNetworkStatusSupplier, diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index b0b9e8f8ad..ea48365cb7 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -4,9 +4,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.DemoConfig import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* @@ -43,7 +41,6 @@ internal object TransactionDomainModule { transactionRepository: TransactionRepository, walletManagersFacade: WalletManagersFacade, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - tokensFeatureToggles: TokensFeatureToggles, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig(), @@ -51,7 +48,6 @@ internal object TransactionDomainModule { transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - tokensFeatureToggles = tokensFeatureToggles, ) } @@ -61,17 +57,13 @@ internal object TransactionDomainModule { cardSdkConfigRepository: CardSdkConfigRepository, walletManagersFacade: WalletManagersFacade, currenciesRepository: CurrenciesRepository, - networksRepository: NetworksRepository, singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - tokensFeatureToggles: TokensFeatureToggles, ): AssociateAssetUseCase { return AssociateAssetUseCase( cardSdkConfigRepository = cardSdkConfigRepository, walletManagersFacade = walletManagersFacade, currenciesRepository = currenciesRepository, - networksRepository = networksRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, - tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index 4096521b75..1fbf5cb42e 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -6,6 +6,8 @@ import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletMemoUseCase import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.delegate.DefaultUserWalletsSyncDelegate +import com.tangem.domain.wallets.delegate.UserWalletsSyncDelegate import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository @@ -28,6 +30,17 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object WalletsDomainModule { + @Provides + fun providesUserWalletsSyncDelegate( + userWalletsListManager: UserWalletsListManager, + dispatchers: CoroutineDispatcherProvider, + ): UserWalletsSyncDelegate { + return DefaultUserWalletsSyncDelegate( + userWalletsListManager = userWalletsListManager, + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun providesGetWalletsUseCase(userWalletsListManager: UserWalletsListManager): GetWalletsUseCase { @@ -102,10 +115,13 @@ internal object WalletsDomainModule { @Provides @Singleton fun providesRenameWalletUseCase( - userWalletsListManager: UserWalletsListManager, - dispatchers: CoroutineDispatcherProvider, + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, ): RenameWalletUseCase { - return RenameWalletUseCase(userWalletsListManager = userWalletsListManager, dispatchers = dispatchers) + return RenameWalletUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) } @Provides @@ -197,4 +213,36 @@ internal object WalletsDomainModule { nftFeatureToggles = nftFeatureToggles, ) } + + @Provides + @Singleton + fun providesUpdateRemoteWalletsInfoUseCase( + walletsRepository: WalletsRepository, + userWalletsSyncDelegate: UserWalletsSyncDelegate, + ): UpdateRemoteWalletsInfoUseCase { + return UpdateRemoteWalletsInfoUseCase( + walletsRepository = walletsRepository, + userWalletsSyncDelegate = userWalletsSyncDelegate, + ) + } + + @Provides + @Singleton + fun providesGetSavedWalletChangesIdUseCase( + userWalletsListManager: UserWalletsListManager, + ): GetSavedWalletChangesUseCase { + return GetSavedWalletChangesUseCase( + userWalletsListManager = userWalletsListManager, + ) + } + + @Provides + @Singleton + fun providesAssociateWalletsWithApplicationIdUseCase( + walletsRepository: WalletsRepository, + ): AssociateWalletsWithApplicationIdUseCase { + return AssociateWalletsWithApplicationIdUseCase( + walletsRepository = walletsRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt index 3bfc0b3a44..a95acf5cd6 100644 --- a/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt +++ b/app/src/main/java/com/tangem/tap/di/routing/AppRouterModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.routing import com.tangem.common.routing.AppRouter +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.tap.routing.ProxyAppRouter import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.configurator.MutableAppRouterConfig @@ -31,4 +33,10 @@ internal object AppRouterModule { @Provides @Singleton fun provideAppRouterConfigurator(): AppRouterConfig = MutableAppRouterConfig() + + @Provides + @Singleton + fun provideRoutingFeatureToggle(featureTogglesManager: FeatureTogglesManager): RoutingFeatureToggle { + return RoutingFeatureToggle(featureTogglesManager) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt index 9b04ab7d07..c41b7b37f0 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultDerivationsRepository.kt @@ -15,9 +15,9 @@ import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.card.BackendId import com.tangem.domain.card.repository.DerivationsRepository +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.operations.derivation.ExtendedPublicKeysMap @@ -41,7 +41,7 @@ internal class DefaultDerivationsRepository( derivePublicKeysByNetworks(userWalletId = userWalletId, networks = currencies.map(CryptoCurrency::network)) } - override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { + override suspend fun derivePublicKeysByNetworkIds(userWalletId: UserWalletId, networkIds: List) { val userWallet = userWalletsStore.getSyncOrNull(userWalletId) ?: error("User wallet not found") derivePublicKeysByNetworks( diff --git a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt index af980fd382..d05c693542 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/MissedDerivationsFinder.kt @@ -8,10 +8,10 @@ import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.configs.CardConfig import com.tangem.domain.common.util.derivationStyleProvider +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network import com.tangem.operations.derivation.ExtendedPublicKeysMap private typealias DerivationData = Pair> @@ -49,7 +49,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { private fun List.mapToNewDerivations(): List { val config = CardConfig.createConfig(scanResponse.card) return mapNotNull { network -> - val blockchain = Blockchain.fromId(id = network.id.value) + val blockchain = Blockchain.fromId(id = network.rawId) val curve = config.primaryCurve(blockchain) ?: return@mapNotNull null findNewDerivations(curve = curve, scanResponse = scanResponse, network = network) @@ -74,7 +74,7 @@ internal class MissedDerivationsFinder(private val scanResponse: ScanResponse) { } private fun Network.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = Blockchain.fromId(id = this.id.value) + val blockchain = Blockchain.fromId(id = rawId) return buildList { add(blockchain.getDerivationPath(curve = curve)) diff --git a/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt new file mode 100644 index 0000000000..5a58923bb9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/notifications/DefaultNotificationsFeatureToggles.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.domain.notifications + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles + +internal class DefaultNotificationsFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : NotificationsFeatureToggles { + override val isNotificationsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("PUSH_NOTIFICATIONS_ENABLED") +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 1fa9dc2b67..758979cd91 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -2,7 +2,6 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.Either import arrow.core.getOrElse -import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult @@ -14,22 +13,18 @@ import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.datasource.local.visa.VisaOTPStorage import com.tangem.datasource.local.visa.VisaOtpData import com.tangem.datasource.local.visa.hasSavedOTP -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.visa.error.VisaActivationError -import com.tangem.domain.visa.error.VisaAuthorizationAPIError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.GenerateOTPCommand import com.tangem.operations.attestation.AttestCardKeyCommand -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask import com.tangem.operations.pins.SetUserCodeCommand import com.tangem.operations.sign.SignHashCommand import com.tangem.operations.sign.SignHashResponse @@ -95,23 +90,7 @@ class VisaCardActivationTask @AssistedInject constructor( context.signAuthorizationChallenge(mode.authorizationChallenge) } is VisaCardActivationTaskMode.SignOnly -> { - val wallet = - card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } - ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - - val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - context.signData( - mode.dataToSignByCardWallet, - derivedPublicKey, - ) + context.signData(mode.dataToSignByCardWallet) } } } @@ -164,16 +143,7 @@ class VisaCardActivationTask @AssistedInject constructor( card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } .value @@ -190,10 +160,7 @@ class VisaCardActivationTask @AssistedInject constructor( otpTaskDeferred.await() - signData( - dataToSign = dataToSign, - derivedPublicKey = derivedPublicKey, - ) + signData(dataToSign = dataToSign) } } @@ -201,28 +168,24 @@ class VisaCardActivationTask @AssistedInject constructor( signedChallenge: VisaAuthSignedChallenge, cardWalletAddress: String, ): Either = either { - catch( - block = { - val tokens = visaAuthRepository.getAccessTokens(signedChallenge) + val tokens = visaAuthRepository.getAccessTokens(signedChallenge) + .getOrElse { raise(it.tangemError) } - visaAuthTokenStorage.store(cardId, tokens) + visaAuthTokenStorage.store(cardId, tokens) - val remoteState = visaActivationRepository.getActivationRemoteState() - if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { - raise(VisaActivationError.WrongRemoteState.tangemError) - } + val remoteState = visaActivationRepository.getActivationRemoteState() + .getOrElse { raise(it.tangemError) } - visaActivationRepository.getCardWalletAcceptanceData( - VisaCardWalletDataToSignRequest( - activationOrderInfo = remoteState.activationOrderInfo, - cardWalletAddress = cardWalletAddress, - ), - ) - }, - catch = { - raise(VisaAuthorizationAPIError.tangemError) - }, - ) + if (remoteState !is VisaActivationRemoteState.CardWalletSignatureRequired) { + return raise(VisaActivationError.WrongRemoteState.tangemError) + } + + visaActivationRepository.getCardWalletAcceptanceData( + VisaCardWalletDataToSignRequest( + activationOrderInfo = remoteState.activationOrderInfo, + cardWalletAddress = cardWalletAddress, + ), + ).getOrElse { raise(it.tangemError) } } private suspend fun SessionContext.createWallet(): CompletionResult { @@ -294,7 +257,6 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) @@ -306,7 +268,6 @@ class VisaCardActivationTask @AssistedInject constructor( val task = SignHashCommand( hash = dataToSign.hashToSign.hexToBytes(), walletPublicKey = wallet.publicKey, - derivationPath = VisaUtilities.visaDefaultDerivationPath, ) val timedResult = RealtimeMonotonicTimeSource.measureTimedValue { @@ -325,7 +286,7 @@ class VisaCardActivationTask @AssistedInject constructor( handleSignedData( dataToSign = dataToSign, response = result.data, - derivedPublicKey = derivedPublicKey, + walletPublicKey = wallet.publicKey, ) } is CompletionResult.Failure -> { @@ -335,23 +296,9 @@ class VisaCardActivationTask @AssistedInject constructor( } } - private suspend fun SessionContext.deriveKey(publicKey: ByteArray): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath - ?: return CompletionResult.Failure(VisaActivationError.FailedToCreateAddress.tangemError) - - val derivationTask = DeriveWalletPublicKeyTask(publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - - return derivationTaskResult - } - private suspend fun SessionContext.handleSignedData( dataToSign: VisaDataToSignByCardWallet, - derivedPublicKey: ExtendedPublicKey, + walletPublicKey: ByteArray, response: SignHashResponse, ): CompletionResult { val otp = otpStorage.getOTP(cardId) ?: run { @@ -362,7 +309,7 @@ class VisaCardActivationTask @AssistedInject constructor( val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( signature = response.signature, hash = dataToSign.hashToSign.hexToBytes(), - publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), + publicKey = walletPublicKey.toDecompressedPublicKey(), ).asRSVLegacyEVM().toHexString().lowercase() val signedActivationData = dataToSign.sign( diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 5b892c93ed..dc7b40ea9b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -40,17 +40,12 @@ class VisaCustomerWalletApproveTask( } if (VisaUtilities.isVisaCard(card.firmwareVersion.doubleValue, card.batchId)) { - // TODO TVF-21 - callback(CompletionResult.Failure(TangemSdkError.Underlying("Can't use Visa card for approve"))) + callback(CompletionResult.Failure(VisaActivationError.VisaCardForApproval.tangemError)) return } if (visaDataForApprove.customerWalletCardId != null && card.cardId != visaDataForApprove.customerWalletCardId) { - callback( - CompletionResult.Failure( - TangemSdkError.Underlying("Use tangem wallet specified during visa registration"), // TODO TVF-21 - ), - ) + callback(CompletionResult.Failure(VisaActivationError.CardIdNotMatched.tangemError)) return } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index adc8e80741..bfb76fbf06 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -7,9 +7,6 @@ internal class DefaultTokensFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : TokensFeatureToggles { - override val isNetworksLoadingRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NETWORKS_LOADING_REFACTORING_ENABLED") - override val isQuotesLoadingRefactoringEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "QUOTES_LOADING_REFACTORING_ENABLED") diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index b83d23f8d9..46a8dadc5e 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -2,29 +2,24 @@ package com.tangem.tap.domain.visa import arrow.core.getOrElse import com.tangem.common.CompletionResult -import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.datasource.local.visa.VisaAuthTokenStorage -import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.error.VisaActivationError -import com.tangem.domain.visa.error.VisaAuthorizationAPIError +import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.error.VisaCardScanError import com.tangem.domain.visa.model.* import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.operations.attestation.AttestCardKeyCommand import com.tangem.operations.attestation.AttestCardKeyResponse -import com.tangem.operations.derivation.DeriveWalletPublicKeyTask -import com.tangem.operations.sign.SignHashCommand -import com.tangem.operations.sign.SignHashResponse +import com.tangem.operations.attestation.AttestWalletKeyResponse +import com.tangem.operations.attestation.AttestWalletKeyTask import kotlinx.coroutines.suspendCancellableCoroutine import timber.log.Timber import javax.inject.Inject @@ -61,54 +56,19 @@ internal class VisaCardScanHandler @Inject constructor( session = session, ) - val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { val activationInput = VisaActivationInput(card.cardId, card.cardPublicKey.toHexString(), card.isAccessCodeSet) val activationStatus = VisaCardActivationStatus.NotStartedActivation(activationInput) return CompletionResult.Success(activationStatus) } - return context.deriveKey(wallet) - } - - private suspend fun SessionContext.deriveKey(wallet: CardWallet): CompletionResult { - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while first scan") - - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - - val derivationTask = DeriveWalletPublicKeyTask(wallet.publicKey, derivationPath) - val derivationTaskResult = suspendCancellableCoroutine { continuation -> - derivationTask.run(session) { result -> - continuation.resume(result) - } - } - return handleDerivationResponse(derivationTaskResult) - } - - private suspend fun SessionContext.handleDerivationResponse( - result: CompletionResult, - ): CompletionResult { - return when (result) { - is CompletionResult.Success -> { - Timber.i("Start task for loading challenge for Visa wallet") - handleWalletAuthorization() - } - is CompletionResult.Failure -> { - CompletionResult.Failure(result.error) - } - } + return context.handleWalletAuthorization() } private suspend fun SessionContext.handleWalletAuthorization(): CompletionResult { Timber.i("Started handling authorization using Visa wallet") - val derivationPath = VisaUtilities.visaDefaultDerivationPath ?: run { - Timber.e("Failed to create derivation path while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToCreateDerivationPath.tangemError) - } - val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: run { @@ -116,43 +76,35 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Failure(VisaCardScanError.FailedToFindWallet.tangemError) } - val extendedPublicKey = wallet.derivedKeys[derivationPath] ?: run { - Timber.e("Failed to find extended public key while handling wallet authorization") - return CompletionResult.Failure(VisaCardScanError.FailedToFindDerivedWalletKey.tangemError) - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(extendedPublicKey.publicKey) + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(wallet.publicKey) .getOrElse { return CompletionResult.Failure(it.tangemError) } Timber.i("Requesting challenge for wallet authorization") - val challengeResponse = runCatching { - // TODO [REDACTED_TASK_KEY] - error("sign and get specific error to switch to card_id flow") - visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) - }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) - return handleCardAuthorization( - cardWalletAddress = walletAddress.value, - ) - } + + val challengeResponse = visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) + .getOrElse { + Timber.i("Failed to get Access token for Wallet public key authorization") + return CompletionResult.Failure(it.tangemError) + } val signChallengeResult = signChallengeWithWallet( publicKey = wallet.publicKey, - derivationPath = derivationPath, nonce = challengeResponse.challenge, ) return when (signChallengeResult) { is CompletionResult.Success -> { + val signature = signChallengeResult.data.cardSignature ?: run { + Timber.i("Failed to sign challenge with Wallet public key") + return CompletionResult.Failure(VisaCardScanError.FailedToSignChallenge.tangemError) + } + Timber.i("Challenge signed with Wallet public key") handleWalletAuthorizationTokens( cardWalletAddress = walletAddress.value, - signedChallenge = challengeResponse - .toSignedChallenge(signChallengeResult.data.signature.toHexString()), + signedChallenge = challengeResponse.toSignedChallenge(signature.toHexString()), ) } is CompletionResult.Failure -> { @@ -166,16 +118,19 @@ internal class VisaCardScanHandler @Inject constructor( cardWalletAddress: String, signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { - val authorizationTokensResponse = runCatching { - visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) - }.getOrElse { - Timber.i( - "Failed to get Access token for Wallet public key authoziation. Authorizing using Card Pub key", - ) - return handleCardAuthorization( - cardWalletAddress = cardWalletAddress, - ) - } + val authorizationTokensResponse = visaAuthRepository.getAccessTokens(signedChallenge = signedChallenge) + .getOrElse { + Timber.i("Failed to get Access token for Wallet public key authorization.") + return if ( + it is VisaApiError.ProductInstanceIsNotActivated || + it is VisaApiError.ProductInstanceNotFoundActivationRequired + ) { + Timber.i("Proceeding with card authorization.") + handleCardAuthorization(cardWalletAddress = cardWalletAddress) + } else { + CompletionResult.Failure(it.tangemError) + } + } Timber.i("Authorized using Wallet public key successfully") @@ -190,14 +145,12 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting authorization challenge to sign") - val challengeResponse = runCatching { - visaAuthRepository.getCardAuthChallenge( - cardId = card.cardId, - cardPublicKey = card.cardPublicKey.toHexString(), - ) - }.getOrElse { - Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.message}") - return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) + val challengeResponse = visaAuthRepository.getCardAuthChallenge( + cardId = card.cardId, + cardPublicKey = card.cardPublicKey.toHexString(), + ).getOrElse { + Timber.e("Failed to get challenge for Card authorization. Plain error: ${it.errorCode}") + return CompletionResult.Failure(it.tangemError) } Timber.i("Received challenge to sign: ${challengeResponse.challenge}") @@ -217,17 +170,14 @@ internal class VisaCardScanHandler @Inject constructor( } } - @Suppress("UnusedPrivateMember") - val authorizationTokensResponse = runCatching { - visaAuthRepository.getAccessTokens( - signedChallenge = challengeResponse.toSignedChallenge( - signedChallenge = attestCardKeyResponse.cardSignature.toHexString(), - salt = attestCardKeyResponse.salt.toHexString(), - ), - ) - }.getOrElse { - Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}") - return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) + val authorizationTokensResponse = visaAuthRepository.getAccessTokens( + signedChallenge = challengeResponse.toSignedChallenge( + signedChallenge = attestCardKeyResponse.cardSignature.toHexString(), + salt = attestCardKeyResponse.salt.toHexString(), + ), + ).getOrElse { + Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}") + return CompletionResult.Failure(it.tangemError) } visaAuthTokenStorage.store( @@ -235,11 +185,9 @@ internal class VisaCardScanHandler @Inject constructor( tokens = authorizationTokensResponse, ) - val activationRemoteState = runCatching { - visaActivationRepository.getActivationRemoteState() - }.getOrElse { - Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}") - return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) + val activationRemoteState = visaActivationRepository.getActivationRemoteState().getOrElse { + Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.errorCode}") + return CompletionResult.Failure(it.tangemError) } val error = when (activationRemoteState) { @@ -271,13 +219,11 @@ internal class VisaCardScanHandler @Inject constructor( private suspend fun SessionContext.signChallengeWithWallet( publicKey: ByteArray, - derivationPath: DerivationPath, nonce: String, - ): CompletionResult { - val signHashCommand = SignHashCommand( - hash = nonce.hexToBytes(), - walletPublicKey = publicKey, - derivationPath = derivationPath, + ): CompletionResult { + val signHashCommand = AttestWalletKeyTask( + publicKey = publicKey, + challenge = nonce.hexToBytes(), ) return suspendCancellableCoroutine { signHashCommand.run(session) { result -> diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 689db8c1c0..0ac30a5cb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -64,14 +65,18 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( if (isNewWc) stub.updateSessions() else legacy.updateSessions() } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { val src = when (source) { SourceType.QR -> WcPairRequest.Source.QR SourceType.DEEPLINK -> WcPairRequest.Source.DEEPLINK SourceType.CLIPBOARD -> WcPairRequest.Source.CLIPBOARD SourceType.ETC -> WcPairRequest.Source.ETC } - if (isNewWc) wcPairService.pair(WcPairRequest(uri, src)) else legacy.pair(uri, source) + if (isNewWc) { + wcPairService.pair(WcPairRequest(uri = uri, source = src, userWalletId = userWalletId)) + } else { + legacy.pair(userWalletId = userWalletId, uri = uri, source = source) + } } override fun disconnect(topic: String) { @@ -110,7 +115,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository override fun updateSessions() = Unit - override fun pair(uri: String, source: SourceType) = Unit + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) = Unit override fun disconnect(topic: String) = Unit @@ -405,7 +410,7 @@ internal class DefaultLegacyWalletConnectRepository( this.userNamespaces = userNamespaces } - override fun pair(uri: String, source: SourceType) { + override fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) { analyticsHandler.send(WalletConnect.NewSessionInitiated(source = source)) WalletKit.pair( params = Wallet.Params.Pair(uri), diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 59d5954e00..c0e22a3b7d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.walletconnect2.domain import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.models.* import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.OpenSession.SourceType import kotlinx.coroutines.flow.Flow @@ -19,7 +20,7 @@ interface LegacyWalletConnectRepository { fun updateSessions() - fun pair(uri: String, source: SourceType) + fun pair(userWalletId: UserWalletId, uri: String, source: SourceType) fun disconnect(topic: String) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index d8d5a989ae..a9009dc947 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -3,8 +3,8 @@ package com.tangem.tap.domain.walletconnect2.domain import arrow.core.flatten import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toNetworkId -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.Network +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.legacy.Account import com.tangem.domain.walletconnect.model.legacy.Session @@ -12,6 +12,7 @@ import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsReposit import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull @@ -139,7 +140,11 @@ class WalletConnectInteractor( if (deeplinkStack.empty()) return val lastDeeplink = deeplinkStack.pop() val action = WalletConnectAction - .OpenSession(lastDeeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + .OpenSession( + wcUri = lastDeeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) }.onFailure { Timber.e("WC deeplink handling failed. $it") @@ -393,7 +398,11 @@ class WalletConnectInteractor( } if (isWalletConnectReadyForDeepLinks) { - val action = WalletConnectAction.OpenSession(deeplink, WalletConnectAction.OpenSession.SourceType.DEEPLINK) + val action = WalletConnectAction.OpenSession( + wcUri = deeplink, + source = WalletConnectAction.OpenSession.SourceType.DEEPLINK, + userWalletId = UserWalletId(userWalletId), + ) store.dispatchOnMain(action) } else { deeplinkStack.push(deeplink) @@ -410,7 +419,7 @@ class WalletConnectInteractor( private suspend fun getAccountsForWc(userWallet: UserWallet, networks: List): List { val walletManagers = networks.mapNotNull { - val blockchain = Blockchain.fromId(it.id.value) + val blockchain = Blockchain.fromId(it.rawId) walletManagersFacade.getOrCreateWalletManager( userWalletId = userWallet.walletId, blockchain = blockchain, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index 7391c77f5b..4288f711b9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.details.redux.walletconnect +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.walletconnect2.domain.WcPreparedRequest import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectError import com.tangem.tap.domain.walletconnect2.domain.models.WalletConnectEvents @@ -14,6 +15,7 @@ sealed class WalletConnectAction : Action { data class OpenSession( val wcUri: String, val source: SourceType, + val userWalletId: UserWalletId, ) : WalletConnectAction() { enum class SourceType { QR, DEEPLINK, CLIPBOARD, ETC } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index 6bbd548de4..7439241a50 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -63,7 +63,11 @@ class WalletConnectMiddleware { is WalletConnectAction.OpenSession -> { val index = action.wcUri.indexOf("@") when (action.wcUri[index + 1]) { - '2' -> walletConnectRepository.pair(uri = action.wcUri, source = action.source) + '2' -> walletConnectRepository.pair( + uri = action.wcUri, + source = action.source, + userWalletId = action.userWalletId, + ) '1' -> { store.dispatchOnMain(WalletConnectAction.UnsupportedDappRequest) store.dispatchOnMain( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt index 73a0989519..0974e0db1b 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/DefaultWalletConnectComponent.kt @@ -22,10 +22,10 @@ import org.rekotlin.StoreSubscriber @Suppress("UnusedPrivateMember") internal class DefaultWalletConnectComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Assisted params: WalletConnectComponent.Params, ) : WalletConnectComponent, AppComponentContext by appComponentContext, StoreSubscriber { - private val model: WalletConnectModel = getOrCreateModel() + private val model: WalletConnectModel = getOrCreateModel(params) private var screenState: MutableState = mutableStateOf(model.updateState(store.state.walletConnectState)) @@ -65,6 +65,9 @@ internal class DefaultWalletConnectComponent @AssistedInject constructor( @AssistedFactory interface Factory : WalletConnectComponent.Factory { - override fun create(context: AppComponentContext, params: Unit): DefaultWalletConnectComponent + override fun create( + context: AppComponentContext, + params: WalletConnectComponent.Params, + ): DefaultWalletConnectComponent } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt index 21deae59a7..762b7afb71 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectModel.kt @@ -4,10 +4,12 @@ import androidx.compose.runtime.Stable import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState +import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList @@ -22,13 +24,22 @@ import javax.inject.Inject internal class WalletConnectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + paramsContainer: ParamsContainer, ) : Model() { + private val params = paramsContainer.require() + init { modelScope.launch { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .map { WalletConnectAction.OpenSession(it, WalletConnectAction.OpenSession.SourceType.QR) } + .map { + WalletConnectAction.OpenSession( + wcUri = it, + source = WalletConnectAction.OpenSession.SourceType.QR, + userWalletId = params.userWalletId, + ) + } .collect { store.dispatch(it) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt index 4e1f4ab3fb..6b9d8c4330 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/api/WalletConnectComponent.kt @@ -2,7 +2,9 @@ package com.tangem.tap.features.details.ui.walletconnect.api import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.wallets.models.UserWalletId interface WalletConnectComponent : ComposableContentComponent { - interface Factory : ComponentFactory + data class Params(val userWalletId: UserWalletId) + interface Factory : ComponentFactory } \ 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 c00ae2518f..7dee2ef45c 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 @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.tangem.blockchainsdk.BlockchainSDKFactory import com.tangem.common.keyboard.KeyboardValidator +import com.tangem.common.routing.RoutingFeatureToggle import com.tangem.core.analytics.Analytics import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.TechAnalyticsEvent @@ -22,6 +23,10 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.notifications.GetApplicationIdUseCase +import com.tangem.domain.notifications.SendPushTokenUseCase +import com.tangem.domain.notifications.models.ApplicationId +import com.tangem.domain.notifications.toggles.NotificationsFeatureToggles import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase @@ -31,6 +36,9 @@ import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.usecase.AssociateWalletsWithApplicationIdUseCase +import com.tangem.domain.wallets.usecase.GetSavedWalletChangesUseCase +import com.tangem.domain.wallets.usecase.UpdateRemoteWalletsInfoUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.features.onramp.deeplink.OnrampDeepLink import com.tangem.tap.common.extensions.setContext @@ -38,11 +46,15 @@ import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.wallet.BuildConfig import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout import timber.log.Timber import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @Suppress("LongParameterList") @HiltViewModel @@ -56,7 +68,6 @@ internal class MainViewModel @Inject constructor( private val userWalletsListManager: UserWalletsListManager, private val dispatchers: CoroutineDispatcherProvider, private val fetchStakingTokensUseCase: FetchStakingTokensUseCase, - private val apiConfigsManager: ApiConfigsManager, private val fetchUserCountryUseCase: FetchUserCountryUseCase, @GlobalUiMessageSender private val messageSender: UiMessageSender, private val keyboardValidator: KeyboardValidator, @@ -67,6 +78,14 @@ internal class MainViewModel @Inject constructor( private val onboardingRepository: OnboardingRepository, private val deepLinksRegistry: DeepLinksRegistry, private val onrampDeepLinkFactory: OnrampDeepLink.Factory, + private val notificationsToggles: NotificationsFeatureToggles, + private val getApplicationIdUseCase: GetApplicationIdUseCase, + private val subscribeOnWalletsUseCase: GetSavedWalletChangesUseCase, + private val associateWalletsWithApplicationIdUseCase: AssociateWalletsWithApplicationIdUseCase, + private val updateRemoteWalletsInfoUseCase: UpdateRemoteWalletsInfoUseCase, + private val sendPushTokenUseCase: SendPushTokenUseCase, + private val apiConfigsManager: ApiConfigsManager, + routingFeatureToggle: RoutingFeatureToggle, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -77,32 +96,38 @@ internal class MainViewModel @Inject constructor( private set init { + /** + * Run any data initialization here that needs to happen before the app starts + * and is hidden behind the SplashScreen + */ loadApplicationResources() - viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() } + /** Run any API data load here that runs in parallel and does not block the app from starting */ + launchAPIRequests { + launch { fetchHotCryptoUseCase() } - viewModelScope.launch { - fetchUserCountryUseCase().onLeft { - Timber.e("Unable to fetch the user country code $it") - } + launch { fetchAppCurrenciesUseCase() } + + launch { fetchStakingTokens() } + + launch { initPushNotifications() } } - viewModelScope.launch { fetchHotCryptoUseCase() } + viewModelScope.launch { incrementAppLaunchCounterUseCase() } - updateAppCurrencies() observeFlips() displayBalancesHidingStatusToast() displayHiddenBalancesModalNotification() - fetchStakingTokens() - deleteDeprecatedLogsUseCase() sendKeyboardIdentifierEvent() preloadImages() - initializeDeepLinks() + if (!routingFeatureToggle.isDeepLinkNavigationEnabled) { + initializeDeepLinks() + } } fun checkForUnfinishedBackup() { @@ -114,16 +139,41 @@ internal class MainViewModel @Inject constructor( /** Loading the resources needed to run the application */ private fun loadApplicationResources() { - viewModelScope.launch(dispatchers.main) { - apiConfigsManager.initialize() + viewModelScope.launch { + launchAPIRequests { + launch { blockchainSDKFactory.init() } + + launch { + withTimeout(timeMillis = 1.seconds.inWholeMilliseconds) { fetchUserCountry() } + } + } - blockchainSDKFactory.init() prepareSelectedWalletFeedback() isSplashScreenShown = false } } + private suspend fun fetchUserCountry() { + fetchUserCountryUseCase().onLeft { + Timber.e("Unable to fetch the user country code $it") + } + } + + private fun launchAPIRequests(function: suspend CoroutineScope.() -> Unit) { + viewModelScope.launch { + if (BuildConfig.TESTER_MENU_ENABLED) { + apiConfigsManager.isInitialized + .filter { it } + .first() // wait until isInitialized becomes true + + function() + } else { + function() + } + } + } + private fun prepareSelectedWalletFeedback() { userWalletsListManager.selectedUserWallet .distinctUntilChanged() @@ -134,18 +184,10 @@ internal class MainViewModel @Inject constructor( .launchIn(viewModelScope) } - private fun updateAppCurrencies() { - viewModelScope.launch(dispatchers.main) { - fetchAppCurrenciesUseCase.invoke() - } - } - - private fun fetchStakingTokens() { - viewModelScope.launch(dispatchers.main) { - fetchStakingTokensUseCase() - .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } - .onRight { Timber.d("Staking token list was fetched successfully") } - } + private suspend fun fetchStakingTokens() { + fetchStakingTokensUseCase() + .onLeft { Timber.e(it.toString(), "Unable to fetch the staking tokens list") } + .onRight { Timber.d("Staking token list was fetched successfully") } } private fun observeFlips() { @@ -343,4 +385,20 @@ internal class MainViewModel @Inject constructor( private fun initializeDeepLinks() { deepLinksRegistry.register(onrampDeepLinkFactory.create(viewModelScope)) } + + private suspend fun initPushNotifications() { + if (notificationsToggles.isNotificationsEnabled) { + getApplicationIdUseCase().onRight { applicationId -> + sendPushTokenUseCase(applicationId = applicationId) + associateWalletsWithApplicationId(applicationId = applicationId) + updateRemoteWalletsInfoUseCase(applicationId = applicationId) + }.onLeft { Timber.e(it.toString()) } + } + } + + private fun associateWalletsWithApplicationId(applicationId: ApplicationId) { + subscribeOnWalletsUseCase().onEach { wallets -> + associateWalletsWithApplicationIdUseCase(applicationId, wallets) + }.launchIn(viewModelScope) + } } \ No newline at end of file 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 9bda54aa7d..a8fec8347f 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 @@ -5,10 +5,10 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.Analytics +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.common.analytics.events.Token import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder @@ -79,7 +79,7 @@ object TradeCryptoMiddleware { val status = action.cryptoCurrencyStatus val currency = status.currency - val blockchain = Blockchain.fromId(currency.network.id.value) + val blockchain = Blockchain.fromId(currency.network.rawId) val exchangeManager = store.state.globalState.exchangeManager val topUrl = exchangeManager.getUrl( action = CurrencyExchangeManager.Action.Buy, diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index c39150c2c2..be40dc6f87 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -13,4 +13,10 @@ internal class DefaultAuthProvider(private val userWalletsListManager: UserWalle override fun getCardId(): String { return userWalletsListManager.selectedUserWalletSync?.scanResponse?.card?.cardId ?: "" } + + override fun getCardsPublicKeys(): Map { + return userWalletsListManager.userWalletsSync.associate { + it.scanResponse.card.cardId to it.scanResponse.card.cardPublicKey.toHexString() + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt index 8cf25d7f82..58b0438ca7 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CryptoCurrencyConverter.kt @@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.tap.common.extensions.inject import com.tangem.tap.domain.model.Currency import com.tangem.tap.proxy.redux.DaggerGraphState @@ -44,7 +44,7 @@ internal class CryptoCurrencyConverter( } override fun convertBack(value: CryptoCurrency): Currency { - val blockchain = Blockchain.fromId(value.network.id.value) + val blockchain = Blockchain.fromId(value.network.rawId) if (blockchain == Blockchain.Unknown) error("CryptoCurrencyConverter convertBack Unknown blockchain") return when (value) { is CryptoCurrency.Coin -> Currency.Blockchain( diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index b117321609..9b46887dcd 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -10,9 +10,9 @@ import com.tangem.blockchain.extensions.Result import com.tangem.domain.common.TapWorkarounds.isTangemTwins import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.GlobalAction @@ -68,7 +68,7 @@ class CurrencyExchangeManager( walletAddress: String, isDarkTheme: Boolean, ): String? { - val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId) if (blockchain.isTestnet()) return blockchain.getTestnetTopUpUrl() val urlBuilder = getExchangeUrlBuilder(action) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 2671d88af8..32256ffa87 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -12,9 +12,9 @@ import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.domain.core.lce.Lce import com.tangem.domain.exchange.ExpressAvailabilityState import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.GetNetworkCoinStatusUseCase -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.repository.CurrenciesRepository diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt index 709fc68592..1641baa48f 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -2,8 +2,8 @@ package com.tangem.tap.network.exchangeServices import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt index b398116c05..97bd71f241 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -10,8 +10,8 @@ import com.tangem.data.onramp.legacy.mercuryoNetwork import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService @@ -77,7 +77,7 @@ internal class MercuryoService(private val environment: MercuryoEnvironment) : E ): String { if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() - val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId) val builder = Uri.Builder() .scheme(ExchangeUrlBuilder.SCHEME) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 17cfce8e39..7bf0f6d0cf 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -10,8 +10,8 @@ import com.tangem.domain.common.extensions.withIOContext import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService @@ -133,7 +133,7 @@ class MoonPayService( ): String? { if (action == CurrencyExchangeManager.Action.Buy) throw UnsupportedOperationException() - val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value) + val blockchain = Blockchain.fromId(cryptoCurrency.network.rawId) val supportedCurrency = blockchain.moonPaySupportedCurrency ?: return null val moonpayCurrency = status?.availableForSell?.firstOrNull { when (cryptoCurrency) { diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 92ac286e88..64d10c0193 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -22,6 +22,7 @@ import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.ChildFactory +import com.tangem.tap.routing.utils.DeepLinkFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,6 +35,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val appRouterConfig: AppRouterConfig, private val uiDependencies: UiDependencies, private val wcRoutingComponentFactory: WcRoutingComponent.Factory, + private val deeplinkFactory: DeepLinkFactory, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -60,7 +62,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor( stack.subscribe(lifecycle) { stack -> val stackItems = stack.items.map { it.configuration } + wcRoutingComponent.onAppRouteChange(stack.active.configuration) + deeplinkFactory.checkRoutingReadiness(stack.active.configuration) + if (appRouterConfig.stack != stackItems) { appRouterConfig.stack = stackItems } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 0f2b7d5303..f1cd71bbe3 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -291,13 +291,13 @@ internal class ChildFactory @Inject constructor( if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { createComponentChild( context = context, - params = Unit, + params = RedesignedWalletConnectComponent.Params(route.userWalletId), componentFactory = redesignedWalletConnectComponentFactory, ) } else { createComponentChild( context = context, - params = Unit, + params = WalletConnectComponent.Params(route.userWalletId), componentFactory = walletConnectComponentFactory, ) } 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 new file mode 100644 index 0000000000..45bde26900 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -0,0 +1,142 @@ +package com.tangem.tap.routing.utils + +import android.net.Uri +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.DeepLinkRoute +import com.tangem.common.routing.DeepLinkScheme +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.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 dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.transformLatest +import timber.log.Timber +import javax.inject.Inject + +@Suppress("LongParameterList") +@ActivityScoped +internal class DeepLinkFactory @Inject constructor( + private val onrampDeepLink: OnrampDeepLinkHandler.Factory, + private val sellDeepLink: SellDeepLinkHandler.Factory, + private val buyDeepLink: BuyDeepLinkHandler.Factory, + private val referralDeepLink: ReferralDeepLinkHandler.Factory, + private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory, + private val walletDeepLink: WalletDeepLinkHandler.Factory, + private val tokenDetailsDeepLink: TokenDetailsDeepLinkHandler.Factory, +) { + private val permittedAppRoute = MutableStateFlow(false) + + private var lastDeepLink: Uri? = null + private val deepLinkHandlerJobHolder = JobHolder() + + @OptIn(ExperimentalCoroutinesApi::class) + fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + lastDeepLink = deeplinkUri + + Timber.i( + """ + Received deep link intent + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + permittedAppRoute + .transformLatest { isPermitted -> + if (isPermitted) { + lastDeepLink?.let { + launchDeepLink(it, coroutineScope) + } + lastDeepLink = null + } + } + .launchIn(coroutineScope) + .saveIn(deepLinkHandlerJobHolder) + } + + /** + * Check if app is ready to handle deeplink + */ + fun checkRoutingReadiness(appRoute: AppRoute) { + permittedAppRoute.value = when (appRoute) { + AppRoute.Initial, + AppRoute.Home, + is AppRoute.Welcome, + is AppRoute.Disclaimer, + is AppRoute.Stories, + is AppRoute.Onboarding, + -> false + else -> true + } + } + + private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + when (deeplinkUri.scheme) { + DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope) + DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + } + } + } + + private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + val queryParams = getQueryParams(deeplinkUri) + when (deeplinkUri.host) { + DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.Sell.host -> sellDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope) + DeepLinkRoute.Referral.host -> referralDeepLink.create() + DeepLinkRoute.Wallet.host -> walletDeepLink.create() + DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(coroutineScope, queryParams) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + |- With params: $queryParams + """.trimIndent(), + ) + } + } + } + + private fun getQueryParams(uri: Uri): Map { + val params = mutableMapOf() + + uri.queryParameterNames.forEach { paramName -> + val paramValue = uri.getQueryParameter(paramName) + + if (paramName.validate() && paramValue?.validate() == true) { + params[paramName] = paramValue + } + } + + return params + } + + /** + * Check for malicious symbol in uri part + */ + private fun String.validate(): Boolean { + val regex = DEEPLINK_VALIDATION_REGEX.toRegex() + + return !regex.containsMatchIn(this) + } + + private companion object { + const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..32be06bd2f --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -0,0 +1,324 @@ +package com.tangem.tap.routing.utils + +import android.net.Uri +import com.tangem.common.routing.AppRoute +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.tokendetails.deeplink.TokenDetailsDeepLinkHandler +import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler +import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.* +import org.junit.After +import org.junit.Before +import org.junit.Test +import timber.log.Timber + +@OptIn(ExperimentalCoroutinesApi::class) +class DeepLinkFactoryTest { + + private val onrampDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val sellDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + private val buyDeepLinkFactory = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + private val referralDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val walletConnectDeepLinkFactory = mockk(relaxed = true) { + every { create(any()) } returns mockk() + } + private val walletDeepLinkFactory = mockk(relaxed = true) { + every { create() } returns mockk() + } + private val tokenDetailsDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } + + private val mockedUri = mockk(relaxed = true) + + private lateinit var testDispatcher: TestDispatcher + private lateinit var testScope: TestScope + + private val deepLinkFactory = DeepLinkFactory( + onrampDeepLinkFactory, + sellDeepLinkFactory, + buyDeepLinkFactory, + referralDeepLinkFactory, + walletConnectDeepLinkFactory, + walletDeepLinkFactory, + tokenDetailsDeepLinkFactory, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() { + testDispatcher = StandardTestDispatcher() + testScope = TestScope(testDispatcher) + + Dispatchers.setMain(testDispatcher) + + every { mockedUri.path } returns "/path" + every { mockedUri.toString() } returns "https://example.com/path?query=param" + every { mockedUri.authority } returns "example.com" + every { mockedUri.port } returns 443 // Default HTTPS port + every { mockedUri.fragment } returns null // No fragment in this URI + + Timber.uprootAll() // Disable Timber logging for tests + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + // Reset the main dispatcher + Dispatchers.resetMain() + // Clean up test coroutines + testScope.cancel() + } + + @Test + fun `handleDeeplink stores uri and launches when permitted`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + // Set permittedAppRoute to true + deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + + advanceUntilIdle() + + // Verify onramp handler was called + verify { + onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) + } + } + + @Test + fun `handleDeeplink does not launch when not permitted`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.checkRoutingReadiness(AppRoute.Initial) + + advanceUntilIdle() + + // Verify no handler was called + verify(inverse = true) { onrampDeepLinkFactory.create(any(), any()) } + } + + @Test + fun `launchDeepLink handles tangem scheme correctly`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify { + onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) + } + } + + @Test + fun `launchDeepLink handles wc scheme correctly`() = runTest { + every { mockedUri.scheme } returns "wc" + every { mockedUri.host } returns "" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify { + walletConnectDeepLinkFactory.create(eq(mockedUri)) + } + } + + @Test + fun `launchDeepLink ignores unknown scheme`() = runTest { + every { mockedUri.scheme } returns "https" + every { mockedUri.host } returns "example.com" + + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify(inverse = true) { + onrampDeepLinkFactory.create(any(), any()) + sellDeepLinkFactory.create(any(), any()) + buyDeepLinkFactory.create(any()) + referralDeepLinkFactory.create() + walletConnectDeepLinkFactory.create(any()) + walletDeepLinkFactory.create() + tokenDetailsDeepLinkFactory.create(any(), any()) + } + } + + @Test + fun `handleTangemDeepLinks routes to correct handler`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.query } returns "param=value" + every { mockedUri.queryParameterNames } returns setOf("param") + every { mockedUri.getQueryParameter("param") } returns "value" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + + // Test Onramp + every { mockedUri.host } returns "onramp" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + + // Test Sell + every { mockedUri.host } returns "redirect_sell" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + + // Test Token Details + every { mockedUri.host } returns "token" + 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 "" + + // Test Buy + every { mockedUri.host } returns "redirect" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { buyDeepLinkFactory.create(eq(testScope)) } + + // Test Referral + every { mockedUri.host } returns "referral" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { referralDeepLinkFactory.create() } + + // Test Wallet + every { mockedUri.host } returns "main" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { walletDeepLinkFactory.create() } + } + + @Test + fun `handleTangemDeepLinks incorrect host`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "unknown" + + deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) + deepLinkFactory.handleDeeplink(mockedUri, testScope) + + advanceUntilIdle() + + verify(inverse = true) { + onrampDeepLinkFactory.create(any(), any()) + sellDeepLinkFactory.create(any(), any()) + buyDeepLinkFactory.create(any()) + referralDeepLinkFactory.create() + walletConnectDeepLinkFactory.create(any()) + walletDeepLinkFactory.create() + tokenDetailsDeepLinkFactory.create(any(), any()) + } + } + + @Test + fun `getParams filters malicious parameters`() = runTest { + every { mockedUri.scheme } returns "tangem" + every { mockedUri.host } returns "onramp" + every { mockedUri.query } returns "safe=ok&malicious=%3Cscript%3E"e=O%27Brien" + every { mockedUri.queryParameterNames } returns setOf("safe", "malicious", "quote") + every { mockedUri.getQueryParameter("safe") } returns "ok" + every { mockedUri.getQueryParameter("malicious") } returns "