Updated on 2026-08-14

This commit is contained in:
Tangem 2023-06-06 16:07:14 +03:00
parent f3afa55469
commit 4affe1293d
12 changed files with 254 additions and 162 deletions

View file

@ -32,6 +32,11 @@ import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.domain.userWalletList.UserWalletsListManager
import com.tangem.tap.domain.userWalletList.di.provideBiometricImplementation
import com.tangem.tap.domain.userWalletList.di.provideRuntimeImplementation
import com.tangem.tap.features.intentHandler.MainIntentHandler
import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler
import com.tangem.tap.features.intentHandler.handlers.BuyCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.SellCurrencyIntentHandler
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.shop.redux.ShopAction
@ -45,6 +50,7 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@ -95,6 +101,9 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
@Inject
lateinit var walletConnectInteractor: WalletConnectInteractor
// TODO: fixme: inject through DI
private val intentHandler: MainIntentHandler = MainIntentHandler()
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
@ -117,6 +126,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lockUserWalletsTimer = LockUserWalletsTimer(owner = this)
initUserWalletsListManager()
initIntentHandlers()
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
@ -133,6 +143,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
)
}
private fun initIntentHandlers() {
val hasSavedWalletsProvider = { store.state.globalState.userWalletsListManager?.hasUserWallets == true }
intentHandler.addHandler(BackgroundScanIntentHandler(hasSavedWalletsProvider))
intentHandler.addHandler(WalletConnectLinkIntentHandler())
intentHandler.addHandler(BuyCurrencyIntentHandler())
intentHandler.addHandler(SellCurrencyIntentHandler())
}
private fun initUserWalletsListManager() {
val manager = if (preferencesStorage.shouldSaveUserWallets) {
UserWalletsListManager.provideBiometricImplementation(
@ -164,12 +182,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
super.onResume()
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
navigateToInitialScreenIfNeeded(intent)
navigateToInitialScreenIfNeededOnResume(intent)
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
intentHandler.handleIntent(intent, userWalletsListManager.hasUserWallets)
scope.launch {
intentHandler.handleIntent(intent)
}
}
override fun onStart() {
@ -185,6 +205,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
override fun onDestroy() {
store.dispatch(NavigationAction.ActivityDestroyed(WeakReference(this)))
intentHandler.removeAll()
super.onDestroy()
}
@ -235,29 +256,38 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac
lockUserWalletsTimer?.restart()
}
private fun navigateToInitialScreenIfNeeded(intent: Intent?) {
private fun navigateToInitialScreenIfNeededOnResume(intentWhichStartedActivity: Intent?) {
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isNotScannedBefore = store.state.globalState.scanResponse == null
val isOnboardingServiceNotActive = store.state.globalState.onboardingState.onboardingStarted
val isShopNotOpened = store.state.shopState.total != null
when {
!backStackIsEmpty && isNotScannedBefore && isOnboardingServiceNotActive && isShopNotOpened -> {
navigateToInitialScreen(intent)
navigateToInitialScreenOnResume(intentWhichStartedActivity)
}
backStackIsEmpty -> {
navigateToInitialScreen(intent)
navigateToInitialScreenOnResume(intentWhichStartedActivity)
}
else -> Unit
}
}
private fun navigateToInitialScreen(intent: Intent?) {
private fun navigateToInitialScreenOnResume(intentWhichStartedActivity: Intent?) {
if (store.state.globalState.userWalletsListManager?.hasUserWallets == true) {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome))
store.dispatchOnMain(WelcomeAction.HandleIntentIfNeeded(intent))
store.dispatchOnMain(WelcomeAction.SetInitialIntent(intentWhichStartedActivity))
scope.launch {
val handler = BackgroundScanIntentHandler(hasSavedUserWalletsProvider = { true })
val intentWasHandled = handler.handleIntent(intentWhichStartedActivity)
if (!intentWasHandled) {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics)
}
}
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home))
intentHandler.handleIntent(intent, hasSavedUserWallets = false)
scope.launch {
intentHandler.handleIntent(intentWhichStartedActivity)
}
}
store.dispatch(BackupAction.CheckForUnfinishedBackup)
}

View file

