diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 63fbb50de7..a15171a939 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,10 +108,10 @@ repositories { dependencies { implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.aar")))) implementation(project(":domain")) - implementation(project(":network")) implementation(project(":common")) implementation(project(":core:res")) implementation(project(":core:ui")) + implementation(project(":core:datasource")) implementation(project(":core:utils")) implementation(project(":libs:crypto")) implementation(project(":libs:auth")) @@ -120,6 +120,9 @@ dependencies { implementation(project(":features:referral:presentation")) implementation(project(":features:referral:domain")) implementation(project(":features:referral:data")) + implementation(project(":features:swap:presentation")) + implementation(project(":features:swap:domain")) + implementation(project(":features:swap:data")) /** AndroidX libraries */ implementation(AndroidX.coreKtx) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index bfc2bf8157..897f0f7c40 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit bfc2bf8157089bce6b44779bdae66df2c920de70 +Subproject commit 897f0f7c402fd53c5ee1fa4ab601ecd4e43ff389 diff --git a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt index c4b3d48e73..0bfb76a8ac 100644 --- a/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt +++ b/app/src/main/java/com/tangem/tap/LockUserWalletsTimer.kt @@ -6,7 +6,11 @@ import androidx.lifecycle.lifecycleScope import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import timber.log.Timber import kotlin.time.Duration diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index b7c9ad608a..5ef7c985a6 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -15,7 +15,6 @@ import com.tangem.operations.backup.BackupService import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager -import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.OnActivityResultCallback import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.extensions.dispatchOnMain @@ -29,6 +28,7 @@ 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.features.shop.redux.ShopAction +import com.tangem.tap.features.welcome.redux.WelcomeAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.wallet.R import com.tangem.wallet.databinding.ActivityMainBinding @@ -66,7 +66,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac private var snackbar: Snackbar? = null private val dialogManager = DialogManager() - private val intentHandler = IntentHandler() private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) private val onActivityResultCallbacks = mutableListOf() @@ -119,16 +118,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted val shopOpened = store.state.shopState.total != null if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) { - val navigateTo = if (userWalletsListManager.hasSavedUserWallets) AppScreen.Welcome else AppScreen.Home - - store.dispatchOnMain(NavigationAction.NavigateTo(navigateTo)) + if (userWalletsListManager.hasSavedUserWallets) { + store.dispatchOnMain(WelcomeAction.HandleDeepLink(intent)) + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Welcome)) + } else { + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Home)) + intentHandler.handleWalletConnectLink(intent) + } } - intentHandler.handleIntent(intent) + intentHandler.handleBackgroundScan(intent) + intentHandler.handleSellCurrencyCallback(intent) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - intentHandler.handleIntent(intent) + intentHandler.handleBackgroundScan(intent) + intentHandler.handleSellCurrencyCallback(intent) + intentHandler.handleWalletConnectLink(intent) } override fun onStart() { diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 41b4fb59bb..0d483b43ee 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -11,11 +11,12 @@ import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder import com.tangem.common.json.MoshiJsonConverter +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.domain.DomainLayer import com.tangem.domain.common.LogConfig -import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.AndroidAssetReader import com.tangem.tap.common.AssetReader +import com.tangem.tap.common.IntentHandler import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.AnalyticsFactory import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder @@ -109,6 +110,7 @@ val walletCurrenciesManager by lazy { val totalFiatBalanceCalculator by lazy { TotalFiatBalanceCalculator.provideDefaultImplementation() } +val intentHandler by lazy { IntentHandler() } @HiltAndroidApp class TapApplication : Application(), ImageLoaderFactory { @@ -156,6 +158,7 @@ class TapApplication : Application(), ImageLoaderFactory { appStateHolder.userTokensRepository = userTokensRepository } + //todo refactor: move to datasource and provide via DI private fun initMoshiConverter() { fun appAdapters(): List = listOf( BigDecimalAdapter(), diff --git a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt index 1e41053369..cfee84fd69 100644 --- a/app/src/main/java/com/tangem/tap/common/IntentHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/IntentHandler.kt @@ -19,13 +19,13 @@ class IntentHandler { private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount" private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress" - fun handleIntent(intent: Intent?) { - handleBackgroundScan(intent) - handleWalletConnectLink(intent) - handleSellCurrencyCallback(intent) + fun handleWalletConnectLink(intent: Intent?) { + if (intent?.scheme == WalletConnectManager.WC_SCHEME) { + store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString())) + } } - private fun handleBackgroundScan(intent: Intent?) { + fun handleBackgroundScan(intent: Intent?) { if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action || NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action ) @@ -40,13 +40,7 @@ class IntentHandler { } } - private fun handleWalletConnectLink(intent: Intent?) { - if (intent?.scheme == WalletConnectManager.WC_SCHEME) { - store.dispatch(WalletConnectAction.HandleDeepLink(intent.data?.toString())) - } - } - - private fun handleSellCurrencyCallback(intent: Intent?) { + fun handleSellCurrencyCallback(intent: Intent?) { try { val transactionID = intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index 3abbbac6d1..711e7ed172 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -54,12 +54,12 @@ fun FragmentActivity.openFragment( if (screen.isDialogFragment) { (fragment as DialogFragment).show(transaction, screen.name) if (addToBackstack) { - transaction.addToBackStack(null) + transaction.addToBackStack(screen.name) } } else { transaction.replace(R.id.fragment_container, fragment, screen.name) - if (addToBackstack && (screen != AppScreen.Home && screen != AppScreen.Welcome)) { - transaction.addToBackStack(null) + if (addToBackstack) { + transaction.addToBackStack(screen.name) } transaction.commitAllowingStateLoss() } diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt index df26b85edc..3e4f3a9702 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationAction.kt @@ -12,7 +12,10 @@ sealed class NavigationAction : Action { val addToBackstack: Boolean = true, ) : NavigationAction() - data class PopBackTo(val screen: AppScreen? = null) : NavigationAction() + data class PopBackTo( + val screen: AppScreen? = null, + val inclusive: Boolean = false, + ) : NavigationAction() data class OpenUrl(val url: String) : NavigationAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt index da9430b1e1..90d6b880da 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationMiddleware.kt @@ -32,13 +32,13 @@ val navigationMiddleware: Middleware = { _, state -> AppScreen.Home, AppScreen.Welcome, -> { - navState?.activity?.get()?.popBackTo(screen = null, inclusive = true) + navState?.activity?.get()?.popBackTo(screen, action.inclusive) if (navState?.backStack?.contains(screen) != true) { store.dispatchOnMain(NavigationAction.NavigateTo(screen)) } } else -> { - navState?.activity?.get()?.popBackTo(screen = action.screen) + navState?.activity?.get()?.popBackTo(screen, action.inclusive) } } } diff --git a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt index 8b4136e90f..7cee3bcfd9 100644 --- a/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/RatesRepository.kt @@ -2,7 +2,7 @@ package com.tangem.tap.domain import com.tangem.common.services.Result import com.tangem.domain.common.ThrottlerWithValues -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import java.math.BigDecimal diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 4d58f2467d..906a0f1306 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -48,9 +48,9 @@ import kotlin.coroutines.suspendCoroutine class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) { val canUseBiometry: Boolean - get() = tangemSdk.biometricManager.canAuthenticate || canEnrollBiometrics + get() = tangemSdk.biometricManager.canAuthenticate || needEnrollBiometrics - val canEnrollBiometrics: Boolean + val needEnrollBiometrics: Boolean get() = tangemSdk.biometricManager.canEnrollBiometrics val biometricManager: BiometricManager @@ -61,10 +61,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co cardId: String? = null, additionalBlockchainsToDerive: Collection? = null, messageRes: Int? = null, - useBiometricsForAccessCode: Boolean = false, ): CompletionResult { - setAccessCodeRequestPolicy(useBiometricsForAccessCode) - val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) return runTaskAsyncReturnOnMain( runnable = ScanProductTask( @@ -109,9 +106,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun derivePublicKeys( cardId: String, derivations: Map>, - useBiometricsForAccessCode: Boolean = false, ): CompletionResult { - setAccessCodeRequestPolicy(useBiometricsForAccessCode) return runTaskAsyncReturnOnMain(DeriveMultipleWalletPublicKeysTask(derivations), cardId) } @@ -192,9 +187,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun scanCard( cardId: String? = null, - useBiometricsForAccessCode: Boolean = false, ): CompletionResult { - setAccessCodeRequestPolicy(useBiometricsForAccessCode) return runTaskAsyncReturnOnMain( runnable = ScanTask(), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 0bc9a2c283..aae3cf8e9a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -2,7 +2,7 @@ package com.tangem.tap.domain import androidx.annotation.StringRes import com.tangem.common.core.TangemError -import com.tangem.network.api.tangemTech.TangemTechError +import com.tangem.datasource.api.tangemTech.TangemTechError import com.tangem.wallet.R interface TapErrors diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 21b0ef03bf..7eeb396463 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -147,7 +147,7 @@ class TapWalletManager { updateConfigManager(data) withMainContext { - store.dispatch(WalletAction.ResetState(data.card.cardId)) + store.dispatch(WalletAction.ResetState(data.card)) store.dispatch(WalletConnectAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard)) diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt index 003c85904a..f41c664276 100644 --- a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt +++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt @@ -1,8 +1,10 @@ package com.tangem.tap.domain.model.builders import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.WalletManager -import com.tangem.tap.domain.model.UserWallet +import com.tangem.domain.common.util.UserWalletId import com.tangem.tap.domain.model.WalletDataModel import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.tokens.models.BlockchainNetwork @@ -17,29 +19,27 @@ interface WalletStoreBuilder { fun walletManager(walletManager: WalletManager?): WalletStoreBuilder } - interface WalletMangerWalletStoreBuilder : WalletStoreBuilder { - fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?): WalletStoreBuilder - } + interface WalletMangerWalletStoreBuilder : WalletStoreBuilder companion object { operator fun invoke( - userWallet: UserWallet, + userWalletId: UserWalletId, blockchainNetwork: BlockchainNetwork, ): BlockchainNetworkWalletStoreBuilder { - return BlockchainNetworkWalletStoreBuilderImpl(userWallet, blockchainNetwork) + return BlockchainNetworkWalletStoreBuilderImpl(userWalletId, blockchainNetwork) } operator fun invoke( - userWallet: UserWallet, + userWalletId: UserWalletId, walletManager: WalletManager, ): WalletMangerWalletStoreBuilder { - return WalletMangerWalletStoreBuilderImpl(userWallet, walletManager) + return WalletMangerWalletStoreBuilderImpl(userWalletId, walletManager) } } } private class BlockchainNetworkWalletStoreBuilderImpl( - private val userWallet: UserWallet, + private val userWalletId: UserWalletId, private val blockchainNetwork: BlockchainNetwork, ) : WalletStoreBuilder.BlockchainNetworkWalletStoreBuilder { private var walletManager: WalletManager? = null @@ -53,7 +53,7 @@ private class BlockchainNetworkWalletStoreBuilderImpl( val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager) return WalletStoreModel( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, blockchainNetwork = blockchainNetwork, walletManager = walletManager, walletsData = (listOf(blockchainWalletData) + tokensWalletsData), @@ -63,25 +63,20 @@ private class BlockchainNetworkWalletStoreBuilderImpl( } private class WalletMangerWalletStoreBuilderImpl( - private val userWallet: UserWallet, + private val userWalletId: UserWalletId, private val walletManager: WalletManager, ) : WalletStoreBuilder.WalletMangerWalletStoreBuilder { - private var blockchainNetwork: BlockchainNetwork? = null - - override fun blockchainNetwork(blockchainNetwork: BlockchainNetwork?) = this.apply { - this.blockchainNetwork = blockchainNetwork - } override fun build(): WalletStoreModel { - val blockchainNetwork = this.blockchainNetwork ?: BlockchainNetwork.fromWalletManager(walletManager) - val blockchainWalletData = blockchainNetwork.getBlockchainWalletData(walletManager) - val tokensWalletsData = blockchainNetwork.getTokensWalletsData(walletManager) + val wallet = walletManager.wallet + val blockchainWalletData = wallet.blockchain.toBlockchainWalletData(walletManager) + val tokenWalletsData = wallet.getTokens().firstOrNull()?.toTokenWalletData(walletManager) return WalletStoreModel( - userWalletId = userWallet.walletId, - blockchainNetwork = blockchainNetwork, + userWalletId = userWalletId, + blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager), walletManager = walletManager, - walletsData = (listOf(blockchainWalletData) + tokensWalletsData), + walletsData = (listOf(blockchainWalletData) + listOfNotNull(tokenWalletsData)), walletRent = null, ) } @@ -117,6 +112,35 @@ private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager? } } +private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): WalletDataModel { + val wallet = walletManager.wallet + return WalletDataModel( + currency = Currency.Blockchain( + blockchain = this, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ), + status = WalletDataModel.Loading, + walletAddresses = wallet.createAddressesData(), + existentialDeposit = getExistentialDeposit(walletManager), + fiatRate = null, + ) +} + +private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataModel { + val wallet = walletManager.wallet + return WalletDataModel( + currency = Currency.Token( + token = this, + blockchain = wallet.blockchain, + derivationPath = wallet.publicKey.derivationPath?.rawPath, + ), + status = WalletDataModel.Loading, + walletAddresses = wallet.createAddressesData(), + existentialDeposit = getExistentialDeposit(walletManager), + fiatRate = null, + ) +} + private fun getExistentialDeposit(walletManager: WalletManager?): BigDecimal? { return (walletManager as? ExistentialDepositProvider)?.getExistentialDeposit() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt index d834911426..6ab45894b9 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt @@ -44,7 +44,6 @@ import kotlinx.coroutines.launch // TODO: Create repository for that object ScanCardProcessor { suspend fun scan( - useBiometricsForAccessCode: Boolean = false, additionalBlockchainsToDerive: Collection? = null, cardId: String? = null, onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {}, @@ -55,29 +54,37 @@ object ScanCardProcessor { ) = withMainContext { onProgressStateChange(true) onScanStateChange(true) - tangemSdkManager.scanProduct( + + tangemSdkManager.changeDisplayedCardIdNumbersCount(null) + + val result = tangemSdkManager.scanProduct( userTokensRepository = userTokensRepository, cardId = cardId, additionalBlockchainsToDerive = additionalBlockchainsToDerive, - useBiometricsForAccessCode = useBiometricsForAccessCode, ) + + store.dispatchOnMain(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) + + result .doOnFailure { error -> onProgressStateChange(false) onScanStateChange(false) onFailure(error) } .doOnSuccess { scanResponse -> + tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse) + onScanStateChange(false) checkForUnfinishedBackupForSaltPay( backupService = backupService, scanResponse = scanResponse, onProgressStateChange = { onProgressStateChange(it) }, - nextHandler = { + nextHandler = { scanResponse1 -> showDisclaimerIfNeed( - scanResponse = scanResponse, - nextHandler = { + scanResponse = scanResponse1, + nextHandler = { scanResponse2 -> onScanSuccess( - scanResponse = scanResponse, + scanResponse = scanResponse2, onProgressStateChange = onProgressStateChange, onSuccess = onSuccess, onWalletNotCreated = onWalletNotCreated, @@ -87,7 +94,6 @@ object ScanCardProcessor { ) }, ) - } } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt index 6e94b5a88a..0e18c4018d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/Currency.kt @@ -3,7 +3,7 @@ package com.tangem.tap.domain.tokens import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.Blockchain import com.tangem.domain.common.extensions.fromNetworkId -import com.tangem.network.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.CoinsResponse @JsonClass(generateAdapter = true) data class CurrencyFromJson( diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt index 7df68e80c3..fbbaad9022 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt @@ -6,9 +6,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.domain.common.extensions.getListOfCoins import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.network.api.tangemTech.CoinsResponse -import com.tangem.network.api.tangemTech.TangemTechService -import com.tangem.network.common.MoshiConverter +import com.tangem.datasource.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.TangemTechService +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.tap.common.AssetReader class LoadAvailableCoinsService( diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt index 7418891662..2aa1dce1a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/OldUserTokensRepository.kt @@ -7,8 +7,8 @@ import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.services.Result import com.tangem.domain.common.extensions.getTokens import com.tangem.domain.common.extensions.toNetworkId -import com.tangem.network.api.tangemTech.TangemTechService -import com.tangem.network.common.MoshiConverter +import com.tangem.datasource.api.tangemTech.TangemTechService +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.tap.common.FileReader import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.domain.tokens.models.TokenDao diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt index c0b7e18155..821a8046ac 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensNetworkService.kt @@ -2,8 +2,8 @@ package com.tangem.tap.domain.tokens import com.tangem.common.core.TangemSdkError import com.tangem.common.services.Result -import com.tangem.network.api.tangemTech.TangemTechService -import com.tangem.network.api.tangemTech.UserTokensResponse +import com.tangem.datasource.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.UserTokensResponse import com.tangem.tap.domain.NoDataError class UserTokensNetworkService(private val tangemTechService: TangemTechService) { diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt index 4d6646ed32..6fb7de9ad7 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt @@ -3,10 +3,10 @@ package com.tangem.tap.domain.tokens import android.content.Context import com.tangem.blockchain.common.DerivationStyle import com.tangem.common.services.Result +import com.tangem.datasource.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.UserTokensResponse import com.tangem.domain.common.CardDTO import com.tangem.domain.common.util.userWalletId -import com.tangem.network.api.tangemTech.TangemTechService -import com.tangem.network.api.tangemTech.UserTokensResponse import com.tangem.tap.common.AndroidFileReader import com.tangem.tap.domain.NoDataError import com.tangem.tap.domain.tokens.models.BlockchainNetwork diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt index 5f36f9bd49..9e5dac0b68 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensStorageService.kt @@ -2,9 +2,9 @@ package com.tangem.tap.domain.tokens import com.squareup.moshi.JsonAdapter import com.tangem.Log +import com.tangem.datasource.api.tangemTech.UserTokensResponse +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.domain.common.CardDTO -import com.tangem.network.api.tangemTech.UserTokensResponse -import com.tangem.network.common.MoshiConverter import com.tangem.tap.common.FileReader import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.models.toCurrencies diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt index d9b586d3e6..9790310eb0 100644 --- a/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/TotalFiatBalanceCalculator.kt @@ -5,10 +5,30 @@ import com.tangem.tap.domain.model.WalletStoreModel import java.math.BigDecimal interface TotalFiatBalanceCalculator { + + /** + * Calculate total fiat balance for list of [WalletStoreModel] + * @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing] + * @param walletStores List of [WalletStoreModel] to calculate fiat amount + * @param initial Initial [TotalFiatBalance] state, used when list of [WalletStoreModel] is empty + * @return [TotalFiatBalance] with state found with the [WalletStoreModel] list + * */ suspend fun calculate( - prevAmount: BigDecimal, + prevAmount: BigDecimal?, walletStores: List, + initial: TotalFiatBalance, ): TotalFiatBalance + /** + * Same as [TotalFiatBalanceCalculator.calculate] but returns null if list of [WalletStoreModel] is empty + * @param prevAmount Previous amount, used in [TotalFiatBalance.Refreshing] + * @param walletStores List of [WalletStoreModel] to calculate fiat amount + * @return [TotalFiatBalance] with state found with the [WalletStoreModel] list + * */ + suspend fun calculateOrNull( + prevAmount: BigDecimal?, + walletStores: List, + ): TotalFiatBalance? + companion object } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt index be318667dd..512e986cbf 100644 --- a/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt +++ b/app/src/main/java/com/tangem/tap/domain/totalBalance/implementation/DefaultTotalFiatBalanceCalculator.kt @@ -11,11 +11,19 @@ import java.math.BigDecimal internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { override suspend fun calculate( - prevAmount: BigDecimal, + prevAmount: BigDecimal?, walletStores: List, + initial: TotalFiatBalance, ): TotalFiatBalance { + return calculateOrNull(prevAmount, walletStores) ?: initial + } + + override suspend fun calculateOrNull( + prevAmount: BigDecimal?, + walletStores: List, + ): TotalFiatBalance? { return if (walletStores.isEmpty()) { - TotalFiatBalance.Loading + null } else { withContext(Dispatchers.Default) { val walletsData = walletStores @@ -25,7 +33,9 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator { when (walletsData.findStatus()) { TotalFiatBalanceStatus.Loading -> TotalFiatBalance.Loading - TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing(prevAmount) + TotalFiatBalanceStatus.Refreshing -> TotalFiatBalance.Refreshing( + amount = prevAmount ?: BigDecimal.ZERO, + ) TotalFiatBalanceStatus.Error -> TotalFiatBalance.Error(calculateAmount()) TotalFiatBalanceStatus.Loaded -> TotalFiatBalance.Loaded(calculateAmount()) } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt index 9aed6f968c..7917ddaa07 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt @@ -10,7 +10,7 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString import com.tangem.domain.common.CardDTO import com.tangem.domain.common.ScanResponse -import com.tangem.network.common.MoshiConverter +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.operations.wallet.CreateWalletResponse import com.tangem.tap.common.AssetReader import com.tangem.tap.tangemSdkManager diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt index 3dc337ae61..62ea1a37a4 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListManager.kt @@ -19,8 +19,14 @@ interface UserWalletsListManager { suspend fun selectWallet(walletId: UserWalletId): CompletionResult - suspend fun save(userWallet: UserWallet): CompletionResult - suspend fun update(userWallet: UserWallet): CompletionResult + /** + * Save user's wallet + * @param userWallet User's wallet to save + * @param canOverride If false, then terminate with [UserWalletListError.WalletAlreadySaved] when user tries to save an + * already saved card + * @return [CompletionResult] operation result + * */ + suspend fun save(userWallet: UserWallet, canOverride: Boolean = false): CompletionResult suspend fun delete(walletIds: List): CompletionResult suspend fun clear(): CompletionResult diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt index d7ab7b396d..ad4c7597c9 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt @@ -110,12 +110,36 @@ internal class BiometricUserWalletsListManager( findSelectedWallet()!! } - override suspend fun save(userWallet: UserWallet): CompletionResult { - return saveInternal(userWallet, override = false) - } + override suspend fun save( + userWallet: UserWallet, + canOverride: Boolean, + ): CompletionResult = withUnlock { + val isWalletSaved = state.value.wallets + .filter { it.isSaved } + .flatMap(UserWallet::cardsInWallet) + .contains(userWallet.cardId) - override suspend fun update(userWallet: UserWallet): CompletionResult { - return saveInternal(userWallet, override = true) + if (isWalletSaved && !canOverride) { + CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) + } else { + keysRepository.save( + walletId = userWallet.walletId, + encryptionKey = userWallet.scanResponse.card.encryptionKey, + ) + .doOnSuccess { keys -> + state.update { prevState -> + prevState.copy( + encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId }, + ) + } + } + .flatMap { publicInformationRepository.save(userWallet) } + .flatMap { sensitiveInformationRepository.save(userWallet) } + .flatMap { loadModels() } + .doOnSuccess { + userWallet.isSaved = true + } + } } override suspend fun delete(walletIds: List): CompletionResult { @@ -162,38 +186,6 @@ internal class BiometricUserWalletsListManager( } } - private suspend fun saveInternal( - userWallet: UserWallet, - override: Boolean, - ): CompletionResult = withUnlock { - val isWalletSaved = state.value.wallets - .filter { it.isSaved } - .flatMap(UserWallet::cardsInWallet) - .contains(userWallet.cardId) - - if (isWalletSaved && !override) { - CompletionResult.Failure(UserWalletListError.WalletAlreadySaved) - } else { - keysRepository.save( - walletId = userWallet.walletId, - encryptionKey = userWallet.scanResponse.card.encryptionKey, - ) - .doOnSuccess { keys -> - state.update { prevState -> - prevState.copy( - encryptionKeys = (keys + prevState.encryptionKeys).distinctBy { it.walletId }, - ) - } - } - .flatMap { publicInformationRepository.save(userWallet) } - .flatMap { sensitiveInformationRepository.save(userWallet) } - .flatMap { loadModels() } - .doOnSuccess { - userWallet.isSaved = true - } - } - } - private suspend inline fun withUnlock( block: () -> CompletionResult, ): CompletionResult { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt index 5135966db1..a3c3a3e929 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/DummyUserWalletsListManager.kt @@ -40,11 +40,7 @@ class DummyUserWalletsListManager : UserWalletsListManager { } } - override suspend fun save(userWallet: UserWallet): CompletionResult { - return CompletionResult.Success(Unit) - } - - override suspend fun update(userWallet: UserWallet): CompletionResult { + override suspend fun save(userWallet: UserWallet, canOverride: Boolean): CompletionResult { return CompletionResult.Success(Unit) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt index c664c46c0a..837ff8452c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt @@ -187,8 +187,8 @@ internal class DefaultWalletCurrenciesManager( .flatMap { walletManager -> walletStoresRepository.storeOrUpdate( userWalletId = userWalletId, - walletStore = WalletStoreBuilder(userWallet, walletManager) - .blockchainNetwork(blockchainNetwork) + walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork) + .walletManager(walletManager) .build(), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt index 52224a9f13..45a7f0e0f9 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/implementation/DefaultWalletStoresManager.kt @@ -122,7 +122,7 @@ internal class DefaultWalletStoresManager( { walletManager -> walletStoresRepository.storeOrUpdate( userWalletId = userWalletId, - walletStore = WalletStoreBuilder(userWallet, blockchainNetwork) + walletStore = WalletStoreBuilder(userWalletId, blockchainNetwork) .walletManager(walletManager) .build(), ) @@ -158,9 +158,10 @@ internal class DefaultWalletStoresManager( refresh = refresh, ) .flatMap { walletManager -> + val userWalletId = userWallet.walletId walletStoresRepository.storeOrUpdate( - userWalletId = userWallet.walletId, - walletStore = WalletStoreBuilder(userWallet, walletManager) + userWalletId = userWalletId, + walletStore = WalletStoreBuilder(userWalletId, walletManager) .build(), ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/di/RepositoryProvider.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/di/RepositoryProvider.kt index 315606c659..7fc7ba38e7 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/di/RepositoryProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/di/RepositoryProvider.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.walletStores.repository.di import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt index df28ac9d4c..fd8a037dba 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt @@ -14,9 +14,9 @@ import com.tangem.common.flatMap import com.tangem.common.flatMapOnFailure import com.tangem.common.map import com.tangem.common.services.Result +import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.domain.common.ScanResponse import com.tangem.domain.common.util.UserWalletId -import com.tangem.network.api.tangemTech.TangemTechService import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.replaceByOrAdd import com.tangem.tap.domain.model.UserWallet diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt index 543640ad2f..c796b81261 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt @@ -7,7 +7,7 @@ import com.squareup.moshi.JsonClass import com.squareup.moshi.Types import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toHexString -import com.tangem.network.common.MoshiConverter +import com.tangem.datasource.api.common.MoshiConverter import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession import com.tangem.tap.features.details.redux.walletconnect.WalletForSession import com.trustwallet.walletconnect.models.WCPeerMeta @@ -18,7 +18,7 @@ import java.nio.charset.Charset class WalletConnectRepository(val context: Application) { private val moshi = MoshiConverter.defaultMoshi() private val walletConnectAdapter: JsonAdapter> = moshi.adapter( - Types.newParameterizedType(List::class.java, SessionDao::class.java) + Types.newParameterizedType(List::class.java, SessionDao::class.java), ) fun saveSession(session: WalletConnectSession) { @@ -76,7 +76,6 @@ class WalletConnectRepository(val context: Application) { } } - @JsonClass(generateAdapter = true) data class SessionDao( val peerId: String, @@ -91,7 +90,7 @@ data class SessionDao( remotePeerId = remotePeerId, wallet = wallet, session = session, - peerMeta = peerMeta + peerMeta = peerMeta, ) } @@ -102,7 +101,7 @@ data class SessionDao( remotePeerId = session.remotePeerId, wallet = session.wallet, session = session.session, - peerMeta = session.peerMeta + peerMeta = session.peerMeta, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index bf8afa7f53..d08fd627f5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -55,10 +55,14 @@ sealed class DetailsAction : Action { ) : AppSettings() } - object EnrollBiometrics : AppSettings() { - object Enroll : AppSettings() - object Cancel : AppSettings() - } + data class CheckBiometricsStatus( + val awaitStatusChange: Boolean, + ) : AppSettings() + + object EnrollBiometrics : AppSettings() + data class BiometricsStatusChanged( + val needEnrollBiometrics: Boolean, + ) : AppSettings() } data class ChangeAppCurrency(val fiatCurrency: FiatCurrency) : DetailsAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 11cd4bb5f4..70751b615f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -30,6 +30,7 @@ import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletStoresManager import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.rekotlin.Action @@ -77,11 +78,7 @@ class DetailsMiddleware { } DetailsAction.ScanCard -> { scope.launch { - tangemSdkManager.scanCard( - cardId = state.scanResponse?.card?.cardId, - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - state.scanResponse?.card?.isAccessCodeSet == true, - ) + tangemSdkManager.scanCard(cardId = state.scanResponse?.card?.cardId) .doOnSuccess { card -> val currentCardId = store.state.globalState.scanResponse?.card ?.userWalletId @@ -121,12 +118,15 @@ class DetailsMiddleware { .flatMap { userWalletsListManager.delete(listOf(card.userWalletId)) } .doOnSuccess { Analytics.send(Settings.CardSettings.FactoryResetFinished()) - val screen = if (userWalletsListManager.hasSavedUserWallets) { - AppScreen.Welcome + + val selectedUserWallet = userWalletsListManager.selectedUserWalletSync + if (selectedUserWallet != null) { + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.onUserWalletSelected(selectedUserWallet) } else { - AppScreen.Home + userWalletsListManager.lock() + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Home)) } - store.dispatchOnMain(NavigationAction.PopBackTo(screen)) } .doOnFailure { error -> (error as? TangemSdkError)?.let { sdkError -> @@ -195,18 +195,39 @@ class DetailsMiddleware { fun handle(state: DetailsState, action: DetailsAction.AppSettings) { when (action) { is DetailsAction.AppSettings.SwitchPrivacySetting -> { - if (tangemSdkManager.canEnrollBiometrics) { - store.dispatch(DetailsAction.AppSettings.EnrollBiometrics) - } when (action.setting) { PrivacySetting.SaveWallets -> toggleSaveWallets(state, enable = action.enable) PrivacySetting.SaveAccessCode -> toggleSaveAccessCodes(state, enable = action.enable) } } - is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> Unit - is DetailsAction.AppSettings.EnrollBiometrics -> Unit - is DetailsAction.AppSettings.EnrollBiometrics.Enroll -> enrollBiometrics() - is DetailsAction.AppSettings.EnrollBiometrics.Cancel -> Unit + is DetailsAction.AppSettings.CheckBiometricsStatus -> { + checkBiometricsStatus(action.awaitStatusChange, state) + } + is DetailsAction.AppSettings.EnrollBiometrics -> { + enrollBiometrics() + } + is DetailsAction.AppSettings.SwitchPrivacySetting.Success, + is DetailsAction.AppSettings.BiometricsStatusChanged, + -> Unit + } + } + + /** + * @param awaitStatusChange If true then start a new coroutine and check the biometric status every 100 + * milliseconds until it changes + * */ + private fun checkBiometricsStatus(awaitStatusChange: Boolean, state: DetailsState) { + scope.launch { + if (awaitStatusChange) { + while (state.needEnrollBiometrics == tangemSdkManager.needEnrollBiometrics) { + delay(timeMillis = 100) + } + } + store.dispatchOnMain( + DetailsAction.AppSettings.BiometricsStatusChanged( + needEnrollBiometrics = tangemSdkManager.needEnrollBiometrics, + ), + ) } } @@ -217,7 +238,7 @@ class DetailsMiddleware { private fun toggleSaveWallets(state: DetailsState, enable: Boolean) = scope.launch { if (state.saveWallets == enable) return@launch if (enable) { - saveCurrentWallet() + saveCurrentWallet(state) } else { deleteSavedWallets() if (state.saveAccessCodes) { @@ -230,16 +251,16 @@ class DetailsMiddleware { if (state.saveAccessCodes == enable) return@launch if (enable) { if (!state.saveWallets) { - saveCurrentWallet() + saveCurrentWallet(state) } - saveAccessCodes() + saveAccessCodes(state) } else { deleteSavedAccessCodes() } } - private suspend fun saveCurrentWallet() { - val scanResponse = store.state.detailsState.scanResponse ?: return + private suspend fun saveCurrentWallet(state: DetailsState) { + val scanResponse = state.scanResponse ?: return val userWallet = UserWalletBuilder(scanResponse).build() userWalletsListManager.save(userWallet) @@ -247,7 +268,7 @@ class DetailsMiddleware { Timber.e(error, "Wallet saving failed") } .doOnSuccess { - preferencesStorage.shouldShowSaveWallet = false + preferencesStorage.shouldShowSaveUserWalletScreen = false preferencesStorage.shouldSaveUserWallets = true store.dispatchOnMain( DetailsAction.AppSettings.SwitchPrivacySetting.Success( @@ -274,8 +295,11 @@ class DetailsMiddleware { } } - private fun saveAccessCodes() { + private fun saveAccessCodes(state: DetailsState) { preferencesStorage.shouldSaveAccessCodes = true + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = state.scanResponse?.card?.isAccessCodeSet == true, + ) store.dispatchOnMain( DetailsAction.AppSettings.SwitchPrivacySetting.Success( setting = PrivacySetting.SaveAccessCode, @@ -288,6 +312,9 @@ class DetailsMiddleware { tangemSdkManager.clearSavedUserCodes() .doOnSuccess { preferencesStorage.shouldSaveAccessCodes = false + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = false, + ) store.dispatchOnMain( DetailsAction.AppSettings.SwitchPrivacySetting.Success( setting = PrivacySetting.SaveAccessCode, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index 28d1b87cb8..f00f0b46b4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -157,15 +157,17 @@ private fun handlePrivacyAction( state: DetailsState, ): DetailsState { return when (action) { - is DetailsAction.AppSettings.SwitchPrivacySetting -> state is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> when (action.setting) { PrivacySetting.SaveWallets -> state.copy(saveWallets = action.enable) PrivacySetting.SaveAccessCode -> state.copy(saveAccessCodes = action.enable) } - is DetailsAction.AppSettings.EnrollBiometrics -> state.copy(needEnrollBiometrics = true) - is DetailsAction.AppSettings.EnrollBiometrics.Enroll, - is DetailsAction.AppSettings.EnrollBiometrics.Cancel, - -> state.copy(needEnrollBiometrics = false) + is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( + needEnrollBiometrics = action.needEnrollBiometrics, + ) + is DetailsAction.AppSettings.SwitchPrivacySetting, + is DetailsAction.AppSettings.EnrollBiometrics, + is DetailsAction.AppSettings.CheckBiometricsStatus, + -> state } } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt index 433db0dd49..25a6cd0902 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsFragment.kt @@ -9,11 +9,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store +import com.tangem.wallet.R import org.rekotlin.StoreSubscriber class AppSettingsFragment : Fragment(), StoreSubscriber { @@ -24,8 +25,9 @@ class AppSettingsFragment : Fragment(), StoreSubscriber { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(android.R.transition.fade) - exitTransition = inflater.inflateTransition(android.R.transition.fade) + enterTransition = inflater.inflateTransition(R.transition.fade) + exitTransition = inflater.inflateTransition(R.transition.fade) + viewModel.checkBiometricsStatus() } override fun onCreateView( @@ -36,7 +38,7 @@ class AppSettingsFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { AppSettingsScreen( state = screenState.value, onBackPressed = { @@ -58,6 +60,11 @@ class AppSettingsFragment : Fragment(), StoreSubscriber { } } + override fun onResume() { + super.onResume() + viewModel.refreshBiometricsStatus() + } + override fun onStop() { super.onStop() store.unsubscribe(this) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index afdd0de6e1..b072be7b74 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -1,24 +1,31 @@ package com.tangem.tap.features.details.ui.appsettings -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CornerSize -import androidx.compose.material.AlertDialog -import androidx.compose.material.MaterialTheme +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.Text -import androidx.compose.material.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent -import com.tangem.core.ui.models.EnrollBiometricsDialog -import com.tangem.tap.common.compose.TangemTypography +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW32 +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.redux.PrivacySetting +import com.tangem.tap.features.details.ui.appsettings.components.EnrollBiometricsCard +import com.tangem.tap.features.details.ui.appsettings.components.SettingsAlertDialog import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold import com.tangem.tap.features.details.ui.common.TangemSwitch import com.tangem.wallet.R @@ -51,47 +58,43 @@ private fun AppSettings( element = it, onDialogStateChange = onDialogStateChange, onSettingToggled = state.onSettingToggled, - modifier = modifier, ) } - EnrollBiometricsDialog(dialog = state.enrollBiometricsDialog) - Column( modifier = modifier .fillMaxSize(), ) { + if (state.showEnrollBiometricsCard) { + EnrollBiometricsCard( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing8) + .fillMaxWidth(), + onClick = state.onEnrollBiometrics, + ) + SpacerH24() + } + AppSettingsElement( state = state, setting = PrivacySetting.SaveWallets, onDialogStateChange = onDialogStateChange, - modifier = modifier, ) - Spacer(modifier = Modifier.size(32.dp)) + SpacerH32() AppSettingsElement( state = state, setting = PrivacySetting.SaveAccessCode, onDialogStateChange = onDialogStateChange, - modifier = modifier, ) } } -@Composable -private fun EnrollBiometricsDialog( - modifier: Modifier = Modifier, - dialog: EnrollBiometricsDialog?, -) { - if (dialog == null) return - EnrollBiometricsDialogContent(modifier, dialog) -} - @Composable private fun AppSettingsElement( + modifier: Modifier = Modifier, state: AppSettingsScreenState, setting: PrivacySetting, onDialogStateChange: (PrivacySetting?) -> Unit, - modifier: Modifier = Modifier, ) { val titleRes = when (setting) { PrivacySetting.SaveWallets -> R.string.app_settings_saved_wallet @@ -103,48 +106,63 @@ private fun AppSettingsElement( } val checked = state.settings[setting] ?: false + val titleTextColor by rememberUpdatedState( + newValue = if (state.isTogglesEnabled) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + val descriptionTextColor by rememberUpdatedState( + newValue = if (state.isTogglesEnabled) { + TangemTheme.colors.text.secondary + } else { + TangemTheme.colors.text.tertiary + }, + ) + Row( modifier = modifier .fillMaxWidth() - .padding(start = 20.dp), + .padding(horizontal = TangemTheme.dimens.spacing20), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { Column( + modifier = Modifier.weight(weight = .9f), verticalArrangement = Arrangement.Center, - modifier = modifier - .weight(0.6f) - .padding(end = 4.dp), ) { Text( text = stringResource(id = titleRes), - style = TangemTypography.subtitle1, - color = colorResource(id = R.color.text_primary_1), + style = TangemTheme.typography.subtitle1, + color = titleTextColor, ) - Spacer(modifier = Modifier.size(4.dp)) + SpacerH4() Text( text = stringResource(id = subtitleRes), - style = TangemTypography.body2, - color = colorResource(id = R.color.text_secondary), + style = TangemTheme.typography.body2, + color = descriptionTextColor, ) } + SpacerW32() TangemSwitch( checked = checked, - onCheckedChange = { + enabled = state.isTogglesEnabled, + onCheckedChange = { isChecked -> onCheckedChange( element = setting, - enabled = it, + enabled = isChecked, onSettingToggled = state.onSettingToggled, onDialogStateChange = onDialogStateChange, ) }, - modifier = modifier - .padding(20.dp), ) } } private fun onCheckedChange( - element: PrivacySetting, enabled: Boolean, + element: PrivacySetting, + enabled: Boolean, onSettingToggled: (PrivacySetting, Boolean) -> Unit, onDialogStateChange: (PrivacySetting?) -> Unit, ) { @@ -152,82 +170,88 @@ private fun onCheckedChange( if (!enabled) { onDialogStateChange(element) } else { - onSettingToggled(element, enabled) + onSettingToggled(element, true) } } +// region Preview @Composable -private fun SettingsAlertDialog( - element: PrivacySetting, - onDialogStateChange: (PrivacySetting?) -> Unit, - onSettingToggled: (PrivacySetting, Boolean) -> Unit, +private fun AppSettingsScreenSample( modifier: Modifier = Modifier, ) { - val text = when (element) { - PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message - PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + AppSettingsScreen( + state = AppSettingsScreenState( + settings = mapOf( + PrivacySetting.SaveWallets to true, + PrivacySetting.SaveAccessCode to false, + ), + showEnrollBiometricsCard = false, + isTogglesEnabled = true, + onSettingToggled = { _, _ -> }, + onEnrollBiometrics = {}, + ), + onBackPressed = { }, + ) } - AlertDialog( - onDismissRequest = { onDialogStateChange(null) }, - confirmButton = { - TextButton( - onClick = { - onDialogStateChange(null) - }, - ) { - Text( - text = stringResource(id = R.string.common_cancel), - color = colorResource(id = R.color.text_secondary), - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - ) - } - }, - dismissButton = { - TextButton( - onClick = { - onDialogStateChange(null) - onSettingToggled(element, false) - }, - modifier = modifier.padding(bottom = 14.dp), - ) { - Text( - text = stringResource(id = R.string.common_delete), - color = colorResource(id = R.color.text_warning), - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - ) - } - }, - title = { - Text( - text = stringResource(id = R.string.common_attention), - color = colorResource(id = R.color.text_primary_1), - ) - }, - text = { - Text( - text = stringResource(id = text), - color = colorResource(id = R.color.text_secondary), - ) - }, - shape = MaterialTheme.shapes.medium.copy(all = CornerSize(size = 28.dp)), - modifier = modifier.padding(vertical = 34.dp, horizontal = 24.dp), - ) } -@Preview +@Preview(showBackground = true, widthDp = 360) @Composable -fun AppSettingsScreenPreview() { - AppSettingsScreen( - state = AppSettingsScreenState( - settings = mapOf( - PrivacySetting.SaveWallets to true, - PrivacySetting.SaveAccessCode to false, +private fun AppSettingsScreenPreview_Light() { + TangemTheme { + AppSettingsScreenSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun AppSettingsScreenPreview_Dark() { + TangemTheme(isDark = true) { + AppSettingsScreenSample() + } +} + +@Composable +private fun AppSettingsScreen_EnrollBiometrics_Sample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + AppSettingsScreen( + state = AppSettingsScreenState( + settings = mapOf( + PrivacySetting.SaveWallets to true, + PrivacySetting.SaveAccessCode to false, + ), + showEnrollBiometricsCard = true, + isTogglesEnabled = false, + onSettingToggled = { _, _ -> }, + onEnrollBiometrics = {}, ), - onSettingToggled = { _, _ -> }, - enrollBiometricsDialog = null, - ), - onBackPressed = { }, - ) -} \ No newline at end of file + onBackPressed = { }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun AppSettingsScreen_EnrollBiometrics_Preview_Light() { + TangemTheme { + AppSettingsScreen_EnrollBiometrics_Sample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun AppSettingsScreen_EnrollBiometrics_Preview_Dark() { + TangemTheme(isDark = true) { + AppSettingsScreen_EnrollBiometrics_Sample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index 1e4c1abeee..615a0a167d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -1,10 +1,11 @@ package com.tangem.tap.features.details.ui.appsettings -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.tap.features.details.redux.PrivacySetting data class AppSettingsScreenState( - val settings: Map, - val enrollBiometricsDialog: EnrollBiometricsDialog?, - val onSettingToggled: (PrivacySetting, Boolean) -> Unit, + val settings: Map = emptyMap(), + val showEnrollBiometricsCard: Boolean = false, + val isTogglesEnabled: Boolean = true, + val onSettingToggled: (PrivacySetting, Boolean) -> Unit = { _, _ -> /* no-op */ }, + val onEnrollBiometrics: () -> Unit = { /* no-op */ }, ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt index 16a6ebdbba..802435f578 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsViewModel.kt @@ -1,6 +1,5 @@ package com.tangem.tap.features.details.ui.appsettings -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.DetailsAction @@ -20,8 +19,14 @@ class AppSettingsViewModel(private val store: Store) { PrivacySetting.SaveWallets to state.saveWallets, PrivacySetting.SaveAccessCode to state.saveAccessCodes, ), - enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null, - onSettingToggled = { privacySetting, enabled -> onSettingsToggled(privacySetting, enabled) }, + showEnrollBiometricsCard = state.needEnrollBiometrics, + isTogglesEnabled = !state.needEnrollBiometrics, + onSettingToggled = { privacySetting, enabled -> + onSettingsToggled(privacySetting, enabled) + }, + onEnrollBiometrics = { + store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) + }, ) } @@ -29,12 +34,11 @@ class AppSettingsViewModel(private val store: Store) { store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) } - private fun createEnrollBiometricsDialog() = EnrollBiometricsDialog( - onCancel = { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Cancel) - }, - onEnroll = { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics.Enroll) - }, - ) + fun checkBiometricsStatus() { + store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = false)) + } + + fun refreshBiometricsStatus() { + store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(awaitStatusChange = true)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt new file mode 100644 index 0000000000..fddc2b0e7b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/EnrollBiometricsCard.kt @@ -0,0 +1,92 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerW16 +import com.tangem.core.ui.res.TangemTheme +import com.tangem.wallet.R + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun EnrollBiometricsCard( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Surface( + modifier = modifier, + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersLarge, + onClick = onClick, + ) { + Row( + modifier = Modifier.padding(all = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_alert_circle_24), + tint = TangemTheme.colors.icon.attention, + contentDescription = null, + ) + SpacerW16() + Column { + Text( + text = stringResource(id = R.string.app_settings_enable_biometrics_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerH4() + Text( + text = stringResource(id = R.string.app_settings_enable_biometrics_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } + } +} + +// region Preview +@Composable +private fun EnrollBiometricsCardSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.secondary), + ) { + EnrollBiometricsCard(onClick = {}) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EnrollBiometricsCardPreview_Light() { + TangemTheme { + EnrollBiometricsCardSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EnrollBiometricsCardPreview_Dark() { + TangemTheme(isDark = true) { + EnrollBiometricsCardSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt new file mode 100644 index 0000000000..65514a2f86 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt @@ -0,0 +1,101 @@ +package com.tangem.tap.features.details.ui.appsettings.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.AlertDialog +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.TextButton +import com.tangem.core.ui.components.WarningTextButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.details.redux.PrivacySetting +import com.tangem.wallet.R + +@Composable +internal fun SettingsAlertDialog( + element: PrivacySetting, + onDialogStateChange: (PrivacySetting?) -> Unit, + onSettingToggled: (PrivacySetting, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val text = when (element) { + PrivacySetting.SaveWallets -> R.string.app_settings_off_saved_wallet_alert_message + PrivacySetting.SaveAccessCode -> R.string.app_settings_off_saved_access_code_alert_message + } + + AlertDialog( + onDismissRequest = { onDialogStateChange(null) }, + confirmButton = { + TextButton( + modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), + text = stringResource(id = R.string.common_cancel), + onClick = { + onDialogStateChange(null) + }, + ) + }, + dismissButton = { + WarningTextButton( + text = stringResource(id = R.string.common_delete), + onClick = { + onDialogStateChange(null) + onSettingToggled(element, false) + }, + ) + }, + title = { + Text( + text = stringResource(id = R.string.common_attention), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.h2, + ) + }, + text = { + Text( + text = stringResource(id = text), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + ) + }, + shape = TangemTheme.shapes.roundedCornersLarge, + modifier = modifier, + ) +} + +// region Preview +@Composable +private fun SettingsAlertDialogSample( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors.background.primary), + ) { + SettingsAlertDialog( + element = PrivacySetting.SaveAccessCode, + onDialogStateChange = {}, + onSettingToggled = { _, _ -> }, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsAlertDialogPreview_Light() { + TangemTheme { + SettingsAlertDialogSample() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun SettingsAlertDialogPreview_Dark() { + TangemTheme(isDark = true) { + SettingsAlertDialogSample() + } +} +// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt index a679020e84..a42f4a1d38 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsFragment.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.details.redux.DetailsState @@ -39,7 +39,7 @@ class CardSettingsFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { CardSettingsScreen( state = screenState.value, onBackPressed = { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt index 2de4f118fb..31941ed1ee 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt @@ -15,6 +15,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.layout.ContentScale @@ -23,6 +25,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.compose.TangemTypography import com.tangem.tap.features.details.ui.common.DetailsMainButton import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold @@ -34,14 +37,22 @@ fun CardSettingsScreen( onBackPressed: () -> Unit, modifier: Modifier = Modifier, ) { + val needReadCard = state.cardDetails == null + val backgroundColor by rememberUpdatedState( + newValue = if (needReadCard) TangemTheme.colors.background.primary + else TangemTheme.colors.background.secondary, + ) + SettingsScreensScaffold( - content = - if (state.cardDetails == null) { - { CardSettingsReadCard(state.onScanCardClick, modifier = modifier) } - } else { - { CardSettings(state = state, modifier = modifier) } + content = { + if (needReadCard) { + CardSettingsReadCard(state.onScanCardClick, modifier = modifier) + } else { + CardSettings(state = state, modifier = modifier) + } }, titleRes = R.string.card_settings_title, + backgroundColor = backgroundColor, onBackClick = onBackPressed, ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt index d8e6bf1b11..dd6f290ebe 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt @@ -25,18 +25,19 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.compose.TangemTypography import com.tangem.wallet.R @Composable fun SettingsScreensScaffold( + modifier: Modifier = Modifier, content: @Composable () -> Unit, background: @Composable (() -> Unit)? = null, fab: @Composable (() -> Unit)? = null, - backgroundColor: Color = colorResource(id = R.color.background_primary), + backgroundColor: Color = TangemTheme.colors.background.secondary, titleRes: Int? = null, onBackClick: () -> Unit, - modifier: Modifier = Modifier, ) { BackHandler(true, onBackClick) @@ -58,9 +59,13 @@ fun SettingsScreensScaffold( Column(modifier = modifier.fillMaxWidth()) { Text( text = stringResource(id = titleRes), - modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp), - style = TangemTypography.headline1, - color = colorResource(id = R.color.text_primary_1), + modifier = modifier.padding( + start = TangemTheme.dimens.spacing20, + end = TangemTheme.dimens.spacing20, + bottom = TangemTheme.dimens.spacing54, + ), + style = TangemTheme.typography.h1, + color = TangemTheme.colors.text.primary1, ) content() } @@ -86,18 +91,20 @@ fun ScreenTitle( @Composable fun EmptyTopBarWithNavigation( - onBackClick: () -> Unit, - backgroundColor: Color = colorResource(id = R.color.background_primary), modifier: Modifier = Modifier, + onBackClick: () -> Unit, + backgroundColor: Color = TangemTheme.colors.background.primary, ) { TopAppBar( + modifier = modifier, title = { }, navigationIcon = { IconButton(onClick = onBackClick) { Icon( - painterResource(id = R.drawable.ic_back), "", - tint = colorResource(id = R.color.icon_primary_1), + painter = painterResource(id = R.drawable.ic_back), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, ) } }, @@ -108,10 +115,10 @@ fun EmptyTopBarWithNavigation( @Composable fun DetailsMainButton( + modifier: Modifier = Modifier, title: String, enabled: Boolean = true, onClick: (() -> Unit), - modifier: Modifier = Modifier, ) { Button( onClick = onClick, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt index bf5268bf6f..d511348af9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/TangemSwitch.kt @@ -33,10 +33,11 @@ import com.tangem.wallet.R @Composable fun TangemSwitch( modifier: Modifier = Modifier, - enabledColor: Color = colorResource(id = R.color.control_checked), - disabledColor: Color = colorResource(id = R.color.icon_informative), + checkedColor: Color = colorResource(id = R.color.control_checked), + uncheckedColor: Color = colorResource(id = R.color.icon_informative), size: Dp = 48.dp, checked: Boolean = false, + enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit, ) { val transition = updateTransition(checked, label = "SwitchState") @@ -45,8 +46,8 @@ fun TangemSwitch( tween(durationMillis = 200, easing = FastOutLinearInEasing) }, label = "", - ) { enabled -> - if (enabled) enabledColor else disabledColor + ) { isChecked -> + if (isChecked) checkedColor else uncheckedColor } val interactionSource = remember { MutableInteractionSource() } @@ -55,6 +56,7 @@ fun TangemSwitch( .clickable( interactionSource = interactionSource, indication = null, + enabled = enabled, ) { onCheckedChange(!checked) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt index 2c206736e5..4cecec1a8f 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsFragment.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.redux.navigation.NavigationAction @@ -40,7 +40,7 @@ class DetailsFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { DetailsScreen( state = detailsScreenState.value, onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt index 68da6ac85f..e55525ae1d 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsScreenState.kt @@ -30,7 +30,7 @@ enum class SettingsElement( LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup), TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App TermsOfUse(R.drawable.ic_text, R.string.details_row_title_card_tou), // Terms of Use for S2C cards only - PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy); + PrivacyPolicy(R.drawable.ic_lock_24, R.string.details_row_privacy_policy); } @Immutable diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt index fa63874859..10270c8ecb 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardFragment.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store @@ -38,7 +38,7 @@ class ResetCardFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { ResetCardScreen( state = screenState.value, onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt index 96d55a808a..567c74c007 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeFragment.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.store @@ -38,7 +38,7 @@ class SecurityModeFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { SecurityModeScreen( state = screenState.value, onBackPressed = { store.dispatch(NavigationAction.PopBackTo()) }, diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt index 8d3de292ae..ec42337992 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/walletconnect/WalletConnectFragment.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.transition.TransitionInflater -import com.google.accompanist.appcompattheme.AppCompatTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState @@ -36,7 +36,7 @@ class WalletConnectFragment : Fragment(), StoreSubscriber { return ComposeView(requireContext()).apply { setContent { isTransitionGroup = true - AppCompatTheme { + TangemTheme { WalletConnectScreen( state = screenState.value, onBackPressed = { diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 88a3e28b3c..450d428367 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -4,6 +4,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf @@ -42,6 +43,10 @@ class HomeFragment : Fragment(), StoreSubscriber { return ComposeView(inflater.context).apply { setContent { + BackHandler { + requireActivity().finish() + } + AppCompatTheme { ScreenContent() } diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index c33b6d9a9b..8f4282fed9 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -22,6 +22,7 @@ import com.tangem.tap.features.send.redux.states.ButtonState import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -71,8 +72,10 @@ private fun handleHomeAction(action: Action) { private fun readCard() = scope.launch { delay(timeMillis = 200) - ScanCardProcessor.scan( + tangemSdkManager.setAccessCodeRequestPolicy( useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, + ) + ScanCardProcessor.scan( onProgressStateChange = { showProgress -> if (showProgress) { changeButtonState(ButtonState.PROGRESS) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 2fe2d59e78..e14d6cff7d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -11,7 +11,6 @@ import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager -import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -56,8 +55,7 @@ class OnboardingHelper { backupCardsIds: List? = null, ) { when { - userWalletsListManager.hasSavedUserWallets -> scope.launch { - delay(timeMillis = 1_200) + preferencesStorage.shouldSaveUserWallets -> scope.launch { store.dispatchOnMain( SaveWalletAction.ProvideBackupInfo( scanResponse = scanResponse, @@ -68,7 +66,7 @@ class OnboardingHelper { store.dispatchOnMain(SaveWalletAction.Save) } tangemSdkManager.canUseBiometry && - preferencesStorage.shouldShowSaveWallet -> scope.launch { + preferencesStorage.shouldShowSaveUserWalletScreen -> scope.launch { delay(timeMillis = 1_200) store.dispatchOnMain( SaveWalletAction.ProvideBackupInfo( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt index 04933d0a46..187425c2af 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt @@ -12,13 +12,13 @@ import com.tangem.common.extensions.isZero import com.tangem.common.services.Result import com.tangem.domain.common.extensions.successOr import com.tangem.domain.common.util.UserWalletId -import com.tangem.network.api.paymentology.AttestationResponse -import com.tangem.network.api.paymentology.PaymentologyApiService -import com.tangem.network.api.paymentology.RegisterKYCRequest -import com.tangem.network.api.paymentology.RegisterWalletRequest -import com.tangem.network.api.paymentology.RegisterWalletResponse -import com.tangem.network.api.paymentology.RegistrationResponse -import com.tangem.network.api.paymentology.tryExtractError +import com.tangem.datasource.api.paymentology.AttestationResponse +import com.tangem.datasource.api.paymentology.PaymentologyApiService +import com.tangem.datasource.api.paymentology.RegisterKYCRequest +import com.tangem.datasource.api.paymentology.RegisterWalletRequest +import com.tangem.datasource.api.paymentology.RegisterWalletResponse +import com.tangem.datasource.api.paymentology.RegistrationResponse +import com.tangem.datasource.api.paymentology.tryExtractError import com.tangem.operations.attestation.AttestWalletKeyResponse import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.domain.getFirstToken @@ -152,12 +152,13 @@ class SaltPayActivationManager( ) } - private fun makeRegisterKYCRequest(): RegisterKYCRequest = RegisterKYCRequest( - cardId = cardId, - publicKey = cardPublicKey, - kycProvider = "UTORG", - kycRefId = kycUrlProvider.kycRefId, - ) + private fun makeRegisterKYCRequest(): RegisterKYCRequest = + RegisterKYCRequest( + cardId = cardId, + publicKey = cardPublicKey, + kycProvider = "UTORG", + kycRefId = kycUrlProvider.kycRefId, + ) companion object { fun stub(): SaltPayActivationManager = SaltPayActivationManager( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt index 69f3e0a674..4d6c4a3c00 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayMiddleware.kt @@ -8,8 +8,8 @@ import com.tangem.common.extensions.guard import com.tangem.common.services.Result import com.tangem.domain.common.extensions.successOr import com.tangem.domain.common.extensions.withMainContext -import com.tangem.network.api.paymentology.KYCStatus -import com.tangem.network.api.paymentology.RegistrationResponse +import com.tangem.datasource.api.paymentology.KYCStatus +import com.tangem.datasource.api.paymentology.RegistrationResponse import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.Onboarding import com.tangem.tap.common.extensions.dispatchDebugErrorNotification diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt index 6acbe4bdd2..e1317622a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/redux/OnboardingSaltPayState.kt @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.common.extensions.guard import com.tangem.domain.common.SaltPayWorkaround import com.tangem.domain.common.ScanResponse -import com.tangem.network.api.paymentology.PaymentologyApiService +import com.tangem.datasource.api.paymentology.PaymentologyApiService import com.tangem.tap.common.toggleWidget.WidgetState import com.tangem.tap.domain.extensions.makeSaltPayWalletManager import com.tangem.tap.features.onboarding.products.wallet.saltPay.GnosisRegistrator diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt index 286b00e96d..bd8b4d0459 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt @@ -52,13 +52,23 @@ internal class SaveWalletMiddleware { } private fun saveWalletIfBiometricsEnrolled(state: SaveWalletState) { - if (tangemSdkManager.canEnrollBiometrics) { + if (tangemSdkManager.needEnrollBiometrics) { store.dispatchOnMain(SaveWalletAction.EnrollBiometrics) } else { saveWallet(state) } } + /** + + * or from [SaveWalletState.backupInfo] if provided from + * [com.tangem.tap.features.onboarding.OnboardingHelper.trySaveWalletAndNavigateToWalletScreen] + * + * If saved user's wallet was selected then pop back to [AppScreen.Wallet] + * or navigate to [AppScreen.WalletSelector] otherwise + * + * TODO: Update that logic after onboarding and backup features refactoring + * */ private fun saveWallet(state: SaveWalletState) { val scanResponse = state.backupInfo?.scanResponse ?: store.state.globalState.scanResponse @@ -69,32 +79,37 @@ internal class SaveWalletMiddleware { .backupCardsIds(state.backupInfo?.backupCardsIds) .build() + val isFirstSavedWallet = !userWalletsListManager.hasSavedUserWallets + saveAccessCodeIfNeeded(state.backupInfo?.accessCode, userWallet.cardsInWallet) - .flatMap { userWalletsListManager.save(userWallet) } + .flatMap { userWalletsListManager.save(userWallet, canOverride = true) } .doOnFailure { error -> store.dispatchOnMain(SaveWalletAction.Save.Error(error)) } .doOnSuccess { preferencesStorage.shouldSaveUserWallets = true - preferencesStorage.shouldSaveAccessCodes = true + + // Enable saving access codes only if this is the first time user save the wallet + preferencesStorage.shouldSaveAccessCodes = isFirstSavedWallet || + preferencesStorage.shouldSaveAccessCodes val isSavedWalletSelected = userWalletsListManager.selectedUserWalletSync?.walletId == userWallet.walletId + store.dispatchOnMain(SaveWalletAction.Save.Success) + if (isSavedWalletSelected) { - store.dispatchOnMain(NavigationAction.PopBackTo()) + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) + store.onUserWalletSelected(userWallet) } else { store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.WalletSelector)) } - - store.dispatchOnMain(SaveWalletAction.Save.Success) - store.onUserWalletSelected(userWallet) } } } private fun saveWalletWasShown() { - preferencesStorage.shouldShowSaveWallet = false + preferencesStorage.shouldShowSaveUserWalletScreen = false } private suspend fun saveAccessCodeIfNeeded( diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt index f3d0d97747..f4663eda6e 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt @@ -6,19 +6,25 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.fragment.app.viewModels -import com.tangem.core.ui.components.dialogs.EnrollBiometricsDialogContent import com.tangem.core.ui.fragments.ComposeBottomSheetFragment -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.resolveReference +import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent +import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { - override val expandedHeightFraction: Float = .95f + override val expandedHeightFraction: Float = .98f private val viewModel by viewModels() diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt index a9ca74fa12..51dbdcadc0 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.saveWallet.ui import androidx.compose.runtime.Immutable -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.tap.features.details.ui.cardsettings.TextReference +import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Immutable internal data class SaveWalletScreenState( diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt index b0807fbf19..14cae87732 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt @@ -1,11 +1,11 @@ package com.tangem.tap.features.saveWallet.ui import androidx.lifecycle.ViewModel -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.tap.features.saveWallet.redux.SaveWalletAction import com.tangem.tap.features.saveWallet.redux.SaveWalletState +import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog import com.tangem.tap.store import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt similarity index 96% rename from core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt rename to app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt index a75943cc04..d79531b7ee 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/dialogs/EnrollBiometricsDialogConent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.components.dialogs +package com.tangem.tap.features.saveWallet.ui.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -17,8 +17,8 @@ import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.SpacerH24 import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.models.EnrollBiometricsDialog import com.tangem.core.ui.res.TangemTheme +import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog @Composable fun EnrollBiometricsDialogContent( diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt index 2451c9234a..40b84a07d4 100644 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt @@ -1,27 +1,32 @@ package com.tangem.tap.features.saveWallet.ui.components import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerH32 +import com.tangem.core.ui.components.SpacerH4 +import com.tangem.core.ui.components.SpacerHHalf +import com.tangem.core.ui.components.SpacerW24 +import com.tangem.core.ui.components.SpacerW8 import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.res.TangemTheme import com.tangem.wallet.R @@ -34,79 +39,132 @@ internal fun SaveWalletScreenContent( onCloseClick: () -> Unit, ) { Column( - modifier = modifier - .padding(horizontal = TangemTheme.dimens.spacing16), + modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally, ) { - Hand() - IconButton( + Header( + modifier = Modifier.fillMaxWidth(), + onCloseClick = onCloseClick, + ) + SpacerHHalf() + Title( modifier = Modifier - .align(Alignment.End) - .padding(all = TangemTheme.dimens.spacing8) - .size(TangemTheme.dimens.size32), - onClick = onCloseClick, + .widthIn(max = TangemTheme.dimens.size200), + ) + SpacerH32() + Description( + modifier = Modifier + .padding( + start = TangemTheme.dimens.spacing34, + end = TangemTheme.dimens.spacing56, + ) + .fillMaxWidth(), + ) + SpacerHHalf() + Footer( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + showProgress = showProgress, + onSaveWalletClick = onSaveWalletClick, + ) + SpacerH16() + } +} + +@Composable +private fun Header( + modifier: Modifier = Modifier, + onCloseClick: () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + ) { + Hand() + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_close), - tint = TangemTheme.colors.icon.secondary, - contentDescription = stringResource(id = R.string.common_cancel), - ) + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onCloseClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_close), + tint = TangemTheme.colors.icon.secondary, + contentDescription = stringResource(id = R.string.common_cancel), + ) + } + SpacerW8() } - Spacer(modifier = Modifier.weight(.7f)) + } +} + +@Composable +private fun Title( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing32), + ) { Icon( modifier = Modifier.size(TangemTheme.dimens.size56), painter = painterResource(id = R.drawable.ic_fingerprint_24), - tint = TangemTheme.colors.icon.informative, + tint = TangemTheme.colors.icon.primary1, contentDescription = null, ) - SpacerH24() - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.text.accent.copy(alpha = .12f), - shape = TangemTheme.shapes.roundedCornersMedium, - ), - ) { - Text( - modifier = Modifier - .padding( - vertical = TangemTheme.dimens.spacing4, - horizontal = TangemTheme.dimens.spacing12, - ), - text = stringResource(R.string.save_user_wallet_agreement_new_feature), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.accent, - textAlign = TextAlign.Center, - ) - } - SpacerH24() Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.onboarding_navbar_save_wallet), + text = stringResource(id = R.string.save_user_wallet_agreement_header_biometrics), style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ) - SpacerH12() - Text( - modifier = Modifier.fillMaxWidth(fraction = .87f), - text = stringResource(R.string.save_user_wallet_agreement_description), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, + } +} + +@Composable +private fun Description( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.Start, + ) { + DescriptionItem( + iconPainter = painterResource(id = R.drawable.ic_face_recognition_24), + title = stringResource(id = R.string.save_user_wallet_agreement_access_title), + description = stringResource(id = R.string.save_user_wallet_agreement_access_description), ) - Spacer(modifier = Modifier.weight(1f)) + DescriptionItem( + iconPainter = painterResource(id = R.drawable.ic_lock_24), + title = stringResource(id = R.string.save_user_wallet_agreement_code_title), + description = stringResource(id = R.string.save_user_wallet_agreement_code_description_biometrics), + ) + } +} + +@Composable +private fun Footer( + modifier: Modifier = Modifier, + showProgress: Boolean, + onSaveWalletClick: () -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { PrimaryButton( modifier = Modifier.fillMaxWidth(), showProgress = showProgress, - text = stringResource( - id = R.string.save_user_wallet_agreement_allow, - stringResource(id = R.string.common_biometric_authentication), - ), + text = stringResource(id = R.string.save_user_wallet_agreement_allow_biometrics), onClick = onSaveWalletClick, ) - SpacerH16() Text( modifier = Modifier.fillMaxWidth(fraction = .7f), text = stringResource(R.string.save_user_wallet_agreement_notice), @@ -114,7 +172,41 @@ internal fun SaveWalletScreenContent( color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) - SpacerH16() + } +} + +@Composable +private fun DescriptionItem( + modifier: Modifier = Modifier, + iconPainter: Painter, + title: String, + description: String, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = iconPainter, + tint = TangemTheme.colors.icon.primary1, + contentDescription = null, + ) + SpacerW24() + Column { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerH4() + Text( + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt similarity index 74% rename from core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt rename to app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt index a48550d1c1..7f785a95cf 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/models/EnrollBiometricsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt @@ -1,4 +1,4 @@ -package com.tangem.core.ui.models +package com.tangem.tap.features.saveWallet.ui.models import androidx.compose.runtime.Immutable diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index eed12cb6b0..8465e9d228 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -35,7 +35,6 @@ import com.tangem.tap.domain.tokens.LoadAvailableCoinsService import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -206,7 +205,6 @@ class TokensMiddleware { val result = tangemSdkManager.derivePublicKeys( cardId = card.cardId, derivations = derivations, - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && card.isAccessCodeSet, ) when (result) { is CompletionResult.Success -> { @@ -288,7 +286,7 @@ class TokensMiddleware { ) scope.launch { - userWalletsListManager.update(updatedUserWallet) + userWalletsListManager.save(updatedUserWallet, canOverride = true) .flatMap { walletCurrenciesManager.addCurrencies( userWallet = updatedUserWallet, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt index 4c5dd07560..146d622d16 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -5,7 +5,7 @@ import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.extensions.toCoinId import com.tangem.domain.common.extensions.toNetworkId import com.tangem.domain.features.addCustomToken.CustomCurrency -import com.tangem.network.api.tangemTech.TokenResponse +import com.tangem.datasource.api.tangemTech.TokenResponse import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.tokens.redux.TokenWithBlockchain diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 1505f4bd2a..be432defdc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -1,7 +1,11 @@ package com.tangem.tap.features.wallet.redux import android.content.Context -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.AddressType import com.tangem.domain.common.CardDTO import com.tangem.tap.common.entities.FiatCurrency @@ -20,7 +24,7 @@ import java.math.BigDecimal sealed class WalletAction : Action { - data class ResetState(val newCardId: String) : WalletAction() + data class ResetState(val newCard: CardDTO) : WalletAction() data class SetIfTestnetCard(val isTestnet: Boolean) : WalletAction() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index cffbcc23bf..b8413753cb 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -14,7 +14,11 @@ import com.tangem.tap.common.toggleWidget.WidgetState import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.domain.tokens.models.BlockchainNetwork import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState -import com.tangem.tap.features.wallet.models.* +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.models.TotalBalance +import com.tangem.tap.features.wallet.models.WalletRent +import com.tangem.tap.features.wallet.models.WalletWarning import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount import com.tangem.tap.features.wallet.redux.reducers.findProgressState import com.tangem.tap.features.wallet.ui.BalanceStatus @@ -43,6 +47,7 @@ data class WalletState( val showBackupWarning: Boolean = false, val missingDerivations: List = emptyList(), val loadingUserTokens: Boolean = false, + val walletCardsCount: Int? = null, ) : StateType { // if you do not delegate - the application crashes on startup, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt index d0720a000c..c60632f60e 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/AppCurrencyMiddleware.kt @@ -1,8 +1,8 @@ package com.tangem.tap.features.wallet.redux.middlewares import com.tangem.common.services.Result -import com.tangem.network.api.tangemTech.CurrenciesResponse -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.TangemTechService import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.MainScreen diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 49b46f98f6..28f6fcc965 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -29,7 +29,6 @@ import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.features.wallet.redux.WalletState import com.tangem.tap.features.wallet.redux.models.WalletDialog import com.tangem.tap.features.wallet.redux.reducers.toWallet -import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.userTokensRepository @@ -183,8 +182,6 @@ class MultiWalletMiddleware { state: WalletState?, ) = scope.launch { ScanCardProcessor.scan( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && - selectedWallet.scanResponse.card.isAccessCodeSet, cardId = selectedWallet.cardId, additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain }, ) { scanResponse -> @@ -192,7 +189,7 @@ class MultiWalletMiddleware { scanResponse = scanResponse, ) - userWalletsListManager.update(userWallet) + userWalletsListManager.save(userWallet, canOverride = true) .doOnSuccess { store.state.globalState.tapWalletManager.loadData(userWallet, refresh = true) } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt index ea7c79345b..83cca6c428 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -294,28 +294,29 @@ class WalletMiddleware { } is WalletAction.UserWalletChanged -> Unit is WalletAction.WalletStoresChanged -> { - scope.launch(Dispatchers.Default) { - fetchTotalFiatBalance(action.walletStores, walletState) - findMissedDerivations(action.walletStores) - tryToShowAppRatingWarning(action.walletStores) - } + fetchTotalFiatBalance(action.walletStores, walletState) + findMissedDerivations(action.walletStores) + tryToShowAppRatingWarning(action.walletStores) } is WalletAction.TotalFiatBalanceChanged -> Unit } } private fun fetchTotalFiatBalance(walletStores: List, state: WalletState) { - scope.launch { - val totalFiatBalance = totalFiatBalanceCalculator.calculate( - prevAmount = state.totalBalance?.fiatAmount ?: BigDecimal.ZERO, + scope.launch(Dispatchers.Default) { + val totalFiatBalance = totalFiatBalanceCalculator.calculateOrNull( + prevAmount = state.totalBalance?.fiatAmount, walletStores = walletStores, ) - store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance)) + + if (totalFiatBalance != null) { + store.dispatchOnMain(WalletAction.TotalFiatBalanceChanged(totalFiatBalance)) + } } } private fun findMissedDerivations(wallStores: List) { - scope.launch { + scope.launch(Dispatchers.Default) { val missedDerivations = wallStores .filter { store -> store.walletsData.any { it.status is WalletDataModel.MissedDerivation } @@ -327,15 +328,17 @@ class WalletMiddleware { } private fun tryToShowAppRatingWarning(walletStores: List) { - warningsMiddleware.tryToShowAppRatingWarning( - hasNonZeroWallets = walletStores - .flatMap { it.walletsData } - .any { it.status.amount.isGreaterThan(BigDecimal.ZERO) }, - ) + scope.launch(Dispatchers.Default) { + warningsMiddleware.tryToShowAppRatingWarning( + hasNonZeroWallets = walletStores + .flatMap { it.walletsData } + .any { it.status.amount.isGreaterThan(BigDecimal.ZERO) }, + ) + } } private fun showSaveWalletIfNeeded() { - if (preferencesStorage.shouldShowSaveWallet + if (preferencesStorage.shouldShowSaveUserWalletScreen && tangemSdkManager.canUseBiometry && store.state.navigationState.backStack.lastOrNull() == AppScreen.Wallet ) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index e9658e14fa..61ea00554a 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -53,7 +53,12 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState) is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) - is WalletAction.ResetState -> newState = WalletState(cardId = action.newCardId) + is WalletAction.ResetState -> { + newState = WalletState( + cardId = action.newCard.cardId, + walletCardsCount = action.newCard.findCardsCount(), + ) + } is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet) is WalletAction.EmptyWallet -> { newState = newState.copy( @@ -333,6 +338,7 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS showBackupWarning = card.isMultiwalletAllowed && card.settings.isBackupAllowed && card.backupStatus == CardDTO.BackupStatus.NoBackup, + walletCardsCount = card.findCardsCount(), ) } is WalletAction.WalletStoresChanged -> { @@ -366,6 +372,11 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS return newState } +private fun CardDTO.findCardsCount(): Int? { + return (this.backupStatus as? CardDTO.BackupStatus.Active)?.cardCount?.inc() + ?.takeIf { this.isMultiwalletAllowed } +} + @JvmName("walletStoreModelToReduxModel") private fun List.mapToReduxModel( isMultiWalletAllowed: Boolean, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 6703abd9a9..2569627f80 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -3,13 +3,16 @@ package com.tangem.tap.features.wallet.ui.wallet import android.widget.Button import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager -import com.tangem.domain.common.CardDTO import com.tangem.domain.common.TapWorkarounds.derivationStyle import com.tangem.domain.common.TapWorkarounds.isTestCard import com.tangem.tap.common.analytics.Analytics import com.tangem.tap.common.analytics.events.MainScreen import com.tangem.tap.common.analytics.events.Portfolio -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.animateVisibility +import com.tangem.tap.common.extensions.formatAmountAsSpannedString +import com.tangem.tap.common.extensions.getQuantityString +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.tokens.CurrenciesRepository @@ -43,21 +46,11 @@ class MultiWalletView : WalletView() { lAddress.root.hide() rowButtons.hide() lSingleWalletBalance.root.hide() + lCardTotalBalance.root.show() rvMultiwallet.show() btnAddToken.show() - setupWalletCardNumber(binding) } - private fun setupWalletCardNumber(binding: FragmentWalletBinding) = with(binding) { - val card = store.state.globalState.scanResponse?.card - if (card?.backupStatus is CardDTO.BackupStatus.Active) { - val cardCount = (card.backupStatus as CardDTO.BackupStatus.Active).cardCount + 1 - tvTwinCardNumber.show() - tvTwinCardNumber.text = tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, cardCount) - } else { - tvTwinCardNumber.hide() - } - } override fun onViewCreated() { setupWalletsRecyclerView() @@ -79,6 +72,7 @@ class MultiWalletView : WalletView() { handleTotalBalance(binding, state.totalBalance, state.state) handleBackupWarning(binding, state.showBackupWarning) handleRescanWarning(binding, state.missingDerivations.isNotEmpty()) + setupWalletCardNumber(binding, state.walletCardsCount) walletsAdapter.submitList(state.walletsData) binding.pbLoadingUserTokens.show(state.loadingUserTokens) @@ -107,6 +101,16 @@ class MultiWalletView : WalletView() { handleErrorStates(state = state, binding = binding, fragment = fragment) } + private fun setupWalletCardNumber(binding: FragmentWalletBinding, walletCardsCount: Int?) = with(binding) { + if (walletCardsCount != null) { + tvTwinCardNumber.show() + tvTwinCardNumber.text = + tvTwinCardNumber.getQuantityString(R.plurals.card_label_card_count, walletCardsCount) + } else { + tvTwinCardNumber.hide() + } + } + private fun handleBackupWarning( binding: FragmentWalletBinding, showBackupWarning: Boolean, @@ -134,21 +138,20 @@ class MultiWalletView : WalletView() { totalBalance: TotalBalance?, progressState: ProgressState, ) = with(binding.lCardTotalBalance) { - root.isVisible = totalBalance != null - if (totalBalance != null) { - // Skip changes when on refreshing state - if (totalBalance.state == ProgressState.Refreshing || - progressState == ProgressState.Refreshing - ) return@with + if (totalBalance == null) { + veilBalance.animateVisibility(show = true) + root.isVisible = progressState == ProgressState.Loading + } else { + root.isVisible = true - if (totalBalance.state == ProgressState.Loading) { - veilBalance.veil() - } else { - veilBalance.unVeil() + // Skip changes when on refreshing state + if (totalBalance.state == ProgressState.Refreshing || progressState == ProgressState.Refreshing) { + return@with } - tvProcessing.animateVisibility( - show = totalBalance.state == ProgressState.Error, - ) + + veilBalance.animateVisibility(show = totalBalance.state == ProgressState.Loading) + tvBalance.animateVisibility(show = totalBalance.state != ProgressState.Loading) + tvProcessing.animateVisibility(show = totalBalance.state == ProgressState.Error) tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString( currencySymbol = totalBalance.fiatCurrency.symbol, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index c0265845aa..abca20330d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -33,6 +33,7 @@ class SingleWalletView : WalletView() { } private fun showSingleWalletView(binding: FragmentWalletBinding) = with(binding) { + tvTwinCardNumber.hide() rvMultiwallet.hide() btnAddToken.hide() rvPendingTransaction.hide() diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt index bac210c6d4..ae053d797b 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt @@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.model.TotalFiatBalance import com.tangem.tap.domain.model.UserWallet import com.tangem.tap.domain.model.WalletStoreModel import com.tangem.tap.domain.model.builders.UserWalletBuilder @@ -19,6 +20,7 @@ import com.tangem.tap.domain.scanCard.ScanCardProcessor import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.tap.totalFiatBalanceCalculator import com.tangem.tap.userWalletsListManager import com.tangem.tap.walletStoresManager @@ -26,6 +28,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber +import java.math.BigDecimal internal class WalletSelectorMiddleware { val middleware: Middleware = { _, appStateProvider -> @@ -127,7 +130,8 @@ internal class WalletSelectorMiddleware { store.dispatchOnMain(WalletSelectorAction.AddWallet.Success) if (isSavedWalletSelected) { - store.dispatchOnMain(NavigationAction.PopBackTo()) + updateAccessCodeRequestPolicy(selectedWallet) + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.onUserWalletSelected(selectedWallet) } } @@ -141,7 +145,8 @@ internal class WalletSelectorMiddleware { store.dispatchOnMain(WalletSelectorAction.HandleError(error)) } .doOnSuccess { selectedWallet -> - store.dispatchOnMain(NavigationAction.PopBackTo()) + updateAccessCodeRequestPolicy(selectedWallet) + store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet)) store.onUserWalletSelected(selectedWallet) } } @@ -163,7 +168,7 @@ internal class WalletSelectorMiddleware { scope.launch { userWalletsListManager.get(walletId = UserWalletId(walletId)) .map { it.copy(name = newName) } - .flatMap { userWalletsListManager.update(it) } + .flatMap { userWalletsListManager.save(it, canOverride = true) } .doOnFailure { error -> store.dispatchOnMain(WalletSelectorAction.HandleError(error)) } @@ -174,7 +179,6 @@ internal class WalletSelectorMiddleware { crossinline onCardScanned: suspend (ScanResponse) -> Unit, ) { ScanCardProcessor.scan( - useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes, onSuccess = { onCardScanned(it) }, @@ -208,11 +212,19 @@ internal class WalletSelectorMiddleware { val isSelectedWalletRemoved = prevSelectedWalletId != selectedWallet.walletId.stringValue if (isSelectedWalletRemoved) { + updateAccessCodeRequestPolicy(selectedWallet) store.onUserWalletSelected(selectedWallet) } } } + private fun updateAccessCodeRequestPolicy(userWallet: UserWallet) { + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && + userWallet.scanResponse.card.isAccessCodeSet, + ) + } + private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance( walletStores: List, ): UserWalletModel { @@ -221,7 +233,6 @@ internal class WalletSelectorMiddleware { is UserWalletModel.Type.MultiCurrency -> type.copy( tokensCount = walletStores.flatMap { it.walletsData }.size, ) - is UserWalletModel.Type.SingleCurrency -> type.copy( blockchainName = walletStores .firstOrNull() @@ -233,6 +244,7 @@ internal class WalletSelectorMiddleware { fiatBalance = totalFiatBalanceCalculator.calculate( prevAmount = fiatBalance.amount, walletStores = walletStores, + initial = TotalFiatBalance.Loaded(BigDecimal.ZERO), ), ) } diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt index 106ff7f37b..5e972b5dfc 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WallectSelectorScreenContent.kt @@ -1,11 +1,23 @@ package com.tangem.tap.features.walletSelector.ui.components -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState @@ -15,7 +27,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.SecondaryButtonIconRight +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.features.details.ui.cardsettings.resolveReference @@ -35,31 +51,16 @@ internal fun WalletSelectorScreenContent( onEditSelectedWalletClick: () -> Unit, onDeleteSelectedWalletsClick: () -> Unit, ) { - val editingWalletsSize by rememberUpdatedState(newValue = state.editingWalletsIds.size) - Column(modifier = modifier) { - Hand() - SpacerH12() - if (editingWalletsSize > 0) { - EditWalletsBar( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - editingWalletsSize = editingWalletsSize, - onClearSelectedClick = onClearSelectedClick, - onEditSelectedWalletClick = onEditSelectedWalletClick, - onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, - ) - } else { - Text( - modifier = Modifier.fillMaxWidth(), - text = stringResource(R.string.user_wallet_list_title), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - } - SpacerH12() + Header( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + editingWalletsIds = state.editingWalletsIds, + onClearSelectedClick = onClearSelectedClick, + onEditSelectedWalletClick = onEditSelectedWalletClick, + onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, + ) Column( modifier = Modifier .verticalScroll( @@ -76,28 +77,15 @@ internal fun WalletSelectorScreenContent( onWalletLongClick = onWalletLongClick, ) SpacerH24() - if (state.isLocked) { - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource( - id = R.string.user_wallet_list_unlock_all, - stringResource(id = R.string.common_biometrics), - ), - showProgress = state.showUnlockProgress, - onClick = onUnlockClick, - ) - SpacerH12() - } - SecondaryButtonIconRight( + Footer( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - text = stringResource(R.string.user_wallet_list_add_button), - showProgress = state.showAddCardProgress, - icon = painterResource(id = R.drawable.ic_tangem), - onClick = onAddCardClick, + isLocked = state.isLocked, + showUnlockProgress = state.showUnlockProgress, + showAddCardProgress = state.showAddCardProgress, + onUnlockClick = onUnlockClick, + onAddCardClick = onAddCardClick, ) SpacerH16() } @@ -105,62 +93,42 @@ internal fun WalletSelectorScreenContent( } @Composable -private fun EditWalletsBar( +private fun Header( modifier: Modifier = Modifier, - editingWalletsSize: Int, + editingWalletsIds: List, onClearSelectedClick: () -> Unit, onEditSelectedWalletClick: () -> Unit, onDeleteSelectedWalletsClick: () -> Unit, ) { - val showEditAction = remember(editingWalletsSize) { editingWalletsSize in 1 until 2 } - Row( - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8) - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - ) { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onClearSelectedClick, + val editingWalletsSize by rememberUpdatedState(newValue = editingWalletsIds.size) + val hasEditingWallets by remember { + derivedStateOf { editingWalletsSize > 0 } + } + + Column(modifier = modifier) { + Hand() + Box( + modifier = Modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size44), + contentAlignment = Alignment.Center, ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_close), - tint = TangemTheme.colors.icon.primary1, - contentDescription = "Unselect wallets", - ) - } - SpacerW16() - Text( - text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - SpacerWMax() - if (showEditAction) { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onEditSelectedWalletClick, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_pencil_outline_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = "Edit wallet name", + if (hasEditingWallets) { + EditWalletsBar( + modifier = Modifier.fillMaxWidth(), + editingWalletsSize = editingWalletsSize, + onClearSelectedClick = onClearSelectedClick, + onEditSelectedWalletClick = onEditSelectedWalletClick, + onDeleteSelectedWalletsClick = onDeleteSelectedWalletsClick, + ) + } else { + Text( + text = stringResource(R.string.user_wallet_list_title), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, ) } - SpacerW8() - } - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onDeleteSelectedWalletsClick, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_trash), - tint = TangemTheme.colors.icon.warning, - contentDescription = "Delete selected wallets", - ) } } } @@ -208,6 +176,101 @@ private fun WalletsList( } } +@Composable +private fun Footer( + modifier: Modifier = Modifier, + isLocked: Boolean, + showUnlockProgress: Boolean, + showAddCardProgress: Boolean, + onUnlockClick: () -> Unit, + onAddCardClick: () -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + if (isLocked) { + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource( + id = R.string.user_wallet_list_unlock_all, + stringResource(id = R.string.common_biometrics), + ), + showProgress = showUnlockProgress, + onClick = onUnlockClick, + ) + } + SecondaryButtonIconRight( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.user_wallet_list_add_button), + showProgress = showAddCardProgress, + icon = painterResource(id = R.drawable.ic_tangem), + onClick = onAddCardClick, + ) + } +} + +@Composable +private fun EditWalletsBar( + modifier: Modifier = Modifier, + editingWalletsSize: Int, + onClearSelectedClick: () -> Unit, + onEditSelectedWalletClick: () -> Unit, + onDeleteSelectedWalletsClick: () -> Unit, +) { + val showEditAction by remember(editingWalletsSize) { + derivedStateOf { editingWalletsSize in 1 until 2 } + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onClearSelectedClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_close), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Unselect wallets", + ) + } + Text( + text = stringResource(id = R.string.user_wallet_list_editing_count, editingWalletsSize), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SpacerWMax() + if (showEditAction) { + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onEditSelectedWalletClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_pencil_outline_24), + tint = TangemTheme.colors.icon.primary1, + contentDescription = "Change wallet name", + ) + } + } + IconButton( + modifier = Modifier.size(TangemTheme.dimens.size32), + onClick = onDeleteSelectedWalletsClick, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size24), + painter = painterResource(id = R.drawable.ic_trash), + tint = TangemTheme.colors.icon.warning, + contentDescription = "Delete selected wallets", + ) + } + } +} + // region Preview @Composable private fun WalletSelectorScreenContentSample( diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt index c95d59deec..ada0168d08 100644 --- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt +++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/WalletItem.kt @@ -7,8 +7,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.Icon import androidx.compose.material.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier @@ -85,14 +84,15 @@ private fun WalletCardImage( .height(TangemTheme.dimens.size32) .clip(TangemTheme.shapes.roundedCornersSmall2) - val checkedColorFilter by rememberUpdatedState( - newValue = if (isChecked) { + val tintColor = TangemTheme.colors.icon.accent.copy(alpha = .6f) + val checkedColorFilter = remember(isChecked) { + if (isChecked) { ColorFilter.tint( - color = TangemTheme.colors.icon.accent.copy(alpha = .6f), + color = tintColor, blendMode = BlendMode.SrcOver, ) - } else null, - ) + } else null + } SubcomposeAsyncImage( modifier = cardImageModifier, @@ -106,22 +106,15 @@ private fun WalletCardImage( contentDescription = null, ) - if (isSelected) { - Box( - modifier = Modifier - .align(Alignment.TopEnd) - .size(TangemTheme.dimens.size18) - .background( - color = Color.White, - shape = CircleShape, - ), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier.padding(all = (0.5).dp), - painter = painterResource(id = R.drawable.ic_check_circle_18), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, + when { + isChecked -> { + CheckedWalletMark( + modifier = Modifier.matchParentSize(), + ) + } + isSelected -> { + SelectedWalletBadge( + modifier = Modifier.align(Alignment.TopEnd), ) } } @@ -308,4 +301,42 @@ private fun CardImageShimmer( .background(TangemTheme.colors.button.secondary), ) } +} + +@Composable +private fun SelectedWalletBadge( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .size(TangemTheme.dimens.size18) + .background( + color = Color.White, + shape = CircleShape, + ), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.padding(all = (0.5).dp), + painter = painterResource(id = R.drawable.ic_check_circle_18), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } +} + +@Composable +private fun CheckedWalletMark( + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check_24), + tint = TangemTheme.colors.icon.primary2, + contentDescription = null, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt index f68e5122d7..681a3cea77 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeAction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.redux +import android.content.Intent import com.tangem.common.core.TangemError import org.rekotlin.Action @@ -14,5 +15,7 @@ internal sealed interface WelcomeAction : Action { data class Error(val error: TangemError) : WelcomeAction } + data class HandleDeepLink(val intent: Intent?) : WelcomeAction + object CloseError : WelcomeAction } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt index 73556a2d6f..88c0038a3c 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.welcome.redux +import android.content.Intent import com.tangem.common.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.domain.common.ScanResponse @@ -10,37 +11,47 @@ import com.tangem.tap.common.redux.navigation.AppScreen 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.intentHandler +import com.tangem.tap.preferencesStorage import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager import com.tangem.tap.userWalletsListManager import kotlinx.coroutines.launch import org.rekotlin.Middleware internal class WelcomeMiddleware { - val middleware: Middleware = { _, _ -> + val middleware: Middleware = { _, appStateProvider -> { next -> { action -> - if (action is WelcomeAction) { - handleAction(action) + val appState = appStateProvider() + if (action is WelcomeAction && appState != null) { + handleAction(action, appState.welcomeState) } next(action) } } } - private fun handleAction(action: WelcomeAction) { + private fun handleAction(action: WelcomeAction, state: WelcomeState) { when (action) { - is WelcomeAction.ProceedWithBiometrics -> proceedWithBiometry() - is WelcomeAction.ProceedWithCard -> proceedWithCard() - is WelcomeAction.ProceedWithBiometrics.Error -> Unit - is WelcomeAction.ProceedWithCard.Error -> Unit - is WelcomeAction.ProceedWithBiometrics.Success -> Unit - is WelcomeAction.ProceedWithCard.Success -> Unit - is WelcomeAction.CloseError -> Unit + is WelcomeAction.ProceedWithBiometrics -> { + proceedWithBiometry(state) + } + is WelcomeAction.ProceedWithCard -> { + proceedWithCard(state) + } + is WelcomeAction.ProceedWithBiometrics.Error, + is WelcomeAction.ProceedWithCard.Error, + is WelcomeAction.ProceedWithBiometrics.Success, + is WelcomeAction.ProceedWithCard.Success, + is WelcomeAction.CloseError, + is WelcomeAction.HandleDeepLink, + -> Unit } } - private fun proceedWithBiometry() { + private fun proceedWithBiometry(state: WelcomeState) { scope.launch { userWalletsListManager.unlockWithBiometry() .doOnFailure { error -> @@ -48,15 +59,22 @@ internal class WelcomeMiddleware { } .doOnSuccess { selectedUserWallet -> if (selectedUserWallet != null) { + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes && + selectedUserWallet.scanResponse.card.isAccessCodeSet, + ) + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchOnMain(WelcomeAction.ProceedWithBiometrics.Success) store.onUserWalletSelected(selectedUserWallet) + + handleDeepLinkIfNeeded(state.deepLinkIntent) } } } } - private fun proceedWithCard() = scope.launch { + private fun proceedWithCard(state: WelcomeState) = scope.launch { scanCardInternal { scanResponse -> val userWallet = UserWalletBuilder(scanResponse).build() @@ -65,13 +83,22 @@ internal class WelcomeMiddleware { store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(error)) } .doOnSuccess { + tangemSdkManager.setAccessCodeRequestPolicy( + useBiometricsForAccessCode = false, + ) store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success) store.onUserWalletSelected(userWallet) + + handleDeepLinkIfNeeded(state.deepLinkIntent) } } } + private fun handleDeepLinkIfNeeded(intent: Intent?) { + intentHandler.handleWalletConnectLink(intent) + } + private suspend inline fun scanCardInternal( crossinline onCardScanned: suspend (ScanResponse) -> Unit, ) { diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt index d5f3e0276b..42a6d4d0e1 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeReducer.kt @@ -12,6 +12,7 @@ internal object WelcomeReducer { private fun internalReduce(action: WelcomeAction, state: WelcomeState): WelcomeState { return when (action) { + is WelcomeAction.HandleDeepLink -> state.copy(deepLinkIntent = action.intent) is WelcomeAction.ProceedWithBiometrics -> state.copy(isUnlockWithBiometricsInProgress = true) is WelcomeAction.ProceedWithCard -> state.copy(isUnlockWithCardInProgress = true) is WelcomeAction.ProceedWithBiometrics.Error -> state.copy( diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt index a4fce23f93..f40046600d 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeState.kt @@ -1,10 +1,12 @@ package com.tangem.tap.features.welcome.redux +import android.content.Intent import com.tangem.common.core.TangemError import org.rekotlin.StateType data class WelcomeState( val isUnlockWithBiometricsInProgress: Boolean = false, val isUnlockWithCardInProgress: Boolean = false, + val deepLinkIntent: Intent? = null, val error: TangemError? = null, ) : StateType \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt index 8ccaffcc44..9ff1ad727f 100644 --- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeFragment.kt @@ -1,12 +1,19 @@ package com.tangem.tap.features.welcome.ui +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHostState -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.colorResource @@ -39,6 +46,10 @@ internal class WelcomeFragment : ComposeFragment() { setSystemBarsColor(color = backgroundColor) } + BackHandler { + requireActivity().finish() + } + Box( modifier = modifier .systemBarsPadding(), 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 35cc636808..9dd2b9746f 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 @@ -6,7 +6,7 @@ import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager 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 5e52716d98..645d5182ed 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 @@ -6,7 +6,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest import com.tangem.domain.common.extensions.withIOContext -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.features.wallet.models.Currency diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt index f4612643d3..d9cc63b72a 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt @@ -3,7 +3,7 @@ package com.tangem.tap.network.payid import com.squareup.moshi.JsonClass import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.createRetrofitInstance import retrofit2.Retrofit class PayIdService { diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt index f5e7ed1f96..05d16f8724 100644 --- a/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdVerifyService.kt @@ -2,7 +2,7 @@ package com.tangem.tap.network.payid import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.createRetrofitInstance /** [REDACTED_AUTHOR] diff --git a/app/src/main/java/com/tangem/tap/persistence/FiatCurrenciesPrefStorage.kt b/app/src/main/java/com/tangem/tap/persistence/FiatCurrenciesPrefStorage.kt index 50a8859516..59c500f9a7 100644 --- a/app/src/main/java/com/tangem/tap/persistence/FiatCurrenciesPrefStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/FiatCurrenciesPrefStorage.kt @@ -3,7 +3,7 @@ package com.tangem.tap.persistence import android.content.SharedPreferences import androidx.core.content.edit import com.tangem.common.json.MoshiJsonConverter -import com.tangem.network.api.tangemTech.CurrenciesResponse +import com.tangem.datasource.api.tangemTech.CurrenciesResponse import com.tangem.tap.common.entities.FiatCurrency /** diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index 413f292589..eaf21b8d83 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -4,7 +4,7 @@ import android.app.Application import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit -import com.tangem.network.common.MoshiConverter +import com.tangem.datasource.api.common.MoshiConverter import java.util.* class PreferencesStorage(applicationContext: Application) { @@ -33,7 +33,7 @@ class PreferencesStorage(applicationContext: Application) { get() = preferences.getLong(CHAT_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L } set(value) = preferences.edit { putLong(CHAT_FIRST_LAUNCH_KEY, value ?: 0) } - var shouldShowSaveWallet: Boolean + var shouldShowSaveUserWalletScreen: Boolean get() = preferences.getBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, true) set(value) = preferences.edit { putBoolean(SAVE_WALLET_DIALOG_SHOWN_KEY, value) diff --git a/app/src/main/res/layout/layout_card_total_balance.xml b/app/src/main/res/layout/layout_card_total_balance.xml index c8dc2dd7bd..3f47e6b612 100644 --- a/app/src/main/res/layout/layout_card_total_balance.xml +++ b/app/src/main/res/layout/layout_card_total_balance.xml @@ -27,20 +27,13 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintTop_toBottomOf="@id/tv_title"> - + + + + private val headers: Map, ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { @@ -21,10 +21,10 @@ open class AddHeaderInterceptor( } } -class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor(mapOf( - "Cache-Control" to "max-age=$maxAgeSeconds", -)) +class CacheControlHttpInterceptor(maxAgeSeconds: Int) : AddHeaderInterceptor( + mapOf("Cache-Control" to "max-age=$maxAgeSeconds"), +) -class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor(mapOf( - "card_public_key" to cardPublicKeyHex, -)) \ No newline at end of file +class CardPublicKeyHttpInterceptor(cardPublicKeyHex: String) : AddHeaderInterceptor( + mapOf("card_public_key" to cardPublicKeyHex), +) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt similarity index 79% rename from network/src/main/java/com/tangem/network/common/MoshiConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt index 47b5cc4b64..054206db9d 100644 --- a/network/src/main/java/com/tangem/network/common/MoshiConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/MoshiConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.network.common +package com.tangem.datasource.api.common import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi @@ -9,8 +9,10 @@ import retrofit2.converter.moshi.MoshiConverterFactory /** [REDACTED_AUTHOR] */ +//todo needs to be refactored object MoshiConverter { + //todo refactor: provide via DI var INSTANCE = MoshiJsonConverter() private set @@ -23,7 +25,9 @@ object MoshiConverter { fun createFactory(moshi: Moshi = INSTANCE.moshi): Converter.Factory = MoshiConverterFactory.create(moshi) + //todo provide via DI using quealifiers fun defaultMoshi(): Moshi = INSTANCE.moshi + //todo provide via DI using quealifiers fun sdkMoshi(): Moshi = MoshiJsonConverter.INSTANCE.moshi } \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/Retrofit.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt similarity index 96% rename from network/src/main/java/com/tangem/network/common/Retrofit.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt index 70bb657bce..01e19dfada 100644 --- a/network/src/main/java/com/tangem/network/common/Retrofit.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/Retrofit.kt @@ -1,4 +1,4 @@ -package com.tangem.network.common +package com.tangem.datasource.api.common import okhttp3.Interceptor import okhttp3.OkHttpClient diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt new file mode 100644 index 0000000000..774d001e04 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt @@ -0,0 +1,200 @@ +package com.tangem.datasource.api.oneinch + +import com.tangem.datasource.api.oneinch.models.AllowanceResponse +import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse +import com.tangem.datasource.api.oneinch.models.ApproveSpenderResponse +import com.tangem.datasource.api.oneinch.models.ProtocolsResponse +import com.tangem.datasource.api.oneinch.models.QuoteResponse +import com.tangem.datasource.api.oneinch.models.StatusResponse +import com.tangem.datasource.api.oneinch.models.SwapResponse +import com.tangem.datasource.api.oneinch.models.TokensResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface OneInchApi { + + /** + * Healthcheck return 200 if service is available + * + * @return [StatusResponse] + */ + @GET("healthcheck") + suspend fun healthcheck(): StatusResponse + + //region Approve + /** + * Address of the 1inch router that must be trusted to spend funds for the exchange + * + * @return [ApproveSpenderResponse] + */ + @GET("approve/spender") + suspend fun approveSpender(): ApproveSpenderResponse + + /** + * Generate data for calling the contract in order to allow the 1inch router to spend funds + * + * @param tokenAddress Token address you want to exchange + * @param amount The number of tokens that the 1inch router is allowed to spend. + * If not specified, it will be allowed to spend an infinite amount of tokens. + * + * @return [ApproveCalldataResponse] Transaction body to allow the exchange with the 1inch router + */ + @GET("approve/transaction") + suspend fun approveTransaction( + @Query("tokenAddress") tokenAddress: String, + @Query("amount") amount: String? = null, + ): ApproveCalldataResponse + + /** + * Get the number of tokens that the 1inch router is allowed to spend + * + * @param tokenAddress Token address you want to exchange + * @param walletAddress Wallet address for which you want to check + * + * @return [AllowanceResponse] + */ + @GET("approve/allowance") + suspend fun approveAllowance( + @Query("tokenAddress") tokenAddress: String, + @Query("walletAddress") walletAddress: String, + ): AllowanceResponse + //endregion Approve + + //region Info + /** + * List of tokens that are available for swap in the 1inch Aggregation protocol + * + * @return [TokensResponse] + */ + @GET("tokens") + suspend fun tokensAvailable(): TokensResponse + + /** + * List of liquidity sources that are available for routing in the 1inch Aggregation protocol + * + * @return + */ + @GET("liquidity-sources") + suspend fun liquiditySources(): ProtocolsResponse + //endregion Info + + //region Swap + /** + * Find the best quote to exchange via 1inch router + * + * @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE + * @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302 + * @param amount amount of a token to sell, set in minimal divisible units e.g.: + * 1.00 DAI set as 1000000000000000000 + * 51.03 USDC set as 51030000 + * + * @param protocols default: all + * @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress, + * the rest will be used as input for a swap + * Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap! + * + * @param gasLimit maximum amount of gas for a swap; + * @param connectorTokens token-connectors can be specified via this parameter. + * The more is set — the longer route estimation will take. + * If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap! + * + * @param complexityLevel maximum number of token-connectors to be used in a transaction. + * The more is used — the longer route estimation will take + * min: 0; max: 3; default: 2; !should be the same for quote and swap! + * + * @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap! + * @param parts limit maximum number of parts each main route parts can be split into; + * should be the same for a quote and swap + * default: 20; max: 100 + * + * @param gasPrice 1inch takes in account gas expenses to determine exchange route. + * It is important to use the same gas price on the quote and swap methods. + * Gas price set in wei: 12.5 GWEI set as 12500000000 + * default: fast from network + * + * @return [QuoteResponse] + */ + suspend fun quote( + @Query("fromTokenAddress") fromTokenAddress: String, + @Query("toTokenAddress") toTokenAddress: String, + @Query("amount") amount: String, + @Query("protocols") protocols: String? = null, + @Query("fee") fee: String? = null, + @Query("gasLimit") gasLimit: String? = null, + @Query("connectorTokens") connectorTokens: String? = null, + @Query("complexityLevel") complexityLevel: String? = null, + @Query("mainRouteParts") mainRouteParts: String? = null, + @Query("parts") parts: String? = null, + @Query("gasPrice") gasPrice: String? = null, + ): QuoteResponse + + /** + * Generate data for calling the 1inch router for exchange + * + * @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE + * @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302 + * @param amount amount of a token to sell, set in minimal divisible units e.g.: + * 1.00 DAI set as 1000000000000000000 + * 51.03 USDC set as 51030000 + * + * @param fromAddress The address that calls the 1inch contract + * @param slippage limit of price slippage you are willing to accept in percentage, may be set with decimals. + * &slippage=0.5 means 0.5% slippage is acceptable. Low values increase chances that transaction will fail, + * high values increase chances of front running. min: 0; max: 50; + * + * @param protocols default: all + * @param destinationAddress Receiver of destination currency. default: fromAddress + * @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress, + * the rest will be used as input for a swap + * Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap! + * + * @param permit https://eips.ethereum.org/EIPS/eip-2612 + * @param compatibilityMode Allows to build calldata without optimized routers + * @param burnChi If true, CHI will be burned from fromAddress to compensate gas. + * Check CHI balance and allowance before turning that on. CHI should be approved for the spender address + * + * @param connectorTokens token-connectors can be specified via this parameter. + * The more is set — the longer route estimation will take. + * If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap! + * + * @param complexityLevel maximum number of token-connectors to be used in a transaction. + * The more is used — the longer route estimation will take + * min: 0; max: 3; default: 2; !should be the same for quote and swap! + * + * @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap! + * @param parts limit maximum number of parts each main route parts can be split into; + * should be the same for a quote and swap + * default: 20; max: 100 + * + * @param gasLimit maximum amount of gas for a swap; + * @param gasPrice 1inch takes in account gas expenses to determine exchange route. + * It is important to use the same gas price on the quote and swap methods. + * Gas price set in wei: 12.5 GWEI set as 12500000000 + * default: fast from network + * + * @return [SwapResponse] + */ + suspend fun swap( + @Query("fromTokenAddress") fromTokenAddress: String, + @Query("toTokenAddress") toTokenAddress: String, + @Query("amount") amount: String, + @Query("fromAddress") fromAddress: String, + @Query("slippage") slippage: Int, + @Query("protocols") protocols: String? = null, + @Query("destReceiver") destinationAddress: String? = null, + @Query("referrerAddress") referrerAddress: String? = null, + @Query("fee") fee: String? = null, + @Query("disableEstimate") disableEstimate: Boolean? = null, + @Query("permit") permit: String? = null, + @Query("compatibilityMode") compatibilityMode: Boolean? = null, + @Query("burnChi") burnChi: Boolean? = null, + @Query("allowPartialFill") allowPartialFill: Boolean? = null, + @Query("parts") parts: String? = null, + @Query("mainRouteParts") mainRouteParts: String? = null, + @Query("connectorTokens") connectorTokens: String? = null, + @Query("complexityLevel") complexityLevel: String? = null, + @Query("gasLimit") gasLimit: String? = null, + @Query("gasPrice") gasPrice: String? = null, + ): SwapResponse + //endregion Swap +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt new file mode 100644 index 0000000000..16956f907e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt @@ -0,0 +1,7 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +data class AllowanceResponse( + @Json(name = "allowance") val allowance: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt new file mode 100644 index 0000000000..f57fab94fd --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt @@ -0,0 +1,18 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Approve calldata response + * + * @property data The encoded data to call the approve method on the swapped token contract + * @property gasPrice Gas price for fast transaction processing + * @property toAddress Token address that will be allowed to exchange through 1inch router + * @property value Native token value in WEI (for approve is always 0) + */ +data class ApproveCalldataResponse( + @Json(name = "data") val data: String, + @Json(name = "gasPrice") val gasPrice: String, + @Json(name = "to") val toAddress: String, + @Json(name = "value") val value: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt new file mode 100644 index 0000000000..0ee9d5b2ac --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Approve spender response + * + * @property address Address of the 1inch router that must be trusted to spend funds for the exchange + */ +data class ApproveSpenderResponse( + @Json(name = "address") val address: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt new file mode 100644 index 0000000000..03c23155ff --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Path view dto + */ +data class PathViewDto( + @Json(name = "name") val name: String, + @Json(name = "part") val part: Int, + @Json(name = "fromTokenAddress") val fromTokenAddress: String, + @Json(name = "toTokenAddress") val toTokenAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt new file mode 100644 index 0000000000..b083cd1ce8 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt @@ -0,0 +1,27 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Protocols response + * + * @property protocols List of protocols that are available for routing in the 1inch Aggregation protocol + */ +data class ProtocolsResponse( + @Json(name = "protocols") val protocols: List, +) + +/** + * Protocol image + * + * @property id Protocol id + * @property title Protocol title + * @property image Protocol logo image + * @property imageColor Protocol logo image in color + */ +data class ProtocolImageDto( + @Json(name = "id") val id: String, + @Json(name = "title") val title: String, + @Json(name = "img") val image: String, + @Json(name = "img_color") val imageColor: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt new file mode 100644 index 0000000000..dca5642f12 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt @@ -0,0 +1,22 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Quote response + * + * @property fromToken Source token info + * @property toToken Destination token info + * @property toTokenAmount Expected amount of destination token + * @property fromTokenAmount Amount of source token + * @property protocols Selected protocols in a path + * @property estimatedGas gas fee + */ +data class QuoteResponse( + @Json(name = "fromToken") val fromToken: TokenOneInchDto, + @Json(name = "toToken") val toToken: TokenOneInchDto, + @Json(name = "toTokenAmount") val toTokenAmount: String, + @Json(name = "fromTokenAmount") val fromTokenAmount: String, + @Json(name = "protocols") val protocols: List, + @Json(name = "estimatedGas") val estimatedGas: Int, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt new file mode 100644 index 0000000000..576851a3b5 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt @@ -0,0 +1,8 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +data class StatusResponse( + @Json(name = "status") val status: String, + @Json(name = "provider") val provider: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt new file mode 100644 index 0000000000..fe342de231 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt @@ -0,0 +1,42 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Swap error dto + * + * One of the following errors: + * + * -Insufficient liquidity + * -Cannot estimate + * -You may not have enough ETH balance for gas fee + * -FromTokenAddress cannot be equals to toTokenAddress + * -Cannot estimate. Don't forget about miner fee. Try to leave the buffer of ETH for gas + * -Not enough balance + * -Not enough allowance + * + * @property statusCode HTTP code + * @property error Error code description + * @property description Error description (one of the following) + * @property requestId Request id + * @property meta Meta information + * @constructor Create empty Swap error dto + */ +data class SwapErrorDto( + @Json(name = "statusCode") val statusCode: Int, + @Json(name = "error") val error: String, + @Json(name = "description") val description: String, + @Json(name = "requestId") val requestId: String, + @Json(name = "meta") val meta: List, +) + +/** + * Nest error meta + * + * @property type Type of field + * @property value Value of field + */ +data class NestErrorMeta( + @Json(name = "type") val type: String, + @Json(name = "value") val value: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt new file mode 100644 index 0000000000..1f25a391e0 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +data class SwapResponse( + @Json(name = "fromToken") val fromToken: TokenOneInchDto, + @Json(name = "toToken") val toToken: TokenOneInchDto, + @Json(name = "toTokenAmount") val toTokenAmount: String, + @Json(name = "fromTokenAmount") val fromTokenAmount: String, + @Json(name = "protocols") val protocols: List, + @Json(name = "transaction") val transaction: TransactionDto, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt new file mode 100644 index 0000000000..0c06f15d84 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt @@ -0,0 +1,20 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Token one inch + * + * @property symbol token symbol + * @property name token name + * @property address token address + * @property decimals token decimals + * @property logoURI token logo image url + */ +data class TokenOneInchDto( + @Json(name = "symbol") val symbol: String, + @Json(name = "name") val name: String, + @Json(name = "address") val address: String, + @Json(name = "decimals") val decimals: Int, + @Json(name = "logoURI") val logoURI: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt new file mode 100644 index 0000000000..ca1b369949 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Tokens response + * + * @property tokens List of supported tokens + */ +data class TokensResponse( + @Json(name = "tokens") val tokens: Map, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt new file mode 100644 index 0000000000..a32e827601 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt @@ -0,0 +1,23 @@ +package com.tangem.datasource.api.oneinch.models + +import com.squareup.moshi.Json + +/** + * Transaction dto + * + * @property fromAddress transactions will be sent from this address + * @property toAddress transactions will be sent to our(1inch) contract address + * @property data The encoded data to call the approve method on the swapped token contract + * @property value Native token value in WEI (for approve is always 0) + * @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000 + * @property gas estimated amount of the gas limit, increase this value by 25% + * @constructor Create empty Transaction dto + */ +data class TransactionDto( + @Json(name = "from") val fromAddress: String, + @Json(name = "to") val toAddress: String, + @Json(name = "data") val data: String, + @Json(name = "value") val value: String, + @Json(name = "gasPrice") val gasPrice: String, + @Json(name = "gas") val gas: String, +) \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt similarity index 95% rename from network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt index 058cd368dd..de7c678d3b 100644 --- a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApi.kt @@ -1,4 +1,4 @@ -package com.tangem.network.api.paymentology +package com.tangem.datasource.api.paymentology import retrofit2.http.Body import retrofit2.http.Headers diff --git a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt similarity index 91% rename from network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt index 8cd70891c3..f46a17b36d 100644 --- a/network/src/main/java/com/tangem/network/api/paymentology/PaymentologyApiService.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/PaymentologyApiService.kt @@ -1,10 +1,10 @@ -package com.tangem.network.api.paymentology +package com.tangem.datasource.api.paymentology import com.tangem.common.extensions.toHexString import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.network.common.MoshiConverter -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.MoshiConverter +import com.tangem.datasource.api.common.createRetrofitInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/network/src/main/java/com/tangem/network/api/paymentology/Requests.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Requests.kt similarity index 98% rename from network/src/main/java/com/tangem/network/api/paymentology/Requests.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Requests.kt index 580e60bfa5..cb0206934e 100644 --- a/network/src/main/java/com/tangem/network/api/paymentology/Requests.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Requests.kt @@ -1,4 +1,4 @@ -package com.tangem.network.api.paymentology +package com.tangem.datasource.api.paymentology import com.squareup.moshi.Json import com.squareup.moshi.JsonClass diff --git a/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Responses.kt similarity index 98% rename from network/src/main/java/com/tangem/network/api/paymentology/Responses.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Responses.kt index b60324b42d..d62630b73e 100644 --- a/network/src/main/java/com/tangem/network/api/paymentology/Responses.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/paymentology/Responses.kt @@ -1,4 +1,4 @@ -package com.tangem.network.api.paymentology +package com.tangem.datasource.api.paymentology import com.squareup.moshi.Json import com.tangem.common.extensions.calculateHashCode diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/Responses.kt similarity index 90% rename from network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/Responses.kt index 4f88fca3b7..532a5debfb 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/Responses.kt @@ -1,4 +1,4 @@ -package com.tangem.network.api.tangemTech +package com.tangem.datasource.api.tangemTech import java.math.BigDecimal @@ -11,7 +11,7 @@ sealed interface TangemTechResponse : HttpResponse data class CoinsResponse( val imageHost: String?, val coins: List, - val total: Int + val total: Int, ) : TangemTechResponse { data class Coin( @@ -19,13 +19,14 @@ data class CoinsResponse( val name: String, val symbol: String, val active: Boolean, - val networks: List = listOf() + val networks: List = listOf(), ) : TangemTechResponse { data class Network( val networkId: String, val contractAddress: String? = null, val decimalCount: BigDecimal? = null, + val exchangeable: Boolean? = false, ) : TangemTechResponse } } diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt similarity index 87% rename from network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 518edd9fec..e518041674 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -1,4 +1,4 @@ -package com.tangem.network.api.tangemTech +package com.tangem.datasource.api.tangemTech import retrofit2.http.Body import retrofit2.http.GET @@ -14,11 +14,12 @@ interface TangemTechApi { @GET("coins") suspend fun coins( @Query("contractAddress") contractAddress: String? = null, + @Query("exchangeable") exchangeable: Boolean? = false, @Query("networkIds") networkIds: String? = null, @Query("active") active: Boolean? = null, @Query("searchText") searchText: String? = null, @Query("offset") offset: Int? = null, - @Query("limit") limit: Int? = null + @Query("limit") limit: Int? = null, ): CoinsResponse @GET("rates") diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt similarity index 91% rename from network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt index 6137e8dfa0..50540a226e 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechService.kt @@ -1,10 +1,10 @@ -package com.tangem.network.api.tangemTech +package com.tangem.datasource.api.tangemTech import com.tangem.common.services.Result import com.tangem.common.services.performRequest -import com.tangem.network.common.AddHeaderInterceptor -import com.tangem.network.common.CacheControlHttpInterceptor -import com.tangem.network.common.createRetrofitInstance +import com.tangem.datasource.api.common.AddHeaderInterceptor +import com.tangem.datasource.api.common.CacheControlHttpInterceptor +import com.tangem.datasource.api.common.createRetrofitInstance import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt index 6fed866ecc..d662a98721 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworkModule.kt @@ -3,7 +3,10 @@ package com.tangem.datasource.di import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.datasource.api.AuthHeaderInterceptor +import com.tangem.datasource.api.oneinch.OneInchApi import com.tangem.datasource.api.referral.ReferralApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.di.qualifiers.OneInchEthereum import com.tangem.lib.auth.AuthProvider import dagger.Module import dagger.Provides @@ -19,6 +22,33 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class NetworkModule { + @Provides + @Singleton + fun provideTangemTechApi(okHttpClient: OkHttpClient): TangemTechApi { + return Retrofit.Builder() + .addConverterFactory( + MoshiConverterFactory.create(), + ) + .baseUrl(DEV_TANGEM_TECH_BASE_URL) + .client(okHttpClient) + .build() + .create(TangemTechApi::class.java) + } + + @Provides + @Singleton + @OneInchEthereum + fun provideOneInchEthereumApi(): OneInchApi { + return Retrofit.Builder() + .addConverterFactory( + MoshiConverterFactory.create(), + ) + .baseUrl(ONE_INCH_ETHER_BASE_URL) + .client(OkHttpClient()) + .build() + .create(OneInchApi::class.java) + } + @Provides @Singleton fun provideReferralApi(okHttpClient: OkHttpClient): ReferralApi { @@ -30,7 +60,7 @@ class NetworkModule { .build(), ), ) - .baseUrl(PROD_REFERRAL_BASE_URL) + .baseUrl(DEV_TANGEM_TECH_BASE_URL) .client(okHttpClient) .build() .create(ReferralApi::class.java) @@ -48,6 +78,8 @@ class NetworkModule { } private companion object { - const val PROD_REFERRAL_BASE_URL = "https://devapi.tangem-tech.com/v1/"//"https://api.tangem-tech.com/v1/" + const val PROD_TANGEM_TECH_BASE_URL = "https://api.tangem-tech.com/v1/" + const val DEV_TANGEM_TECH_BASE_URL = "https://devapi.tangem-tech.com/v1/" + const val ONE_INCH_ETHER_BASE_URL = "" } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/qualifiers/OneInchApiQualifiers.kt b/core/datasource/src/main/java/com/tangem/datasource/di/qualifiers/OneInchApiQualifiers.kt new file mode 100644 index 0000000000..f312541fde --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/qualifiers/OneInchApiQualifiers.kt @@ -0,0 +1,15 @@ +package com.tangem.datasource.di.qualifiers + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OneInchEthereum + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OneInchBinance + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class OneInchOptimism \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings_final.xml b/core/res/src/main/res/values-de/strings_final.xml index 5e5e0bc90b..bb7027a32f 100644 --- a/core/res/src/main/res/values-de/strings_final.xml +++ b/core/res/src/main/res/values-de/strings_final.xml @@ -114,12 +114,15 @@ Karte scannen Save your Wallet Would you like to use %s? + Would you like to use biometrics? Access the app Log into the app and check your balance without scanning the card Access code %s will be requested instead of the access code for interactions with your wallet + Biometrics will be requested instead of the access code for interactions with your wallet Note that making a transaction with your funds will still require your card Allow to use %s + Allow to use biometrics New feature My Wallets Multi-currency @@ -218,10 +221,12 @@ Entfernen Privacy policy Enable biometric authorization - It looks like you have biometric authorization disabled, it is necessary to save wallets + It looks like you have biometric authentication disabled, it is necessary to save wallets Enable Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access. %d selected biometric authentication biometrics + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem App diff --git a/core/res/src/main/res/values-fr/strings_final.xml b/core/res/src/main/res/values-fr/strings_final.xml index fdba526339..bacd616a66 100644 --- a/core/res/src/main/res/values-fr/strings_final.xml +++ b/core/res/src/main/res/values-fr/strings_final.xml @@ -114,12 +114,15 @@ Scannez la carte Save your Wallet Would you like to use %s? + Would you like to use biometrics? Access the app Log into the app and check your balance without scanning the card Access code %s will be requested instead of the access code for interactions with your wallet + Biometrics will be requested instead of the access code for interactions with your wallet Note that making a transaction with your funds will still require your card Allow to use %s + Allow to use biometrics New feature My Wallets Multi-currency @@ -218,10 +221,12 @@ Supprimer Privacy policy Enable biometric authorization - It looks like you have biometric authorization disabled, it is necessary to save wallets + It looks like you have biometric authentication disabled, it is necessary to save wallets Enable Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access. %d selected biometric authentication biometrics + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem App diff --git a/core/res/src/main/res/values-it/strings_final.xml b/core/res/src/main/res/values-it/strings_final.xml index b812e23de8..34a7c383db 100644 --- a/core/res/src/main/res/values-it/strings_final.xml +++ b/core/res/src/main/res/values-it/strings_final.xml @@ -114,12 +114,15 @@ Scansiona carta Save your Wallet Would you like to use %s? + Would you like to use biometrics? Access the app Log into the app and check your balance without scanning the card Access code %s will be requested instead of the access code for interactions with your wallet + Biometrics will be requested instead of the access code for interactions with your wallet Note that making a transaction with your funds will still require your card Allow to use %s + Allow to use biometrics New feature My Wallets Multi-currency @@ -218,10 +221,12 @@ Rimuovere Privacy policy Enable biometric authorization - It looks like you have biometric authorization disabled, it is necessary to save wallets + It looks like you have biometric authentication disabled, it is necessary to save wallets Enable Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access. %d selected biometric authentication biometrics + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem App diff --git a/core/res/src/main/res/values-ru/strings_final.xml b/core/res/src/main/res/values-ru/strings_final.xml index d8a604f287..14e74e4d88 100644 --- a/core/res/src/main/res/values-ru/strings_final.xml +++ b/core/res/src/main/res/values-ru/strings_final.xml @@ -114,12 +114,15 @@ Сканировать карту Сохраните ваш кошелек Вы хотите использовать %s? + Вы хотите использовать биометрию? Доступ в приложение Войдите в приложение и следите за своим балансом без сканирования карты Код доступа Для операций с вашим кошельком будет запрашиваться %s вместо кода доступа карты + Для операций с вашим кошельком будет запрашиваться биометрия вместо кода доступа карты Обратите внимание, что для совершения транзакции с вашими средствами по-прежнему потребуется ваша карта Использовать %s + Использовать биометрию Новый функционал Мои кошельки Мультивалютные @@ -224,4 +227,6 @@ %d выбрано биометрическую аутентификацию биометрией + Включите биометрическую аутентификацию + Перейдите в настройки, чтобы включить биометрическую аутентификацию в приложении Tangem App diff --git a/core/res/src/main/res/values/strings_final.xml b/core/res/src/main/res/values/strings_final.xml index c7b9866169..d41e3ca81f 100644 --- a/core/res/src/main/res/values/strings_final.xml +++ b/core/res/src/main/res/values/strings_final.xml @@ -114,12 +114,15 @@ Scan card Save your wallet Would you like to use %s? + Would you like to use biometrics? Access the app Log into the app and check your balance without scanning the card Access code %s will be requested instead of the access code for interactions with your wallet + Biometrics will be requested instead of the access code for interactions with your wallet Note that making a transaction with your funds will still require your card Allow to use %s + Allow to use biometrics New feature My Wallets Multi-currency @@ -218,10 +221,12 @@ Delete Privacy policy Enable biometric authorization - It looks like you have biometric authorization disabled, it is necessary to save wallets + It looks like you have biometric authentication disabled, it is necessary to save wallets Enable Save your Wallet feature allows you to use your wallet with biometric auth without tapping your card to the phone to gain access. %d selected biometric authentication biometrics + Enable biometric authentication + Go to settings to enable biometric authentication in the Tangem App diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt index 11dae7b514..80ca296491 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Buttons.kt @@ -54,7 +54,7 @@ fun TextButton( onClick = onClick, enabled = enabled, showProgress = false, - colors = TangemButtonsDefaults.textButtonColors, + colors = TangemButtonsDefaults.defaultTextButtonColors, size = TangemButtonSize.Text, ) } @@ -77,7 +77,26 @@ fun TextButtonIconLeft( onClick = onClick, enabled = enabled, showProgress = false, - colors = TangemButtonsDefaults.textButtonColors, + colors = TangemButtonsDefaults.defaultTextButtonColors, + size = TangemButtonSize.Text, + ) +} + +@Composable +fun WarningTextButton( + modifier: Modifier = Modifier, + text: String, + enabled: Boolean = true, + onClick: () -> Unit, +) { + TangemButton( + modifier = modifier, + text = text, + icon = TangemButtonIcon.None, + onClick = onClick, + enabled = enabled, + showProgress = false, + colors = TangemButtonsDefaults.warningTextButtonColors, size = TangemButtonSize.Text, ) } @@ -325,7 +344,7 @@ private fun TangemButtonSize.toShape(): Shape = when (this) { TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall } -private object TangemButtonsDefaults { +object TangemButtonsDefaults { val elevation: ButtonElevation @Composable get() = ButtonDefaults .elevation( @@ -349,17 +368,25 @@ private object TangemButtonsDefaults { disabledContentColor = TangemTheme.colors.text.disabled, ) - val textButtonColors: ButtonColors + val defaultTextButtonColors: ButtonColors @Composable get() = TangemButtonColors( backgroundColor = Color.Transparent, contentColor = TangemTheme.colors.text.secondary, disabledBackgroundColor = Color.Transparent, disabledContentColor = TangemTheme.colors.text.disabled, ) + + val warningTextButtonColors: ButtonColors + @Composable get() = TangemButtonColors( + backgroundColor = Color.Transparent, + contentColor = TangemTheme.colors.text.warning, + disabledBackgroundColor = Color.Transparent, + disabledContentColor = TangemTheme.colors.text.disabled, + ) } @Immutable -private class TangemButtonColors( +private open class TangemButtonColors( private val backgroundColor: Color, private val contentColor: Color, private val disabledBackgroundColor: Color, @@ -561,6 +588,11 @@ private fun TextButtonSample( enabled = false, onClick = { /* no-op */ }, ) + Divider(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8)) + WarningTextButton( + text = "Delete", + onClick = { /* no-op */ }, + ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt index 7b99b3f27e..fcc5dc2d24 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Spacers.kt @@ -61,6 +61,11 @@ fun SpacerH24(modifier: Modifier = Modifier) { SpacerH(24.dp, modifier) } +@Composable +fun SpacerH28(modifier: Modifier = Modifier) { + SpacerH(28.dp, modifier) +} + @Composable fun SpacerH32(modifier: Modifier = Modifier) { SpacerH(32.dp, modifier) @@ -71,6 +76,12 @@ fun SpacerH32(modifier: Modifier = Modifier) { fun ColumnScope.SpacerHMax(modifier: Modifier = Modifier) { Spacer(modifier = modifier.weight(1f)) } + +// TODO: Refactor with context receivers +@Composable +fun ColumnScope.SpacerHHalf(modifier: Modifier = Modifier) { + Spacer(modifier = modifier.weight(.5f)) +} // endregion Horizontal // region Vertical diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index 5b111b5e96..38321bcba1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -40,6 +40,7 @@ data class TangemDimens internal constructor( val size32: Dp = 32.dp, val size40: Dp = 40.dp, val size42: Dp = 42.dp, + val size44: Dp = 44.dp, val size48: Dp = 48.dp, val size56: Dp = 56.dp, val size62: Dp = 62.dp, @@ -58,8 +59,10 @@ data class TangemDimens internal constructor( val spacing12: Dp = 12.dp, val spacing14: Dp = 14.dp, val spacing16: Dp = 16.dp, + val spacing20: Dp = 20.dp, val spacing24: Dp = 24.dp, val spacing32: Dp = 32.dp, + val spacing34: Dp = 34.dp, val spacing44: Dp = 44.dp, val spacing50: Dp = 50.dp, val spacing54: Dp = 54.dp, diff --git a/core/ui/src/main/res/drawable/ic_alert_circle_24.xml b/core/ui/src/main/res/drawable/ic_alert_circle_24.xml new file mode 100644 index 0000000000..278bfde255 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert_circle_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_face_recognition_24.xml b/core/ui/src/main/res/drawable/ic_face_recognition_24.xml new file mode 100644 index 0000000000..a0b2816482 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_face_recognition_24.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_lock.xml b/core/ui/src/main/res/drawable/ic_lock_24.xml similarity index 100% rename from app/src/main/res/drawable/ic_lock.xml rename to core/ui/src/main/res/drawable/ic_lock_24.xml diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts index 6909db2791..e365d69c25 100644 --- a/domain/build.gradle.kts +++ b/domain/build.gradle.kts @@ -71,7 +71,7 @@ android { } dependencies { - implementation(project(":network")) + implementation(project(":core:datasource")) implementation(project(":common")) /** Tangem libraries */ diff --git a/domain/src/main/java/com/tangem/domain/DomainDialog.kt b/domain/src/main/java/com/tangem/domain/DomainDialog.kt index bc7d30a7da..22e0ae8e28 100644 --- a/domain/src/main/java/com/tangem/domain/DomainDialog.kt +++ b/domain/src/main/java/com/tangem/domain/DomainDialog.kt @@ -1,7 +1,7 @@ package com.tangem.domain import com.tangem.common.extensions.VoidCallback -import com.tangem.network.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.CoinsResponse /** [REDACTED_AUTHOR] diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt b/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt index 3304349b69..925eaa5f82 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt @@ -1,8 +1,8 @@ package com.tangem.domain.common.extensions import com.tangem.common.services.Result -import com.tangem.network.api.tangemTech.CoinsResponse -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.TangemTechService suspend fun TangemTechService.getTokens( contractAddress: String, diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt index a502ae5041..98a63b6565 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt @@ -2,8 +2,8 @@ package com.tangem.domain.features.addCustomToken import com.tangem.common.services.Result import com.tangem.domain.common.extensions.getTokens -import com.tangem.network.api.tangemTech.CoinsResponse -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.TangemTechService /** [REDACTED_AUTHOR] diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt index 6949c1de51..3a5946f963 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt @@ -8,7 +8,7 @@ import com.tangem.domain.common.form.Field import com.tangem.domain.common.form.FieldId import com.tangem.domain.features.addCustomToken.CustomCurrency import com.tangem.domain.features.addCustomToken.CustomTokenFieldId -import com.tangem.network.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.CoinsResponse import org.rekotlin.Action /** diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt index 2b56a47f90..3a48aa34a7 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt @@ -56,7 +56,7 @@ import com.tangem.domain.redux.domainStore import com.tangem.domain.redux.extensions.dispatchOnMain import com.tangem.domain.redux.global.DomainGlobalAction import com.tangem.domain.redux.global.DomainGlobalState -import com.tangem.network.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.CoinsResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt index 6dde5b01c6..978232de57 100644 --- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt +++ b/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt @@ -37,7 +37,7 @@ import com.tangem.domain.features.addCustomToken.TokenDerivationPathField import com.tangem.domain.features.addCustomToken.TokenField import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.state.StringActionStateConverter -import com.tangem.network.api.tangemTech.CoinsResponse +import com.tangem.datasource.api.tangemTech.CoinsResponse import org.rekotlin.Action import org.rekotlin.StateType diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt index d80b0bab73..a0d0cb833b 100644 --- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt @@ -5,7 +5,7 @@ import com.tangem.common.extensions.toHexString import com.tangem.domain.redux.BaseStoreHub import com.tangem.domain.redux.DomainState import com.tangem.domain.redux.ReStoreReducer -import com.tangem.network.common.CardPublicKeyHttpInterceptor +import com.tangem.datasource.api.common.CardPublicKeyHttpInterceptor import org.rekotlin.Action /** diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt index b420816c55..a013b2128f 100644 --- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt +++ b/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt @@ -3,8 +3,8 @@ package com.tangem.domain.redux.global import com.tangem.domain.DomainDialog import com.tangem.domain.common.LogConfig import com.tangem.domain.common.ScanResponse -import com.tangem.network.api.paymentology.PaymentologyApiService -import com.tangem.network.api.tangemTech.TangemTechService +import com.tangem.datasource.api.paymentology.PaymentologyApiService +import com.tangem.datasource.api.tangemTech.TangemTechService /** [REDACTED_AUTHOR] @@ -19,6 +19,8 @@ data class DomainGlobalState( ) data class NetworkServices( - val tangemTechService: TangemTechService = TangemTechService(LogConfig.network.tangemTechService), - val paymentologyService: PaymentologyApiService = PaymentologyApiService(LogConfig.network.paymentologyApiService), + val tangemTechService: TangemTechService = TangemTechService( + LogConfig.network.tangemTechService), + val paymentologyService: PaymentologyApiService = PaymentologyApiService( + LogConfig.network.paymentologyApiService), ) diff --git a/network/.gitignore b/features/swap/data/.gitignore similarity index 100% rename from network/.gitignore rename to features/swap/data/.gitignore diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts new file mode 100644 index 0000000000..298948e089 --- /dev/null +++ b/features/swap/data/build.gradle.kts @@ -0,0 +1,37 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } +} + +dependencies { + + /** Project*/ + implementation(project(":core:datasource")) + implementation(project(":core:utils")) + implementation(project(":features:swap:domain")) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/swap/data/src/main/AndroidManifest.xml b/features/swap/data/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..5981b0aeee --- /dev/null +++ b/features/swap/data/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt new file mode 100644 index 0000000000..2095876a1b --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -0,0 +1,82 @@ +package com.tangem.feature.swap + +import com.tangem.datasource.api.oneinch.OneInchApi +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.feature.swap.converters.ApproveConverter +import com.tangem.feature.swap.converters.QuotesConverter +import com.tangem.feature.swap.converters.SwapConverter +import com.tangem.feature.swap.converters.TokensConverter +import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.models.ApproveModel +import com.tangem.feature.swap.domain.models.Currency +import com.tangem.feature.swap.domain.models.QuoteModel +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class SwapRepositoryImpl @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val oneInchApi: OneInchApi, + private val tokensConverter: TokensConverter, + private val quotesConverter: QuotesConverter, + private val swapConverter: SwapConverter, + private val approveConverter: ApproveConverter, + private val coroutineDispatcher: CoroutineDispatcherProvider, +) : SwapRepository { + + override suspend fun getExchangeableTokens(networkId: String): List { + return withContext(coroutineDispatcher.io) { + tokensConverter.convertList(tangemTechApi.coins(exchangeable = true, networkIds = networkId).coins) + } + } + + override suspend fun findBestQuote(fromTokenAddress: String, toTokenAddress: String, amount: String): QuoteModel { + return withContext(coroutineDispatcher.io) { + quotesConverter.convert( + oneInchApi.quote( + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount, + ), + ) + } + } + + override suspend fun addressForTrust(): String { + return withContext(coroutineDispatcher.io) { + oneInchApi.approveSpender().address + } + } + + override suspend fun dataToApprove(tokenAddress: String, amount: String): ApproveModel { + return withContext(coroutineDispatcher.io) { + approveConverter.convert(oneInchApi.approveTransaction(tokenAddress, amount)) + } + } + + override suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): String { + return withContext(coroutineDispatcher.io) { + oneInchApi.approveAllowance(tokenAddress, walletAddress).allowance + } + } + + override suspend fun prepareSwapTransaction( + fromTokenAddress: String, + toTokenAddress: String, + amount: String, + fromAddress: String, + slippage: Int, + ) { + return withContext(coroutineDispatcher.io) { + swapConverter.convert( + oneInchApi.swap( + fromTokenAddress = fromTokenAddress, + toTokenAddress = toTokenAddress, + amount = amount, + fromAddress = fromAddress, + slippage = slippage, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt new file mode 100644 index 0000000000..4d3574869d --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ApproveConverter.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse +import com.tangem.feature.swap.domain.models.ApproveModel +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class ApproveConverter @Inject constructor() : Converter { + + override fun convert(value: ApproveCalldataResponse): ApproveModel { + return ApproveModel( + data = value.data, + gasPrice = value.gasPrice, + toAddress = value.toAddress, + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt new file mode 100644 index 0000000000..52723c6596 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/QuotesConverter.kt @@ -0,0 +1,16 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.oneinch.models.QuoteResponse +import com.tangem.feature.swap.domain.models.QuoteModel +import com.tangem.utils.converter.Converter + +class QuotesConverter : Converter { + + override fun convert(value: QuoteResponse): QuoteModel { + return QuoteModel( + fromTokenAmount = value.fromTokenAmount, + toTokenAmount = value.toTokenAmount, + estimatedGas = value.estimatedGas, + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt new file mode 100644 index 0000000000..244ae4f2a0 --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapConverter.kt @@ -0,0 +1,24 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.oneinch.models.SwapResponse +import com.tangem.feature.swap.domain.models.SwapTransactionModel +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class SwapConverter @Inject constructor() : Converter { + + override fun convert(value: SwapResponse): SwapTransactionModel { + return SwapTransactionModel( + fromTokenAddress = value.fromToken.address, + toTokenAddress = value.toToken.address, + toTokenAmount = value.fromTokenAmount, + fromTokenAmount = value.toTokenAmount, + fromWalletAddress = value.transaction.fromAddress, + toWalletAddress = value.transaction.toAddress, + data = value.transaction.data, + value = value.transaction.value, + gasPrice = value.transaction.gasPrice, + gas = value.transaction.gas, + ) + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt new file mode 100644 index 0000000000..c5414d666c --- /dev/null +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/TokensConverter.kt @@ -0,0 +1,32 @@ +package com.tangem.feature.swap.converters + +import com.tangem.datasource.api.tangemTech.CoinsResponse +import com.tangem.feature.swap.domain.models.Currency +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +class TokensConverter @Inject constructor() : Converter { + + override fun convert(value: CoinsResponse.Coin): Currency { + val network = value.networks.first() + return if (network.contractAddress != null && network.decimalCount != null) { + Currency.NonNativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = network.networkId, + contractAddress = network.contractAddress!!, + decimalCount = network.decimalCount!!.intValueExact(), + logoUrl = "",//todo add logo + ) + } else { + Currency.NativeToken( + id = value.id, + name = value.name, + symbol = value.symbol, + networkId = network.networkId, + logoUrl = "", //todo add logo + ) + } + } +} \ No newline at end of file diff --git a/features/swap/domain/.gitignore b/features/swap/domain/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/domain/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts new file mode 100644 index 0000000000..e8259cee84 --- /dev/null +++ b/features/swap/domain/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("java-library") + id("org.jetbrains.kotlin.jvm") +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt new file mode 100644 index 0000000000..eebb325e5e --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -0,0 +1,4 @@ +package com.tangem.feature.swap.domain + +interface SwapInteractor { +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt new file mode 100644 index 0000000000..963f87bfdb --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -0,0 +1,52 @@ +package com.tangem.feature.swap.domain + +import com.tangem.feature.swap.domain.models.ApproveModel +import com.tangem.feature.swap.domain.models.Currency +import com.tangem.feature.swap.domain.models.QuoteModel + +interface SwapRepository { + + suspend fun getExchangeableTokens(networkId: String): List + + suspend fun findBestQuote( + fromTokenAddress: String, + toTokenAddress: String, + amount: String, + ): QuoteModel + + /** + * Returns address of 1inch router that must be trusted + * + * @return address + */ + suspend fun addressForTrust(): String + + /** + * Generate "data" for calling contract in order to allow 1inch spend funds + * + * @param tokenAddress token you want to exchange + * @param amount number of tokens is allowed. By default infinite + */ + suspend fun dataToApprove( + tokenAddress: String, + amount: String, + ): ApproveModel + + /** + * Get the number of tokens that the 1inch router is allowed to spend + * + * @param tokenAddress Token address you want to exchange + * @param walletAddress address for which you want to check + * + * @return amount of tokens allowed to spend + */ + suspend fun checkTokensSpendAllowance(tokenAddress: String, walletAddress: String): String + + suspend fun prepareSwapTransaction( + fromTokenAddress: String, + toTokenAddress: String, + amount: String, + fromAddress: String, + slippage: Int, + ) +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt new file mode 100644 index 0000000000..2fce5634f9 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ApproveModel.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.swap.domain.models + +/** + * Approve model + * + * @property data The encoded data to call the approve method on the swapped token contract + * @property gasPrice Gas price for fast transaction processing + * @property toAddress Token address that will be allowed to exchange through 1inch router + */ +data class ApproveModel( + val data: String, + val gasPrice: String, + val toAddress: String, +) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt new file mode 100644 index 0000000000..05fe0cad3b --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/Currency.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.swap.domain.models + +sealed class Currency( + open val id: String, + open val name: String, + open val symbol: String, + open val networkId: String, + open val logoUrl: String, +) { + + data class NativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, + override val logoUrl: String, + ) : Currency(id, name, symbol, networkId, logoUrl) + + data class NonNativeToken( + override val id: String, + override val name: String, + override val symbol: String, + override val networkId: String, + override val logoUrl: String, + val contractAddress: String, + val decimalCount: Int, + ) : Currency(id, name, symbol, networkId, logoUrl) +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt new file mode 100644 index 0000000000..e71378fd22 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/QuoteModel.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.swap.domain.models + +data class QuoteModel( + val fromTokenAmount: String, + val toTokenAmount: String, + val estimatedGas: Int +) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapTransactionModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapTransactionModel.kt new file mode 100644 index 0000000000..f36d74eb61 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapTransactionModel.kt @@ -0,0 +1,29 @@ +package com.tangem.feature.swap.domain.models + +/** + * Swap transaction model + * + * @property fromTokenAddress token from which want to convert + * @property toTokenAddress token to want to convert + * @property toTokenAmount amount "target" token + * @property fromTokenAmount amount "initial" token + * @property fromWalletAddress wallet from address (transactions will be sent from this address) + * @property toWalletAddress transactions will be sent to our(1inch) contract address + * @property data The encoded data to call the approve method on the swapped token contract + * @property value Native token value in WEI (for approve is always 0) + * @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000 + * @property gas estimated amount of the gas limit, increase this value by 25% + * @constructor Create empty Swap transaction model + */ +data class SwapTransactionModel( + val fromTokenAddress: String, + val toTokenAddress: String, + val toTokenAmount: String, + val fromTokenAmount: String, + val fromWalletAddress: String, + val toWalletAddress: String, + val data: String, + val value: String, + val gasPrice: String, + val gas: String, +) \ No newline at end of file diff --git a/features/swap/presentation/.gitignore b/features/swap/presentation/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/presentation/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts new file mode 100644 index 0000000000..8eac586d41 --- /dev/null +++ b/features/swap/presentation/build.gradle.kts @@ -0,0 +1,59 @@ +plugins { + id("com.android.library") + kotlin("android") + kotlin("kapt") + id("com.google.dagger.hilt.android") +} + +android { + + defaultConfig { + compileSdk = AppConfig.compileSdkVersion + minSdk = AppConfig.minSdkVersion + targetSdk = AppConfig.targetSdkVersion + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + isCoreLibraryDesugaringEnabled = false + } + + buildFeatures { + compose = true + } + + composeOptions { + kotlinCompilerExtensionVersion = Versions.compose + } +} + +dependencies { + /** Core modules */ + implementation(project(":core:utils")) + implementation(project(":core:ui")) + + /** AndroidX */ + implementation(AndroidX.appCompat) + implementation(AndroidX.fragmentKtx) + implementation(AndroidX.lifecycleViewModelKtx) + + /** Compose */ + implementation(Compose.foundation) + implementation(Compose.material) + implementation(Compose.uiTooling) + + /** Domain */ + implementation(project(":features:swap:domain")) + + /** Other libraries */ + implementation(Library.composeShimmer) + + /** DI */ + implementation(Library.hilt) + kapt(Library.hiltKapt) +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/AndroidManifest.xml b/features/swap/presentation/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..927ad53a71 --- /dev/null +++ b/features/swap/presentation/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/network/build.gradle.kts b/network/build.gradle.kts deleted file mode 100644 index 0c82757e05..0000000000 --- a/network/build.gradle.kts +++ /dev/null @@ -1,26 +0,0 @@ -plugins { - id("java-library") - id("org.jetbrains.kotlin.jvm") -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - implementation(project(":common")) - - /** Tangem libraries */ - implementation(Tangem.cardCore) - - /** Other libraries */ - implementation(Library.retrofit) - implementation(Library.retrofitMoshiConverter) - implementation(Library.moshi) - implementation(Library.moshiKotlin) - implementation(Library.okHttp) - implementation(Library.okHttpLogging) - implementation(Library.timber) - implementation(Library.coroutine) -} \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/common/NetworError.kt b/network/src/main/java/com/tangem/network/common/NetworError.kt deleted file mode 100644 index dfe852ecc9..0000000000 --- a/network/src/main/java/com/tangem/network/common/NetworError.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.network.common - -import com.tangem.common.module.ModuleError -import com.tangem.common.module.ModuleErrorCode -import com.tangem.common.module.ModuleMessage - -/** -[REDACTED_AUTHOR] - */ -sealed interface NetworkModuleMessage : ModuleMessage - -sealed class NetworkError( - subCode: Int, - override val message: String, - override val data: Any?, -) : NetworkModuleMessage, ModuleError() { - override val code: Int = ModuleErrorCode.NETWORK + subCode - - companion object { - // base code used for all error in the module - // const val CODE_ANY_OTHER = 100..199, 200..299 - } -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 013139ff41..028fd3f613 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,5 @@ include(":app") include(":domain") -include(":network") include(":common") // region Core modules @@ -19,4 +18,7 @@ include(":libs:auth") include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") +include(":features:swap:data") +include(":features:swap:domain") +include(":features:swap:presentation") // endregion Feature modules \ No newline at end of file