@ -102,7 +102,6 @@ val walletCurrenciesManager by lazy {
val totalFiatBalanceCalculator by lazy {
TotalFiatBalanceCalculator.provideDefaultImplementation()
}
val intentHandler by lazy { IntentHandler() }
@HiltAndroidApp
class TapApplication : Application(), ImageLoaderFactory {

View file

@ -1,127 +0,0 @@
package com.tangem.tap.common
import android.content.Intent
import android.net.Uri
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
class IntentHandler {
private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED,
NfcAdapter.ACTION_TECH_DISCOVERED,
NfcAdapter.ACTION_TAG_DISCOVERED,
)
fun handleIntent(intent: Intent?, hasSavedUserWallets: Boolean) {
handleBackgroundScan(intent, hasSavedUserWallets)
handleWalletConnectLink(intent)
handleBuyCurrencyCallback(intent)
handleSellCurrencyCallback(intent)
}
fun handleWalletConnectLink(intent: Intent?) {
val wcUri = when (intent?.scheme) {
WalletConnectManager.WC_SCHEME -> {
intent.data?.toString()
}
TANGEM_SCHEME -> {
intent.data?.toString()?.removePrefixOrNull(TANGEM_WC_PREFIX)
}
else -> {
null
}
}
if (wcUri != null) {
store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
}
}
fun handleBackgroundScan(intent: Intent?, hasSavedUserWallets: Boolean): Boolean {
if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
}
if (tag == null) return false
intent.action = null
if (hasSavedUserWallets) {
// TODO: Remove delay after [REDACTED_JIRA]
scope.launch {
delay(timeMillis = 200)
store.dispatch(WelcomeAction.ProceedWithCard)
}
} else {
store.dispatch(HomeAction.ReadCard())
}
return true
}
private fun handleBuyCurrencyCallback(intent: Intent?) {
val data = intent?.data ?: return
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
if (data.host == successUri.host && data.authority == successUri.authority) {
val currency = store.state.walletState.selectedCurrency ?: return
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(Token.Bought(currencyType))
}
}
fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(
WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
}
}
companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val TANGEM_SCHEME = "tangem"
private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.tap.features.intentHandler
import android.content.Intent
/**
[REDACTED_AUTHOR]
*/
interface IntentHandler {
suspend fun handleIntent(intent: Intent?): Boolean
}

View file

@ -0,0 +1,29 @@
package com.tangem.tap.features.intentHandler
import android.content.Intent
/**
[REDACTED_AUTHOR]
*/
class MainIntentHandler {
private val intentHandlers = mutableListOf<IntentHandler>()
fun addHandler(handler: IntentHandler) {
intentHandlers.add(handler)
}
fun removeIntentHandler(handler: IntentHandler) {
intentHandlers.remove(handler)
}
fun removeAll() {
intentHandlers.clear()
}
suspend fun handleIntent(intent: Intent?) {
intentHandlers.toList().forEach {
it.handleIntent(intent)
}
}
}

View file

@ -0,0 +1,52 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.os.Build
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.welcome.redux.WelcomeAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
[REDACTED_AUTHOR]
*/
class BackgroundScanIntentHandler(
private val hasSavedUserWalletsProvider: () -> Boolean,
) : IntentHandler {
private val nfcActions = arrayOf(
NfcAdapter.ACTION_NDEF_DISCOVERED,
NfcAdapter.ACTION_TECH_DISCOVERED,
NfcAdapter.ACTION_TAG_DISCOVERED,
)
override suspend fun handleIntent(intent: Intent?): Boolean {
if (intent == null || intent.action !in nfcActions) return false
val tag: Tag? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG, Tag::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(NfcAdapter.EXTRA_TAG)
}
if (tag == null) return false
intent.action = null
if (hasSavedUserWalletsProvider.invoke()) {
// TODO: Remove delay after [REDACTED_JIRA]
scope.launch {
delay(timeMillis = 200)
store.dispatch(WelcomeAction.ProceedWithCard)
}
} else {
store.dispatch(HomeAction.ReadCard())
}
return true
}
}

View file

@ -0,0 +1,30 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import android.net.Uri
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class BuyCurrencyIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
val data = intent?.data ?: return false
val currency = store.state.walletState.selectedCurrency ?: return false
val successUri = Uri.parse(ExchangeUrlBuilder.SUCCESS_URL)
return if (data.host == successUri.host && data.authority == successUri.authority) {
val currencyType = AnalyticsParam.CurrencyType.Currency(currency)
Analytics.send(Token.Bought(currencyType))
true
} else {
false
}
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
class SellCurrencyIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
return try {
val intentData = intent?.data ?: return false
val transactionID = intentData.getQueryParameter(TRANSACTION_ID_PARAM) ?: return false
val currency = intentData.getQueryParameter(CURRENCY_CODE_PARAM) ?: return false
val amount = intentData.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return false
val destinationAddress = intentData.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM) ?: return false
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(
WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID,
),
)
true
} catch (exception: Exception) {
Timber.d("Not MoonPay URL")
false
}
}
companion object {
private const val TRANSACTION_ID_PARAM = "transactionId"
private const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.intentHandler.handlers
import android.content.Intent
import com.tangem.tap.common.extensions.removePrefixOrNull
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.intentHandler.IntentHandler
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
class WalletConnectLinkIntentHandler : IntentHandler {
override suspend fun handleIntent(intent: Intent?): Boolean {
val intentData = intent?.data ?: return false
val scheme = intent.scheme ?: return false
val wcUri = when (scheme) {
WalletConnectManager.WC_SCHEME -> intentData.toString()
TANGEM_SCHEME -> intentData.toString().removePrefixOrNull(TANGEM_WC_PREFIX)
else -> null
}
return if (wcUri == null) {
false
} else {
store.dispatch(WalletConnectAction.HandleDeepLink(wcUri))
true
}
}
companion object {
private const val TANGEM_SCHEME = "tangem"
private const val TANGEM_WC_PREFIX = "tangem://wc?uri="
}
}

View file

@ -15,7 +15,7 @@ internal sealed interface WelcomeAction : Action {
data class Error(val error: TangemError) : WelcomeAction
}
data class HandleIntentIfNeeded(val intent: Intent?) : WelcomeAction
data class SetInitialIntent(val intent: Intent?) : WelcomeAction
object CloseError : WelcomeAction

View file

@ -1,11 +1,8 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.*
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -19,6 +16,8 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.domain.userWalletList.unlockIfLockable
import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.signin.redux.SignInAction
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
@ -39,24 +38,10 @@ internal class WelcomeMiddleware {
private fun handleAction(action: WelcomeAction, state: WelcomeState) {
when (action) {
is WelcomeAction.ProceedWithBiometrics -> {
proceedWithBiometrics(state)
}
is WelcomeAction.ProceedWithCard -> {
proceedWithCard(state)
}
is WelcomeAction.HandleIntentIfNeeded -> {
handleInitialIntent(action.intent)
}
is WelcomeAction.ClearUserWallets -> {
disableUserWalletsSaving()
}
is WelcomeAction.ProceedWithBiometrics.Error,
is WelcomeAction.ProceedWithCard.Error,
is WelcomeAction.ProceedWithBiometrics.Success,
is WelcomeAction.ProceedWithCard.Success,
is WelcomeAction.CloseError,
-> Unit
is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometrics(state)
is WelcomeAction.ProceedWithCard -> proceedWithCard(state)
is WelcomeAction.ClearUserWallets -> disableUserWalletsSaving()
else -> Unit
}
}
@ -85,7 +70,9 @@ internal class WelcomeMiddleware {
store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success)
store.onUserWalletSelected(userWallet = selectedUserWallet)
intentHandler.handleWalletConnectLink(state.intent)
state.intent?.let {
WalletConnectLinkIntentHandler().handleIntent(it)
}
}
}
@ -104,7 +91,9 @@ internal class WelcomeMiddleware {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
store.onUserWalletSelected(userWallet = userWallet)
intentHandler.handleWalletConnectLink(state.intent)
state.intent?.let {
WalletConnectLinkIntentHandler().handleIntent(it)
}
}
}
}

View file

@ -14,7 +14,7 @@ internal object WelcomeReducer {
private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState {
return when (action) {
is WelcomeAction.HandleIntentIfNeeded -> state.copy(intent = action.intent)
is WelcomeAction.SetInitialIntent -> state.copy(intent = action.intent)
is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true)
is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true)
is WelcomeAction.ProceedWithBiometrics.Error -> state.copy(