diff --git a/app/build.gradle b/app/build.gradle index 439820d87c..2c09a36f9c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,10 +68,12 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'com.google.android.material:material:1.2.1' - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1' + implementation 'com.google.android.material:material:1.3.0' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + implementation "androidx.core:core-ktx:1.3.2" - implementation 'com.tangem:blockchain:1.141.0' +// implementation 'com.tangem:blockchain:1.141.0' + implementation 'blockchain-sdk-kotlin:blockchain:tangem-20210302.084810-3' implementation 'com.tangem:core:1.101.0' implementation 'com.tangem:sdk:1.101.0' @@ -79,10 +81,10 @@ dependencies { implementation "androidx.browser:browser:1.3.0" //lifecycle - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.3.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0" implementation "org.rekotlin:rekotlin:1.0.4" diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index 6497a63edc..d5f031080b 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -13,6 +13,7 @@ import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.createMoshi import com.tangem.tap.persistence.PreferencesStorage @@ -26,6 +27,7 @@ val store = Store( state = AppState() ) lateinit var preferencesStorage: PreferencesStorage +lateinit var currenciesRepository: CurrenciesRepository class TapApplication : Application() { override fun onCreate() { @@ -45,6 +47,7 @@ class TapApplication : Application() { NetworkConnectivity.createInstance(store, this) preferencesStorage = PreferencesStorage(this) PicassoHelper.initPicassoWithCaching(this) + currenciesRepository = CurrenciesRepository(this) loadConfigs() } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt new file mode 100644 index 0000000000..b2905ec473 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/AssetManager.kt @@ -0,0 +1,6 @@ +package com.tangem.tap.common.extensions + +import android.content.res.AssetManager + +fun AssetManager.readJsonFileToString(fileName: String): String = + this.open("$fileName.json").bufferedReader().readText() \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt new file mode 100644 index 0000000000..e03e96133b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.extensions + +import androidx.annotation.DrawableRes +import com.tangem.blockchain.common.Blockchain +import com.tangem.wallet.R + +@DrawableRes +fun Blockchain.getIconRes(): Int { + return when (this) { + Blockchain.Unknown, Blockchain.Ducatus, Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet, + Blockchain.BinanceTestnet -> 1 + Blockchain.Bitcoin -> R.drawable.ic_btc + Blockchain.BitcoinCash -> R.drawable.ic_btc_cash + Blockchain.Litecoin -> R.drawable.ic_ltc + Blockchain.Ethereum -> R.drawable.ic_eth + Blockchain.RSK -> R.drawable.ic_rsk + Blockchain.Cardano, Blockchain.CardanoShelley -> R.drawable.ic_cardano + Blockchain.Binance -> R.drawable.ic_binance + Blockchain.Tezos -> R.drawable.ic_tezos + Blockchain.XRP -> R.drawable.ic_xrp + Blockchain.Stellar -> R.drawable.ic_stellar + } +} \ No newline at end of file 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 56f0d70061..6e348ea8cf 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 @@ -12,6 +12,8 @@ import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment import com.tangem.tap.features.home.HomeFragment import com.tangem.tap.features.send.ui.SendFragment +import com.tangem.tap.features.tokens.ui.AddTokensFragment +import com.tangem.tap.features.wallet.ui.WalletDetailsFragment import com.tangem.tap.features.wallet.ui.WalletFragment import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment import com.tangem.wallet.R @@ -46,5 +48,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment { AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment() AppScreen.CreateTwinWallet -> CreateTwinWalletFragment() AppScreen.TwinsOnboarding -> TwinsOnboardingFragment() + AppScreen.AddTokens -> AddTokensFragment() + AppScreen.WalletDetails -> WalletDetailsFragment() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index 42fc659da8..8ed277a345 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -1,11 +1,5 @@ package com.tangem.tap.common.extensions -import android.graphics.Bitmap -import android.graphics.Color -import com.google.zxing.BarcodeFormat -import com.google.zxing.EncodeHintType -import com.google.zxing.qrcode.QRCodeWriter -import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.tangem.common.extensions.isZero import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.network.coinmarketcap.FiatCurrency @@ -15,27 +9,6 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import java.util.* - -fun String.toQrCode(): Bitmap { - val hintMap = Hashtable() - hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage - hintMap[EncodeHintType.MARGIN] = 2 - - val qrCodeWriter = QRCodeWriter() - - val size = 256 - - val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) - val width = bitMatrix.width - val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) - for (x in 0 until width) { - for (y in 0 until width) { - bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) - } - } - return bmp -} - fun BigDecimal.toFormattedString( decimals: Int, roundingMode: RoundingMode = RoundingMode.DOWN, locale: Locale = Locale.US ): String { diff --git a/app/src/main/java/com/tangem/tap/common/extensions/String.kt b/app/src/main/java/com/tangem/tap/common/extensions/String.kt new file mode 100644 index 0000000000..f0f4ef1d00 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/String.kt @@ -0,0 +1,62 @@ +package com.tangem.tap.common.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Color +import android.text.Spannable +import android.text.style.ForegroundColorSpan +import androidx.core.content.ContextCompat +import androidx.core.text.toSpannable +import com.google.zxing.BarcodeFormat +import com.google.zxing.EncodeHintType +import com.google.zxing.qrcode.QRCodeWriter +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel +import java.util.* + +fun String?.ellipsizeBeforeSpace(allowedSize: Int): String { + if (this.isNullOrBlank()) return "" + val size = this.length + val sizeDifference = size - allowedSize + val endIndex = this.indexOf(" ") + val startIndex = endIndex - sizeDifference + val newString = this.removeRange(startIndex, endIndex) + return newString.substring(0 until startIndex) + "..." + + newString.substring(startIndex until newString.length) +} + +fun String.colorSegment( + context: Context, + color: Int, + startIndex: Int = 0, + endIndex: Int = this.length +): Spannable { + return this.toSpannable() + .also { spannable -> + spannable.setSpan( + ForegroundColorSpan(ContextCompat.getColor(context, color)), + startIndex, + endIndex, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } +} + +fun String.toQrCode(): Bitmap { + val hintMap = Hashtable() + hintMap[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.M // H = 30% damage + hintMap[EncodeHintType.MARGIN] = 2 + + val qrCodeWriter = QRCodeWriter() + + val size = 256 + + val bitMatrix = qrCodeWriter.encode(this, BarcodeFormat.QR_CODE, size, size, hintMap) + val width = bitMatrix.width + val bmp = Bitmap.createBitmap(width, width, Bitmap.Config.RGB_565) + for (x in 0 until width) { + for (y in 0 until width) { + bmp.setPixel(y, x, if (bitMatrix.get(x, y)) Color.BLACK else Color.WHITE) + } + } + return bmp +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt index 5774e94b61..48ee67afc7 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/TextView.kt @@ -56,4 +56,13 @@ fun TextInputLayout.enableError(enable: Boolean, errorMessage: String? = null) { error = null isErrorEnabled = false } +} + +fun TextView.isEllipsized(): Boolean { + val layout = this.layout + if (layout != null) { + val lines: Int = layout.lineCount + return (lines > 0) && (layout.getEllipsisCount(lines - 1) > 0) + } + return false } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Token.kt b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt new file mode 100644 index 0000000000..f9499f8e70 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.extensions + +import androidx.annotation.ColorInt +import androidx.core.graphics.toColorInt +import com.tangem.blockchain.common.Token + +@ColorInt +fun Token.getColor(): Int { + return ("#" + this.contractAddress.subSequence(2..7).toString()) + .toColorInt() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index c7bb0daf0f..a6a0d303ac 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -8,8 +8,6 @@ import android.content.Intent import android.content.res.Resources import android.graphics.drawable.Drawable import android.os.Build -import android.text.Spannable -import android.text.style.ForegroundColorSpan import android.util.TypedValue import android.view.View import android.view.ViewGroup @@ -19,7 +17,6 @@ import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.core.content.ContextCompat -import androidx.core.text.toSpannable import androidx.fragment.app.Fragment import com.google.android.material.card.MaterialCardView @@ -106,24 +103,6 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) { } } - -fun String.colorSegment( - context: Context, - color: Int, - startIndex: Int = 0, - endIndex: Int = this.length -): Spannable { - return this.toSpannable() - .also { spannable -> - spannable.setSpan( - ForegroundColorSpan(ContextCompat.getColor(context, color)), - startIndex, - endIndex, - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE - ) - } -} - fun View.hideKeyboard() { val inputMethodManager = context.getSystemService(android.content.Context.INPUT_METHOD_SERVICE) as? InputMethodManager inputMethodManager?.hideSoftInputFromWindow(this.windowToken, 0) diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 5e80adec68..c60bdaa8f1 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -6,6 +6,7 @@ import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer import com.tangem.tap.features.home.redux.HomeReducer import com.tangem.tap.features.send.redux.reducers.SendScreenReducer +import com.tangem.tap.features.tokens.redux.TokensReducer import com.tangem.tap.features.wallet.redux.WalletReducer import org.rekotlin.Action @@ -20,7 +21,8 @@ fun appReducer(action: Action, state: AppState?): AppState { walletState = WalletReducer.reduce(action, state), sendState = SendScreenReducer.reduce(action, state.sendState), detailsState = DetailsReducer.reduce(action, state), - disclaimerState = DisclaimerReducer.reduce(action, state) + disclaimerState = DisclaimerReducer.reduce(action, state), + tokensState = TokensReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e60ae97fa7..4548ca046f 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -12,8 +12,10 @@ import com.tangem.tap.features.home.redux.HomeMiddleware import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.send.redux.middlewares.sendMiddleware import com.tangem.tap.features.send.redux.states.SendState -import com.tangem.tap.features.wallet.redux.WalletMiddleware +import com.tangem.tap.features.tokens.redux.TokensMiddleware +import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.middlewares.WalletMiddleware import org.rekotlin.Middleware import org.rekotlin.StateType @@ -24,7 +26,8 @@ data class AppState( val walletState: WalletState = WalletState(), val sendState: SendState = SendState(), val detailsState: DetailsState = DetailsState(), - val disclaimerState: DisclaimerState = DisclaimerState() + val disclaimerState: DisclaimerState = DisclaimerState(), + val tokensState: TokensState = TokensState(), ) : StateType { companion object { @@ -35,7 +38,8 @@ data class AppState( WalletMiddleware().walletMiddleware, sendMiddleware, DetailsMiddleware().detailsMiddleware, - DisclaimerMiddleware().disclaimerMiddleware + DisclaimerMiddleware().disclaimerMiddleware, + TokensMiddleware().tokensMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index f47245676d..49bc412a5d 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -6,14 +6,10 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.features.details.redux.SecurityOption import org.rekotlin.Action -import java.math.BigDecimal sealed class GlobalAction : Action { data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction() - data class SetFiatRate( - val fiatRates: Pair - ) : GlobalAction() data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction() object RestoreAppCurrency : GlobalAction() { data class Success(val appCurrency: FiatCurrencyName) : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index a0534fe8af..16892e5475 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -23,7 +23,7 @@ val globalMiddleware: Middleware = { dispatch, appState -> if (it.hideWarning(action.warning)) { if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) { //TODO: No appropriate warningMessage identification. Make it better later - store.dispatch(WalletAction.SaveCardId) + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) } store.dispatch(WalletAction.SetWarnings(it.getWarnings(WarningMessage.Location.MainScreen))) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index afbb6d5b7e..fcd42c9395 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -13,16 +13,11 @@ fun globalReducer(action: Action, state: AppState): GlobalState { return when (action) { is GlobalAction.SaveScanNoteResponse -> globalState.copy(scanNoteResponse = action.scanNoteResponse) - is GlobalAction.SetFiatRate -> { - val rates = globalState.conversionRates.rates.toMutableMap() - rates[action.fiatRates.first] = action.fiatRates.second - globalState.copy(conversionRates = ConversionRates(rates)) - } is GlobalAction.ChangeAppCurrency -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.RestoreAppCurrency.Success -> { - globalState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf())) + globalState.copy(appCurrency = action.appCurrency) } is GlobalAction.UpdateWalletSignedHashes -> { val card = globalState.scanNoteResponse?.card?.copy( diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 2ee123a725..4d96b02a3c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -9,7 +9,6 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.domain.tasks.ScanNoteResponse import com.tangem.tap.network.coinmarketcap.CoinMarketCapService import org.rekotlin.StateType -import java.math.BigDecimal data class GlobalState( val scanNoteResponse: ScanNoteResponse? = null, @@ -17,21 +16,11 @@ data class GlobalState( val payIdManager: PayIdManager = PayIdManager(), val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(), val tangemService: TangemService = TangemService(), - val conversionRates: ConversionRates = ConversionRates(emptyMap()), val configManager: ConfigManager? = null, val warningManager: WarningMessagesManager? = null, val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY ) : StateType -data class ConversionRates( - val rates: Map, -) { - - fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? { - return rates[cryptoCurrency] - } -} - typealias CryptoCurrencyName = String typealias FiatCurrencyName = String diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt index 393a4e7e77..26344b0175 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt @@ -10,6 +10,6 @@ data class NavigationState( ) : StateType enum class AppScreen { - Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, - CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding + Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer, + CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt index 3773bd41b7..a6e329ca04 100644 --- a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt @@ -2,46 +2,13 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.Blockchain import com.tangem.commands.common.network.Result -import com.tangem.tap.network.payid.PayIdService import com.tangem.tap.network.payid.PayIdVerifyService -import com.tangem.tap.network.payid.SetPayIdResponse import com.tangem.tap.network.payid.VerifyPayIdResponse import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import retrofit2.HttpException import java.util.* class PayIdManager { - private val payIdService = PayIdService() - - suspend fun getPayId(cardId: String, publicKey: String): Result = withContext(Dispatchers.IO) { - val result = payIdService.getPayId(cardId, publicKey) - when (result) { - is Result.Success -> return@withContext Result.Success(result.data.payId) - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 404) return@withContext Result.Success(null) - } - return@withContext result - } - } - } - - - suspend fun setPayId( - cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain - ): Result = withContext(Dispatchers.IO) { - val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork()) - when (result) { - is Result.Success -> return@withContext result - is Result.Failure -> { - (result.error as? HttpException)?.let { - if (it.code() == 409) return@withContext Result.Failure(TapError.PayIdAlreadyCreated) - } - return@withContext result - } - } - } suspend fun verifyPayId(payId: String, blockchain: Blockchain): Result = withContext(Dispatchers.IO) { val splitPayId = payId.split("\$") diff --git a/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt new file mode 100644 index 0000000000..b9ec4705a5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/TapBlockchainConfig.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.domain + +class TapBlockchainConfig { + companion object { + val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT" + val blockcypherTokens: Set = setOf( + "aa8184b0e0894b88a5688e01b3dc1e82", + "56c4ca23c6484c8f8864c32fde4def8d", + "66a8a37c5e9d4d2c9bb191acfe7f93aa" + ) + val infuraProjectId: String = "613a0b14833145968b1f656240c7d245" + } +} \ No newline at end of file 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 8ffe1480c6..d62759f6f4 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,23 +1,22 @@ package com.tangem.tap.domain -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.* import com.tangem.commands.common.card.CardStatus import com.tangem.commands.common.network.Result -import com.tangem.common.extensions.toHexString import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.currenciesRepository import com.tangem.tap.domain.configurable.config.ConfigManager import com.tangem.tap.domain.extensions.amountToCreateAccount import com.tangem.tap.domain.extensions.isNoAccountError import com.tangem.tap.domain.tasks.ScanNoteResponse +import com.tangem.tap.domain.tasks.isMultiwalletAllowed import com.tangem.tap.domain.twins.TwinsHelper import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity import com.tangem.tap.network.coinmarketcap.CoinMarketCapService @@ -28,29 +27,25 @@ import java.math.BigDecimal class TapWalletManager { - private val payIdManager = PayIdManager() private val coinMarketCapService = CoinMarketCapService() - suspend fun loadWalletData() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - store.dispatch(WalletAction.LoadWallet.Failure()) - return + private val blockchainSdkConfig by lazy { + store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig() + } + private val walletManagerFactory: WalletManagerFactory + by lazy { WalletManagerFactory(blockchainSdkConfig) } + + suspend fun loadWalletData(walletManager: WalletManager) { + val result = try { + walletManager.update() + Result.Success(walletManager.wallet) + } catch (exception: Exception) { + Result.Failure(exception) } - loadWallet(walletManager) + handleUpdateWalletResult(result, walletManager) } - suspend fun loadPayId() { - val result = loadPayIdIfNeeded() - result?.let { handlePayIdResult(it) } - } - - suspend fun updateWallet() { - val walletManager = store.state.globalState.scanNoteResponse?.walletManager - if (walletManager == null) { - withContext(Dispatchers.Main) { store.dispatch(WalletAction.UpdateWallet.Failure()) } - return - } + suspend fun updateWallet(walletManager: WalletManager) { val result = try { walletManager.update() Result.Success(walletManager.wallet) @@ -68,12 +63,18 @@ class TapWalletManager { } - suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) { - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet ?: return - + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) { val currencyList = wallet.getTokens().map { it.symbol }.toMutableList() currencyList.add(wallet.blockchain.currency) + loadFiatRate(fiatCurrency, currencyList) + } + suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, cryptoCurrencyName: CryptoCurrencyName) { + val currencyList = listOf(cryptoCurrencyName) + loadFiatRate(fiatCurrency, currencyList) + } + + private suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencyList: List) { val results = mutableListOf?>>() currencyList.forEach { results.add(it to coinMarketCapService.getRate(it, fiatCurrency)) } handleFiatRatesResult(results) @@ -88,22 +89,20 @@ class TapWalletManager { store.state.globalState.warningManager?.setBlockchain(data.walletManager?.wallet?.blockchain) val configManager = store.state.globalState.configManager if (TapWorkarounds.isStart2Coin) { - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled) configManager?.turnOff(ConfigManager.isTopUpEnabled) } else if (data.walletManager?.wallet?.blockchain == Blockchain.Bitcoin - || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id){ - configManager?.turnOff(ConfigManager.isWalletPayIdEnabled) + || data.card.cardData?.blockchainName == Blockchain.Bitcoin.id) { configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) configManager?.resetToDefault(ConfigManager.isTopUpEnabled) } else { - configManager?.resetToDefault(ConfigManager.isWalletPayIdEnabled) configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled) configManager?.resetToDefault(ConfigManager.isTopUpEnabled) } withContext(Dispatchers.Main) { store.dispatch(WalletAction.ResetState) store.dispatch(GlobalAction.SaveScanNoteResponse(data)) + store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) if (data.card.isTwinCard()) { val secondCardId = TwinsHelper.getTwinsCardId(data.card.cardId) val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId) @@ -113,13 +112,17 @@ class TapWalletManager { )) } } + if (data.walletManager?.wallet?.blockchain == Blockchain.Ethereum || + data.walletManager?.wallet?.blockchain == Blockchain.EthereumTestnet) { + store.dispatch(TokensAction.LoadCardTokens) + } loadData(data) } } suspend fun loadData(data: ScanNoteResponse) { withContext(Dispatchers.Main) { - store.dispatch(WalletAction.CheckIfWarningNeeded) + store.dispatch(WalletAction.CheckSignedHashes.CheckIfWarningNeeded) val artworkId = data.verifyResponse?.artworkInfo?.id if (data.walletManager != null) { if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { @@ -128,11 +131,40 @@ class TapWalletManager { } val config = store.state.globalState.configManager?.config ?: return@withContext - store.dispatch(WalletAction.LoadWallet( - data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, config.isTopUpEnabled)) + val primaryWalletManager = data.walletManager + val primaryBlockchain = listOf(data.walletManager.wallet.blockchain) + val primaryToken = primaryWalletManager.presetTokens.toList() + + store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(primaryBlockchain[0])) + if (primaryWalletManager.presetTokens.isNotEmpty()) { + store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken.first())) + } + + if (data.card.isMultiwalletAllowed) { + val savedCurrencies = currenciesRepository.loadCardCurrencies(data.card.cardId) + val tokens = if (savedCurrencies.tokens.isNotEmpty()) { + primaryToken + savedCurrencies.tokens + } else { + primaryToken + } + + val walletManagers = listOf(primaryWalletManager) + + currenciesRepository.getBlockchains() + .filterNot { it == primaryWalletManager.wallet.blockchain } + .mapNotNull { walletManagerFactory.makeWalletManager(data.card, it) } + val otherBlockhains = savedCurrencies.blockchains + + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) + store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain + otherBlockhains)) + store.dispatch(WalletAction.MultiWallet.AddTokens(tokens)) + } else { + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(listOf(primaryWalletManager))) + store.dispatch(WalletAction.MultiWallet.AddBlockchains(primaryBlockchain)) + } + store.dispatch(WalletAction.SetArtworkId(data.verifyResponse?.artworkInfo?.id)) + store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled)) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) - store.dispatch(WalletAction.LoadFiatRate) - store.dispatch(WalletAction.LoadPayId) + store.dispatch(WalletAction.LoadFiatRate()) } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { store.dispatch(WalletAction.EmptyWallet) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) @@ -143,14 +175,22 @@ class TapWalletManager { } } - private suspend fun loadWallet(walletManager: WalletManager) { - val result = try { - walletManager.update() - Result.Success(walletManager.wallet) - } catch (exception: Exception) { - Result.Failure(exception) + suspend fun reloadData(data: ScanNoteResponse) { + withContext(Dispatchers.Main) { + if (data.walletManager != null) { + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) { + store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection)) + return@withContext + } + val config = store.state.globalState.configManager?.config ?: return@withContext + store.dispatch(WalletAction.LoadWallet(config.isTopUpEnabled)) + store.dispatch(WalletAction.LoadFiatRate()) + } else if (data.card.status == CardStatus.Empty || data.card.isTwinCard()) { + store.dispatch(WalletAction.EmptyWallet) + } else { + store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain)) + } } - handleUpdateWalletResult(result, walletManager) } private suspend fun handleUpdateWalletResult(result: Result, walletManager: WalletManager) { @@ -164,59 +204,24 @@ class TapWalletManager { val token = walletManager.wallet.getFirstToken() val amountToCreateAccount = blockchain.amountToCreateAccount(token) if (amountToCreateAccount != null) { - store.dispatch(WalletAction.LoadWallet.NoAccount(amountToCreateAccount.toString())) + store.dispatch(WalletAction.LoadWallet.NoAccount( + walletManager.wallet, + amountToCreateAccount.toString())) return@withContext } } - store.dispatch(WalletAction.LoadWallet.Failure(result.error?.localizedMessage)) + store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage)) } } } } - - private suspend fun loadPayIdIfNeeded(): Result? { - val scanNoteResponse = store.state.globalState.scanNoteResponse - if (store.state.globalState.configManager?.config?.isWalletPayIdEnabled == false - || scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) { - return null - } - val cardId = scanNoteResponse?.card?.cardId - val publicKey = scanNoteResponse?.card?.cardPublicKey - if (cardId == null || publicKey == null) { - return null - } - return payIdManager.getPayId(cardId, publicKey.toHexString()) - } - - private suspend fun handlePayIdResult(result: Result) { - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> { - val config = store.state.globalState.configManager?.config - if (config?.isWalletPayIdEnabled == false) { - store.dispatch(WalletAction.DisablePayId) - return@withContext - } - val payId = result.data - if (payId == null) { - store.dispatch(WalletAction.LoadPayId.NotCreated) - } else { - store.dispatch(WalletAction.LoadPayId.Success(payId)) - } - } - is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure) - } - } - } - private suspend fun handleFiatRatesResult(results: List?>>) { withContext(Dispatchers.Main) { results.map { when (it.second) { is Result.Success -> { val rate = it.first to (it.second as Result.Success).data - store.dispatch(GlobalAction.SetFiatRate(rate)) store.dispatch(WalletAction.LoadFiatRate.Success(rate)) } is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure) diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index 67025b175d..b1ccd85929 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.configurable.config +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.tangem_sdk_new.ui.animation.VoidCallback import com.tangem.tap.domain.configurable.Loader @@ -10,7 +11,7 @@ data class Config( val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de", val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE", val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C", - val isWalletPayIdEnabled: Boolean = true, + val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(), val isSendingToPayIdEnabled: Boolean = true, val isTopUpEnabled: Boolean = false, val isCreatingTwinCardsAllowed: Boolean = false @@ -39,7 +40,6 @@ class ConfigManager( fun turnOff(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = false) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = false) isTopUpEnabled -> config = config.copy(isTopUpEnabled = false) isCreatingTwinCardsAllowed -> config = config.copy(isCreatingTwinCardsAllowed = false) @@ -48,7 +48,6 @@ class ConfigManager( fun resetToDefault(name: String) { when (name) { - isWalletPayIdEnabled -> config = config.copy(isWalletPayIdEnabled = defaultConfig.isWalletPayIdEnabled) isSendingToPayIdEnabled -> config = config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled) isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled) isCreatingTwinCardsAllowed -> config = @@ -60,13 +59,11 @@ class ConfigManager( val model = featureModel ?: return config = config.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed ) defaultConfig = defaultConfig.copy( - isWalletPayIdEnabled = model.isWalletPayIdEnabled, isTopUpEnabled = model.isTopUpEnabled, isSendingToPayIdEnabled = model.isSendingToPayIdEnabled, isCreatingTwinCardsAllowed = model.isCreatingTwinCardsAllowed @@ -78,22 +75,28 @@ class ConfigManager( config = config.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) defaultConfig = defaultConfig.copy( coinMarketCapKey = values.coinMarketCapKey, moonPayApiKey = values.moonPayApiKey, - moonPayApiSecretKey = values.moonPayApiSecretKey + moonPayApiSecretKey = values.moonPayApiSecretKey, + blockchainSdkConfig = BlockchainSdkConfig( + blockchairApiKey = values.blockchairApiKey, + blockcypherTokens = values.blockcypherTokens, + infuraProjectId = values.infuraProjectId + ) ) } companion object { - const val isWalletPayIdEnabled = "isWalletPayIdEnabled" const val isSendingToPayIdEnabled = "isSendingToPayIdEnabled" const val isCreatingTwinCardsAllowed = "isCreatingTwinCardsAllowed" const val isTopUpEnabled = "isTopUpEnabled" - const val coinMarketCapKey = "coinMarketCapKey" - const val moonPayApiKey = "moonPayApiKey" - const val moonPayApiSecretKey = "moonPayApiSecretKey" } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 32ce46da8b..69c5f0a8b0 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -15,6 +15,9 @@ class ConfigValueModel( val coinMarketCapKey: String, val moonPayApiKey: String, val moonPayApiSecretKey: String, + val blockchairApiKey: String?, + val blockcypherTokens: Set?, + val infuraProjectId: String?, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index 66c4238850..3ceeaadda6 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -4,32 +4,41 @@ import com.tangem.CardSession import com.tangem.CardSessionRunnable import com.tangem.TangemError import com.tangem.TangemSdkError +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.commands.CommandResponse import com.tangem.commands.ReadIssuerDataCommand import com.tangem.commands.common.card.Card import com.tangem.commands.common.card.CardStatus +import com.tangem.commands.common.card.EllipticCurve import com.tangem.commands.common.card.masks.Product import com.tangem.commands.verifycard.VerifyCardCommand import com.tangem.commands.verifycard.VerifyCardResponse import com.tangem.common.CompletionResult import com.tangem.common.extensions.toHexString import com.tangem.tap.domain.TapSdkError +import com.tangem.tap.domain.TapWorkarounds import com.tangem.tap.domain.twins.TwinCardsManager import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.store import com.tangem.tasks.ScanTask data class ScanNoteResponse( val walletManager: WalletManager?, val card: Card, val verifyResponse: VerifyCardResponse? = null, - val secondTwinPublicKey: String? = null + val secondTwinPublicKey: String? = null, + val multiwalletCard: Boolean = false, ) : CommandResponse class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { override val requiresPin2 = false + private val blockchainSdkConfig = store.state.globalState.configManager?.config + ?.blockchainSdkConfig ?: BlockchainSdkConfig() + private val walletManagerFactory = WalletManagerFactory(blockchainSdkConfig) + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { ScanTask().run(session) { result -> when (result) { @@ -53,7 +62,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable { callback(CompletionResult.Success(ScanNoteResponse( - walletManager, card, verifyResult.data, publicKey + walletManager, card, verifyResult.data, publicKey, + batchesAllowingMultiwallet.contains(card.cardData?.batchId) ))) } is CompletionResult.Failure -> { @@ -95,7 +105,7 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable> = moshi.adapter( + Types.newParameterizedType(List::class.java, TokenDao::class.java) + ) + private val blockchainsAdapter: JsonAdapter> = moshi.adapter( + Types.newParameterizedType(List::class.java, Blockchain::class.java) + ) + + fun loadCardCurrencies(cardId: String): CardCurrencies { + return CardCurrencies(loadSavedTokens(cardId), loadSavedBlockchains(cardId)) + } + + fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) { + saveTokens(cardId, currencies.tokens) + saveBlockchains(cardId, currencies.blockchains) + } + + fun saveAddedToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId) + token + saveTokens(cardId, tokens) + } + + fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId) + blockchain + saveBlockchains(cardId, blockchains) + } + + fun removeToken(cardId: String, token: Token) { + val tokens = loadSavedTokens(cardId).filterNot { it == token } + saveTokens(cardId, tokens) + } + + fun removeBlockchain(cardId: String, blockchain: Blockchain) { + val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain } + saveBlockchains(cardId, blockchains) + } + + private fun loadSavedTokens(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForTokens(cardId)) + tokensAdapter.fromJson(json)!!.map { it.toToken() } + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveTokens(cardId: String, tokens: List) { + val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }) + context.rewriteFile(json, getFileNameForTokens(cardId)) + } + + private fun loadSavedBlockchains(cardId: String): List { + return try { + val json = context.readFileText(getFileNameForBlockchains(cardId)) + blockchainsAdapter.fromJson(json) ?: emptyList() + } catch (exception: Exception) { + emptyList() + } + } + + private fun saveBlockchains(cardId: String, blockchains: List) { + val json = blockchainsAdapter.toJson(blockchains) + context.rewriteFile(json, getFileNameForBlockchains(cardId)) + } + + private fun Context.readFileText(fileName: String): String = + this.openFileInput(fileName).bufferedReader().readText() + + private fun Context.rewriteFile(content: String, fileName: String) { + this.openFileOutput(fileName, Context.MODE_PRIVATE).use { + it.write(content.toByteArray(), 0, content.length) + } + } + + fun getPopularTokens(): List { + val json = context.assets.readJsonFileToString(POPULAR_TOKENS_FILE_NAME) + return tokensAdapter.fromJson(json)!!.map { it.toToken() } + } + + fun getBlockchains(): List { + return listOf( + Blockchain.Bitcoin, Blockchain.BitcoinCash, Blockchain.Binance, Blockchain.Litecoin, + Blockchain.XRP, Blockchain.Tezos, + Blockchain.Ethereum, Blockchain.RSK) + } + + companion object { + private const val POPULAR_TOKENS_FILE_NAME = "erc20_tokens" + private const val FILE_NAME_PREFIX_TOKENS = "tokens" + private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains" + fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId" + fun getFileNameForBlockchains(cardId: String): String = "${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId" + } +} + + +@JsonClass(generateAdapter = true) +data class TokenDao( + val name: String, + val symbol: String, + val contractAddress: String, + val decimalCount: Int +) { + fun toToken(): Token { + return Token(name = name, symbol = symbol, contractAddress = contractAddress, decimals = decimalCount) + } + + companion object { + fun fromToken(token: Token): TokenDao { + return TokenDao(name = token.name, symbol = token.symbol, contractAddress = token.contractAddress, decimalCount = token.decimals) + + } + } +} + +@JsonClass(generateAdapter = true) +data class CardCurrenciesDao( + val tokens: List, + val blockchains: List +) { + fun toCardCurrencies(): CardCurrencies { + return CardCurrencies(tokens = tokens.map { it.toToken() }, blockchains = blockchains) + } + + companion object { + fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao { + return CardCurrenciesDao(tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }, blockchains = cardCurrencies.blockchains) + } + } +} + +data class CardCurrencies( + val tokens: List, + val blockchains: List +) \ No newline at end of file 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 2ec73c0f00..5bb753ff4c 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 @@ -17,7 +17,7 @@ sealed class DetailsAction : Action { data class PrepareScreen( val card: Card, val scanNoteResponse: ScanNoteResponse, - val wallet: Wallet?, + val wallets: List, val isCreatingTwinWalletAllowed: Boolean?, val fiatCurrencyName: FiatCurrencyName, val fiatCurrencies: List? = null, 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 95a0a3952c..b66aa958e9 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 @@ -104,7 +104,7 @@ class DetailsMiddleware { is DetailsAction.AppCurrencyAction.SelectAppCurrency -> { preferencesStorage.saveAppCurrency(action.fiatCurrencyName) store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName)) - store.dispatch(WalletAction.LoadFiatRate) + store.dispatch(WalletAction.LoadFiatRate()) } } } @@ -152,9 +152,7 @@ class DetailsMiddleware { store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure) } } - } - } } } 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 805d31f8c9..9c90579d69 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 @@ -66,14 +66,15 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta } else { null } + return DetailsState( - card = action.card, wallet = action.wallet, + card = action.card, wallets = action.wallets, cardInfo = action.card.toCardInfo(), appCurrencyState = AppCurrencyState( action.fiatCurrencyName ), securityScreenState = SecurityScreenState(currentOption = securityOption), - createTwinWalletState = twinsState + createTwinWalletState = twinsState, ) } @@ -81,8 +82,9 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS return when (action) { DetailsAction.EraseWallet.Check -> { val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true - val notEmpty = state.wallet?.recentTransactions?.isNullOrEmpty() != true || - state.wallet.amounts.toSendableAmounts().isNotEmpty() + val notEmpty = state.wallets.any { + !it.recentTransactions.isNullOrEmpty() || it.amounts.toSendableAmounts().isNotEmpty() + } val eraseWalletState = when { notAllowedByCard -> EraseWalletState.NotAllowedByCard notEmpty -> EraseWalletState.NotEmpty diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 4730507ebc..f4d58c3000 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -12,7 +12,7 @@ import java.util.* data class DetailsState( val card: Card? = null, - val wallet: Wallet? = null, + val wallets: List = emptyList(), val cardInfo: CardInfo? = null, val appCurrencyState: AppCurrencyState = AppCurrencyState(), val eraseWalletState: EraseWalletState? = null, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt index 38cfac39bc..d40bb904d7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/twins/CreateTwinWalletMiddleware.kt @@ -18,7 +18,7 @@ class CreateTwinWalletMiddleware { fun handle(action: DetailsAction.CreateTwinWalletAction) { when (action) { is DetailsAction.CreateTwinWalletAction.ShowWarning -> { - val wallet = store.state.detailsState.wallet + val wallet = store.state.detailsState.wallets.getOrNull(0) if (wallet == null) { store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning)) return diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 262482b360..50de2e5535 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -77,7 +77,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber { when (result.error) { @@ -142,7 +150,7 @@ private fun sendTransaction( } // make it easier latter by handling an appropriate enumError or, like on iOS, // accept a string identifier of the error message - message.contains("Target account is not created. To create account send 1+ XLM.")-> { + message.contains("Target account is not created. To create account send 1+ XLM.") -> { dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) } else -> { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt index 0d816d6d62..4144059510 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/SendScreenReducer.kt @@ -63,12 +63,10 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!! val decimals = amountToExtract.decimals - val coinConverter = createCurrencyConverter(walletManager.wallet.blockchain.currency, decimals) - val tokenConverter = createCurrencyConverter(prepareAction.tokenAmount?.currencySymbol ?: "", decimals) return sendState.copy( walletManager = walletManager, - coinConverter = coinConverter, - tokenConverter = tokenConverter, + coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) }, + tokenConverter = action.tokenRate?. let { CurrencyConverter(it, decimals) }, amountState = sendState.amountState.copy( amountToExtract = amountToExtract, typeOfAmount = amountToExtract.type, @@ -79,11 +77,6 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer { ) ) } - - private fun createCurrencyConverter(currency: String, decimals: Int): CurrencyConverter? { - val rate = store.state.globalState.conversionRates.getRate(currency) - return if (rate == null) null else CurrencyConverter(rate, decimals) - } } internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState { diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index ca47b5613e..cba07cdeb5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -34,8 +34,8 @@ import com.tangem.tap.features.send.redux.FeeActionUi.* import com.tangem.tap.features.send.redux.states.FeeType import com.tangem.tap.features.send.redux.states.MainCurrencyType import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber -import com.tangem.tap.features.wallet.ui.SpacesItemDecoration -import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.mainScope import com.tangem.tap.store import com.tangem.wallet.R diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 20d3988515..8084b7e5d5 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -22,7 +22,7 @@ import com.tangem.tap.features.send.redux.states.* import com.tangem.tap.features.send.ui.FeeUiHelper import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog -import com.tangem.tap.features.wallet.ui.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.btn_expand_collapse.* diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt new file mode 100644 index 0000000000..e0c9abe119 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Token +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.features.wallet.redux.WalletData +import org.rekotlin.Action + +sealed class TokensAction : Action { + + object LoadCurrencies : TokensAction() { + data class Success(val currencies: List) : TokensAction() + } + + object LoadCardTokens : TokensAction() { + data class Success(val tokens: List) : TokensAction() + } + + data class SetAddedCurrencies(val wallets: List) : TokensAction() + + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..7923d17bd4 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import com.tangem.tap.store +import org.rekotlin.Middleware + +class TokensMiddleware { + + val tokensMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + when (action) { + is TokensAction.LoadCurrencies -> { + val tokens = currenciesRepository.getPopularTokens() + val blockchains = currenciesRepository.getBlockchains() + val currencies = CurrencyListItem.createListOfCurrencies( + blockchains, tokens + ) + store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) + } + } + next(action) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt new file mode 100644 index 0000000000..e0c5600ad1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -0,0 +1,30 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +class TokensReducer { + companion object { + fun reduce(action: Action, state: AppState): TokensState = internalReduce(action, state) + } +} + +private fun internalReduce(action: Action, state: AppState): TokensState { + if (action !is TokensAction) return state.tokensState + + val tokensState = state.tokensState + return when (action) { + is TokensAction.LoadCurrencies.Success -> { + tokensState.copy(currencies = action.currencies) + } + is TokensAction.SetAddedCurrencies -> { + tokensState.copy(addedCurrencies = action.wallets.mapNotNull { it.currencyData.currencySymbol }) + } + is TokensAction.LoadCardTokens.Success -> { + tokensState.copy(addedTokens = LinkedHashSet( + action.tokens.map { TokenWithAmount(it, null) } + )) + } + else -> tokensState + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt new file mode 100644 index 0000000000..5169e63b96 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.tokens.redux + +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem +import org.rekotlin.StateType + +data class TokensState( + val addedTokens: LinkedHashSet = LinkedHashSet(), + val addedCurrencies: List = emptyList(), + val currencies: List = emptyList(), +) : StateType + +data class TokenWithAmount( + val token: Token, + val amount: Amount? = null +) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt new file mode 100644 index 0000000000..e3d8eb80fb --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -0,0 +1,119 @@ +package com.tangem.tap.features.tokens.ui + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensState +import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter +import com.tangem.tap.mainScope +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_add_tokens.* +import kotlinx.android.synthetic.main.fragment_details_twin_cards.toolbar +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import org.rekotlin.StoreSubscriber + + +class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), + StoreSubscriber { + + private lateinit var viewAdapter: CurrenciesAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.tokensState == newState.tokensState + }.select { it.tokensState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + setupPopularTokensRecyclerView() + } + + + private fun setupPopularTokensRecyclerView() { + viewAdapter = CurrenciesAdapter() + rv_popular_tokens.layoutManager = LinearLayoutManager(context) + rv_popular_tokens.adapter = viewAdapter + } + + + override fun newState(state: TokensState) { + if (activity == null) return + viewAdapter.addedCurrencies = state.addedCurrencies + viewAdapter.submitUnfilteredList(state.currencies) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_search -> { + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.popular_tokens, menu) + + val menuItem = menu.findItem(R.id.menu_search) + val searchView: SearchView = menuItem.actionView as SearchView + searchView.queryHint = "Type here to search" + searchView.maxWidth = android.R.attr.width + searchView.inputtedTextAsFlow() + .debounce(400) + .distinctUntilChanged() + .onEach { + viewAdapter.filter(searchView.query) + } + .launchIn(mainScope) + return super.onCreateOptionsMenu(menu, inflater); + } +} + +fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { + val watcher = setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + offer(newText ?: "") + return false + } + }) + awaitClose { (watcher) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt new file mode 100644 index 0000000000..9fe1ba05bd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -0,0 +1,165 @@ +package com.tangem.tap.features.tokens.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_subtitle.view.* +import kotlinx.android.synthetic.main.item_popular_token.view.* +import java.util.* + +class CurrenciesAdapter : ListAdapter(DiffUtilCallback) { + + var addedCurrencies: List = emptyList() + + private var unfilteredList = listOf() + + fun submitUnfilteredList(list: List) { + unfilteredList = list + submitList(list) + } + + override fun getItemViewType(position: Int): Int { + return when (currentList[position]) { + is CurrencyListItem.TitleListItem -> 0 + is CurrencyListItem.BlockchainListItem, is CurrencyListItem.TokenListItem -> 1 + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + 0 -> TitleViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_subtitle, parent, false)) + 1 -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + else -> CurrenciesViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_popular_token, parent, false)) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val listItem = currentList[position] + if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) { + holder.bind(listItem) + } else if (holder is CurrenciesViewHolder) { + holder.bind(listItem, addedCurrencies) + } + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: CurrencyListItem, newItem: CurrencyListItem + ) = oldItem == newItem + } + + fun filter(query: CharSequence?) { + val list = mutableListOf() + + if (!query.isNullOrEmpty()) { + val queryNormalized = query.toString().toLowerCase(Locale.US) + list.addAll( + unfilteredList.filter { element -> + when (element) { + is CurrencyListItem.BlockchainListItem -> { + element.blockchain.currency.toLowerCase(Locale.US).contains(queryNormalized) + || element.blockchain.fullName.toLowerCase(Locale.US).contains(queryNormalized) + + } + is CurrencyListItem.TokenListItem -> { + element.token.name.toLowerCase(Locale.US).contains(queryNormalized) + || element.token.symbol.toLowerCase(Locale.US).contains(queryNormalized) + } + else -> false + } + }) + } else { + list.addAll(unfilteredList) + } + submitList(list) + } + + class CurrenciesViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(currency: CurrencyListItem, addedCurrencies: List) { + when (currency) { + is CurrencyListItem.BlockchainListItem -> { + val blockchain = currency.blockchain + view.tv_currency_name.text = blockchain.fullName + view.tv_currency_symbol.text = blockchain.currency + val isAdded = addedCurrencies.any { it == blockchain.currency } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(currency.blockchain.getIconRes()) + view.iv_currency.colorFilter = null + view.tv_token_letter.text = null + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + is CurrencyListItem.TokenListItem -> { + val token = currency.token + view.tv_currency_name.text = token.name + view.tv_currency_symbol.text = token.symbol + + val isAdded = addedCurrencies.any { it == token.symbol } + view.btn_add_token.show(!isAdded) + view.btn_token_added.show(isAdded) + + view.iv_currency.setImageResource(R.drawable.shape_circle) + view.iv_currency.setColorFilter(token.getColor()) + view.tv_token_letter.text = token.symbol.take(1) + + view.btn_add_token.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.AddToken(token)) + view.btn_add_token.hide() + view.btn_token_added.show() + } + } + } + } + } + + class TitleViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + fun bind(title: CurrencyListItem.TitleListItem) { + view.tv_subtitle.text = view.getString(title.titleResId).toUpperCase(Locale.US) + } + } +} + + +sealed class CurrencyListItem { + data class TokenListItem(val token: Token) : CurrencyListItem() + data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem() + data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem() + + companion object { + fun createListOfCurrencies(blockchains: List, tokens: List): List { + val blockchainsTitle = R.string.add_tokens_subtitle_blockchains + val tokensTitle = R.string.add_tokens_subtitle_tokens + return listOf(TitleListItem(blockchainsTitle)) + + blockchains.map { BlockchainListItem(it) } + + listOf(TitleListItem(tokensTitle)) + + tokens.map { TokenListItem(it) } + } + } +} + diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt index 21cdc9d649..0ade915979 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/PendingTransaction.kt @@ -1,5 +1,6 @@ package com.tangem.tap.features.wallet.models +import com.tangem.blockchain.common.Token import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.tap.common.extensions.toFormattedString @@ -47,4 +48,13 @@ fun List.toPendingTransactions(walletAddress: String): List.removeUnknownTransactions(): List { return this.filter { it.type != PendingTransactionType.Unknown } +} + +fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: String): PendingTransaction? { + if (this.amount.currencySymbol != token.symbol) return null + return this.toPendingTransaction(walletAddress) +} + +fun List.toPendingTransactionsForToken(token: Token, walletAddress: String): List { + return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) } } \ No newline at end of file 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 5f11e932bf..222d18a108 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,11 +1,9 @@ package com.tangem.tap.features.wallet.redux import android.content.Context -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType import com.tangem.commands.common.card.Card -import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.TapError @@ -23,73 +21,94 @@ sealed class WalletAction : Action { data class Failure(val error: TapError) : WalletAction() } - data class LoadWallet( - val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean - ) : WalletAction() { + + data class LoadWallet(val allowTopUp: Boolean? = null, val currency: CryptoCurrencyName? = null) : WalletAction() { data class Success(val wallet: Wallet) : WalletAction() - data class NoAccount(val amountToCreateAccount: String) : WalletAction() - data class Failure(val errorMessage: String? = null) : WalletAction() + data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : WalletAction() + data class Failure(val wallet: Wallet, val errorMessage: String? = null) : WalletAction() } - object CheckIfWarningNeeded : WalletAction() - object CheckHashesCountOnline : WalletAction() - object NeedToCheckHashesCountOnline : WalletAction() - object ConfirmHashesCount : WalletAction() - data class SetWarnings(val warningList: List) : WalletAction() - object SaveCardId : WalletAction() + data class SetArtworkId(val artworkId: String?) : WalletAction() - object UpdateWallet : WalletAction() { +// sealed class ProcessWallet : WalletAction() { +// data class LoadWallet(val artworkId: String?, val allowTopUp: Boolean +// ) : ProcessWallet() { +// data class Success(val wallet: Wallet) : ProcessWallet() +// data class NoAccount(val wallet: Wallet, val amountToCreateAccount: String) : ProcessWallet() +// data class Failure(val wallet: Wallet, val errorMessage: String? = null) : ProcessWallet() +// } +// +// data class UpdateWallet(val currency: CryptoCurrencyName? = null) : ProcessWallet() { +// object ScheduleUpdatingWallet : ProcessWallet() +// data class Success(val wallet: Wallet) : ProcessWallet() +// data class Failure(val errorMessage: String? = null) : ProcessWallet() +// } +// } + + + sealed class MultiWallet : WalletAction() { + data class SetIsMultiwalletAllowed(val isMultiwalletAllowed: Boolean) : MultiWallet() + data class AddWalletManagers(val walletManagers: List) : MultiWallet() + data class AddBlockchain(val blockchain: Blockchain) : MultiWallet() + data class AddBlockchains(val blockchains: List) : MultiWallet() + data class AddTokens(val tokens: List) : MultiWallet() + data class AddToken(val token: Token) : MultiWallet() + data class TokenLoaded(val amount: Amount) : MultiWallet() + data class SelectWallet(val walletData: WalletData?) : MultiWallet() + data class RemoveWallet(val walletData: WalletData) : MultiWallet() + data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet() + data class SetPrimaryToken(val token: Token) : MultiWallet() + } + + sealed class CheckSignedHashes : WalletAction() { + object CheckIfWarningNeeded : CheckSignedHashes() + object CheckHashesCountOnline : CheckSignedHashes() + object NeedToCheckHashesCountOnline : CheckSignedHashes() + object ConfirmHashesCount : CheckSignedHashes() + object SaveCardId : CheckSignedHashes() + } + + data class SetWarnings(val warningList: List) : WalletAction() + + data class UpdateWallet(val currency: CryptoCurrencyName? = null) : WalletAction() { object ScheduleUpdatingWallet : WalletAction() data class Success(val wallet: Wallet) : WalletAction() data class Failure(val errorMessage: String? = null) : WalletAction() } - object LoadFiatRate : WalletAction() { - data class Success(val fiatRates: Pair) : WalletAction() + data class LoadFiatRate( + val wallet: Wallet? = null, val currency: CryptoCurrencyName? = null + ) : WalletAction() { + data class Success(val fiatRate: Pair) : WalletAction() object Failure : WalletAction() } - object LoadPayId : WalletAction() { - data class Success(val payId: String) : WalletAction() - object NotCreated : WalletAction() - object Failure : WalletAction() - } - - object DisablePayId : WalletAction() - data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() { data class Success(val artwork: Artwork) : WalletAction() object Failure : WalletAction() } - object Scan : WalletAction() - data class Send(val amount: Amount? = null) : WalletAction() { - data class ChooseCurrency(val amounts: List?) : WalletAction() - object Cancel : WalletAction() - } - object CreatePayId : WalletAction() { - data class CompleteCreatingPayId(val payId: String) : WalletAction() - data class Success(val payId: String) : WalletAction() - object EmptyField : WalletAction(), ErrorAction { - override val error = TapError.PayIdEmptyField - } - - class Failure(override val error: TapError) : WalletAction(), ErrorAction - object Cancel : WalletAction() - } - - data class CopyAddress(val context: Context) : WalletAction() { + data class CopyAddress(val address: String, val context: Context) : WalletAction() { object Success : WalletAction(), NotificationAction { override val messageResource = R.string.wallet_notification_address_copied } } + data class ShareAddress(val address: String, val context: Context) : WalletAction() + object ShowQrCode : WalletAction() object HideDialog : WalletAction() - data class ExploreAddress(val context: Context) : WalletAction() + data class ExploreAddress(val exploreUrl: String, val context: Context) : WalletAction() + object CreateWallet : WalletAction() object EmptyWallet : WalletAction() + object Scan : WalletAction() + + data class Send(val amount: Amount? = null) : WalletAction() { + data class ChooseCurrency(val amounts: List?) : WalletAction() + object Cancel : WalletAction() + } sealed class TopUpAction : WalletAction() { data class TopUp(val context: Context, val toolbarColor: Int) : TopUpAction() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt deleted file mode 100644 index a06b52a6b1..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletMiddleware.kt +++ /dev/null @@ -1,316 +0,0 @@ -package com.tangem.tap.features.wallet.redux - -import android.content.Intent -import android.net.Uri -import androidx.browser.customtabs.CustomTabsIntent -import androidx.core.content.ContextCompat -import com.tangem.blockchain.common.* -import com.tangem.blockchain.extensions.SimpleResult -import com.tangem.commands.common.card.Card -import com.tangem.commands.common.card.CardType -import com.tangem.commands.common.network.Result -import com.tangem.common.CompletionResult -import com.tangem.common.extensions.getType -import com.tangem.common.extensions.toHexString -import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler -import com.tangem.tap.common.extensions.copyToClipboard -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.PayIdManager -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.TopUpHelper -import com.tangem.tap.domain.configurable.warningMessage.WarningMessage -import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager -import com.tangem.tap.domain.extensions.toSendableAmounts -import com.tangem.tap.domain.twins.TwinsHelper -import com.tangem.tap.domain.twins.isTwinCard -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.twins.CreateTwinWallet -import com.tangem.tap.features.send.redux.PrepareSendScreen -import com.tangem.tap.features.wallet.models.toPendingTransactions -import com.tangem.tap.network.NetworkConnectivity -import com.tangem.tap.network.NetworkStateChanged -import com.tangem.tap.preferencesStorage -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.rekotlin.Action -import org.rekotlin.Middleware - -class WalletMiddleware { - private val topUpMiddleware = TopUpMiddleware() - - val walletMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - when (action) { - is WalletAction.TopUpAction -> topUpMiddleware.handle(action) - is WalletAction.LoadWallet -> { - scope.launch { - store.state.globalState.tapWalletManager.loadWalletData() - } - } - is WalletAction.LoadPayId -> { - scope.launch { - store.state.globalState.tapWalletManager.loadPayId() - } - } - is WalletAction.LoadFiatRate -> { - scope.launch { - store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency) - } - } - is WalletAction.CreateWallet -> { - if (store.state.walletState.twinCardsState != null) { - store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( - store.state.globalState.scanNoteResponse?.card?.cardId?.let { - TwinsHelper.getTwinCardNumber(it) - }, - CreateTwinWallet.CreateWallet - )) - } else { - scope.launch { - val result = tangemSdkManager.createWallet( - store.state.globalState.scanNoteResponse?.card?.cardId - ) - when (result) { - is CompletionResult.Success -> { - store.state.globalState.tapWalletManager - .onCardScanned(result.data) - } - - } - } - } - } - is WalletAction.UpdateWallet -> { - if (store.state.walletState.state == ProgressState.Done) { - scope.launch { store.state.globalState.tapWalletManager.updateWallet() } - } - } - is WalletAction.UpdateWallet.Success -> setupWalletUpdate(action.wallet) - is WalletAction.LoadWallet.Success -> { - store.dispatch(WalletAction.CheckHashesCountOnline) - if (!store.state.walletState.updatingWallet) setupWalletUpdate(action.wallet) - } - is WalletAction.CreatePayId.CompleteCreatingPayId -> { - scope.launch { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet - val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey - if (cardId != null && wallet != null && publicKey != null) { - val result = PayIdManager().setPayId( - cardId, publicKey.toHexString(), - action.payId, wallet.address, wallet.blockchain - ) - withContext(Dispatchers.Main) { - when (result) { - is Result.Success -> - store.dispatch(WalletAction.CreatePayId.Success(action.payId)) - is Result.Failure -> { - val error = result.error as? TapError - ?: TapError.PayIdCreatingError - store.dispatch(WalletAction.CreatePayId.Failure(error)) - } - } - } - } - } - } - is WalletAction.Scan -> { - scope.launch { - val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) - when (result) { - is CompletionResult.Success -> { - tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card) - store.state.globalState.tapWalletManager - .onCardScanned(result.data, true) - if (store.state.walletState.twinCardsState != null) { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) - } - } - } - } - } - } - is WalletAction.LoadData -> { - scope.launch { - store.state.globalState.scanNoteResponse?.let { - store.state.globalState.tapWalletManager.loadData(it) - } - } - } - is NetworkStateChanged -> { - store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> - store.dispatch(WalletAction.CheckHashesCountOnline) - scope.launch { - store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse) - } - } - } - is WalletAction.CopyAddress -> { - store.state.walletState.walletAddresses?.selectedAddress?.address?.let { - action.context.copyToClipboard(it) - store.dispatch(WalletAction.CopyAddress.Success) - } - } - is WalletAction.ExploreAddress -> { - val uri = Uri.parse(store.state.walletState.walletAddresses?.selectedAddress?.exploreUrl) - val intent = Intent(Intent.ACTION_VIEW, uri) - ContextCompat.startActivity(action.context, intent, null) - } - is WalletAction.Send -> { - val newAction = prepareSendAction(action.amount) - store.dispatch(newAction) - if (newAction is PrepareSendScreen) { - store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) - } - } - is WalletAction.CheckIfWarningNeeded -> { - val globalState = store.state.globalState - val validator = globalState.scanNoteResponse?.walletManager as? SignatureCountValidator - globalState.scanNoteResponse?.card?.let { card -> - store.state.globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local) - if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning()) - if (!preferencesStorage.wasCardScannedBefore(card.cardId)) { - checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) } - } - updateWarningMessages() - } - - } - is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline() - is WalletAction.SaveCardId -> { - val cardId = store.state.globalState.scanNoteResponse?.card?.cardId - cardId?.let { preferencesStorage.saveScannedCardId(it) } - } - is WalletAction.TwinsAction.SetTwinCard -> { - val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() - if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - preferencesStorage.saveTwinsOnboardingShown() - } - } - next(action) - } - } - } - - private fun setupWalletUpdate(wallet: Wallet) { - if (!wallet.recentTransactions.toPendingTransactions(wallet.address).isNullOrEmpty()) { - store.dispatch(WalletAction.UpdateWallet.ScheduleUpdatingWallet) - scope.launch(Dispatchers.IO) { - delay(10000) - withContext(Dispatchers.Main) { - store.dispatch(WalletAction.UpdateWallet) - } - } - } - } - - - private fun prepareSendAction(amount: Amount?): Action { - return if (amount != null) { - if (amount.type is AmountType.Token) { - PrepareSendScreen(store.state.walletState.wallet?.amounts?.get(AmountType.Coin), amount) - } else { - PrepareSendScreen(amount) - } - } else { - val amounts = store.state.walletState.wallet?.amounts?.toSendableAmounts() - if (amounts?.size ?: 0 > 1) { - WalletAction.Send.ChooseCurrency(amounts) - } else { - val amountToSend = amounts?.first() - PrepareSendScreen(amountToSend) - } - } - } - - private fun checkIfWarningNeeded( - card: Card, signatureCountValidator: SignatureCountValidator? = null, - ): WarningMessage? { - if (card.isTwinCard()) return null - - return if (signatureCountValidator == null) { - if (card.walletSignedHashes ?: 0 > 0) { - WarningMessagesManager.alreadySignedHashesWarning() - } else { - store.dispatch(WalletAction.SaveCardId) - null - } - } else { - store.dispatch(WalletAction.NeedToCheckHashesCountOnline) - null - } - } - - private fun checkHashesCountOnline() { - if (store.state.walletState.hashesCountVerified != false) return - if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return - - val card = store.state.globalState.scanNoteResponse?.card - if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return - - if (card.isTwinCard()) return - - val validator = store.state.globalState.scanNoteResponse?.walletManager - as? SignatureCountValidator - scope.launch { - val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) - withContext(Dispatchers.Main) { - when (result) { - SimpleResult.Success -> { - store.dispatch(WalletAction.ConfirmHashesCount) - store.dispatch(WalletAction.SaveCardId) - } - is SimpleResult.Failure -> - if (result.error is BlockchainSdkError.SignatureCountNotMatched) { - addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) - } else if (card.walletSignedHashes ?: 0 > 0) { - addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) - } - } - } - } - } - - private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { - store.state.globalState.warningManager?.addWarning(warning) - if (autoUpdate) updateWarningMessages() - } - - private fun updateWarningMessages() { - val warningManager = store.state.globalState.warningManager ?: return - store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen))) - } -} - -private class TopUpMiddleware { - fun handle(action: WalletAction.TopUpAction) { - when (action) { - is WalletAction.TopUpAction.TopUp -> { - val config = store.state.globalState.configManager?.config ?: return - val defaultAddress = store.state.walletState.walletAddresses!!.list[0].address - val url = TopUpHelper.getUrl( - store.state.walletState.currencyData.currencySymbol!!, - defaultAddress, - config.moonPayApiKey, - config.moonPayApiSecretKey - ) - val customTabsIntent = CustomTabsIntent.Builder() - .setToolbarColor(action.toolbarColor) - .build() - customTabsIntent.launchUrl(action.context, Uri.parse(url)); - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt index 275ad57895..a75c65a889 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt @@ -1,25 +1,26 @@ package com.tangem.tap.features.wallet.redux import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Wallet import com.tangem.commands.common.network.TangemService -import com.tangem.common.extensions.isZero import com.tangem.common.extensions.toHexString import com.tangem.tap.common.extensions.toFiatString import com.tangem.tap.common.extensions.toFormattedCurrencyString -import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.common.redux.global.FiatCurrencyName import com.tangem.tap.domain.TapError import com.tangem.tap.domain.getFirstToken import com.tangem.tap.domain.twins.TwinCardNumber -import com.tangem.tap.features.wallet.models.removeUnknownTransactions -import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.reducers.MultiWalletReducer +import com.tangem.tap.features.wallet.redux.reducers.OnWalletLoadedReducer +import com.tangem.tap.features.wallet.redux.reducers.TwinsReducer import com.tangem.tap.features.wallet.ui.BalanceStatus import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.TokenData -import com.tangem.tap.store import org.rekotlin.Action +import java.math.BigDecimal class WalletReducer { companion object { @@ -29,11 +30,19 @@ class WalletReducer { private fun internalReduce(action: Action, state: AppState): WalletState { + val multiWalletReducer = MultiWalletReducer() + val twinsReducer = TwinsReducer() + val onWalletLoadedReducer = OnWalletLoadedReducer() + if (action !is WalletAction) return state.walletState var newState = state.walletState when (action) { + is WalletAction.CheckSignedHashes -> newState = handleCheckSignedHashesActions(action, newState) + is WalletAction.TwinsAction -> newState = twinsReducer.reduce(action, newState) + is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState) + is WalletAction.ResetState -> newState = WalletState() is WalletAction.EmptyWallet -> { val creatingWalletAllowed = !(newState.twinCardsState != null && @@ -41,121 +50,149 @@ private fun internalReduce(action: Action, state: AppState): WalletState { newState = newState.copy( state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), - mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), - topUpState = TopUpState(false) - ) + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), + mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed), + topUpState = TopUpState(false) + ))) } is WalletAction.LoadData.Failure -> { when (action.error) { is TapError.NoInternetConnection -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet newState = newState.copy( state = ProgressState.Error, error = ErrorType.NoInternetConnection, - walletAddresses = createAddressList(wallet, newState.walletAddresses), - currencyData = BalanceWidgetData( - status = BalanceStatus.Unreachable, - currency = wallet?.blockchain?.fullName, - token = wallet?.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - }), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(false) ) } is TapError.UnknownBlockchain -> { newState = newState.copy( state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), - topUpState = TopUpState(false) + wallets = listOf(WalletData( + currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), + topUpState = TopUpState(false) + )) ) } } + } + is WalletAction.LoadData -> { + newState = newState.copy( + state = ProgressState.Loading, + error = null, + ) } is WalletAction.LoadWallet -> { - val wallet = action.wallet + if (action.currency == null) { + val wallets = newState.wallets.map { wallet -> + wallet.copy( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = action.allowTopUp + ?: wallet.topUpState.allowed) + ) + } + newState = newState.copy( + state = ProgressState.Loading, + wallets = wallets + ) + } else { + val walletManager = newState.getWalletManager(action.currency) ?: return newState + val currencies = listOf(walletManager.wallet.blockchain.currency + walletManager.presetTokens.map { it.symbol }) + val newWallets = newState.wallets.filter { currencies.contains(it.currencyData.currencySymbol) } + .map { wallet -> + wallet.copy( + currencyData = wallet.currencyData.copy( + status = BalanceStatus.Loading, + currency = wallet.currencyData.currency, + currencySymbol = wallet.currencyData.currencySymbol, + ), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState( + allowed = action.allowTopUp ?: wallet.topUpState.allowed + ) + ) + } + val wallets = newState.replaceSomeWallets(newWallets) + newState = newState.copy(wallets = wallets) + } + + } + is WalletAction.LoadWallet.Success -> newState = onWalletLoadedReducer.reduce(action.wallet, newState) + is WalletAction.UpdateWallet.Success -> { + newState = onWalletLoadedReducer.reduce(action.wallet, newState) +// newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty()) + } + + is WalletAction.LoadWallet.NoAccount -> { + val walletData = newState.getWalletData(action.wallet.blockchain.currency)?.copy( + currencyData = BalanceWidgetData( + BalanceStatus.NoAccount, action.wallet.blockchain.fullName, + currencySymbol = action.wallet.blockchain.currency, + amountToCreateAccount = action.amountToCreateAccount + ) + ) + val wallets = newState.replaceWalletInWallets(walletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + newState = newState.copy( + state = state, + wallets = wallets + + ) + } + is WalletAction.LoadWallet.Failure -> { + val walletData = newState.getWalletData(action.wallet.blockchain.currency) + val newWalletData = walletData?.copy( + currencyData = walletData.currencyData.copy( + status = BalanceStatus.Unreachable, + errorMessage = action.errorMessage + ), + topUpState = TopUpState(false) + ) + val wallets = newState.replaceWalletInWallets(newWalletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + newState = newState.copy( + state = state, wallets = wallets + ) + } + is WalletAction.SetArtworkId -> { val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { newState.cardImage } else { null } - newState = newState.copy( - state = ProgressState.Loading, - cardImage = cardImage, - currencyData = BalanceWidgetData( - BalanceStatus.Loading, - wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - token = wallet.getFirstToken()?.symbol?.let { - TokenData("", tokenSymbol = it) - } - ), - walletAddresses = createAddressList(wallet, newState.walletAddresses), - mainButton = WalletMainButton.SendButton(false), - topUpState = TopUpState(allowed = action.allowTopUp) - ) + newState = newState.copy(cardImage = cardImage) } - is WalletAction.LoadWallet.Success -> newState = onWalletLoaded(action.wallet, newState) - is WalletAction.LoadWallet.NoAccount -> { - val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet - newState = newState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - BalanceStatus.NoAccount, wallet?.blockchain?.fullName, - currencySymbol = wallet?.blockchain?.currency, - amountToCreateAccount = action.amountToCreateAccount - ) - ) - } - is WalletAction.LoadWallet.Failure -> newState = newState.copy( - state = ProgressState.Done, - currencyData = newState.currencyData.copy( - status = BalanceStatus.Unreachable, - errorMessage = action.errorMessage - ), - topUpState = TopUpState(false) - ) - is WalletAction.UpdateWallet -> { - if (store.state.walletState.state == ProgressState.Done) { - newState = newState.copy(updatingWallet = true) - } - } - is WalletAction.UpdateWallet.ScheduleUpdatingWallet -> - newState = newState.copy(updatingWallet = true) - is WalletAction.UpdateWallet.Success -> { - newState = onWalletLoaded(action.wallet, newState) - newState = newState.copy(updatingWallet = newState.pendingTransactions.isNotEmpty()) - } - is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false) - is WalletAction.LoadFiatRate -> { - newState.copy(currencyData = newState.currencyData.copy( - fiatAmount = null, - token = newState.currencyData.token?.copy(fiatAmount = null)) - ) - } - is WalletAction.LoadFiatRate.Success -> { - val rate = action.fiatRates.second ?: return newState - val currency = action.fiatRates.first - val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) { - newState.wallet?.amounts?.get(AmountType.Coin)?.value - ?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.fiatAmount - } - val token = newState.wallet?.getFirstToken() - val tokenFiatAmount = if (currency == token?.symbol) { - newState.wallet?.getTokenAmount(token)?.value?.toFiatString(rate, state.globalState.appCurrency) - } else { - newState.currencyData.token?.fiatAmount - } - newState = newState.copy(currencyData = newState.currencyData.copy( - fiatAmount = fiatAmount, - token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount) - )) - } +// is WalletAction.UpdateWallet -> { +// if (store.state.walletState.state == ProgressState.Done) { +// newState = newState.copy(updatingWallet = true) +// } +// } +// is WalletAction.UpdateWallet.ScheduleUpdatingWallet -> +// newState = newState.copy(updatingWallet = true) + +// is WalletAction.UpdateWallet.Failure -> newState = newState.copy(updatingWallet = false) +// is WalletAction.LoadFiatRate -> { +// newState.copy(currencyData = newState.currencyData.copy( +// fiatAmount = null, +// token = newState.currencyData.token?.copy(fiatAmount = null)) +// ) +// } + is WalletAction.LoadFiatRate.Success -> + newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState) is WalletAction.LoadArtwork -> { val cardId = action.card.cardId val cardPublicKey = action.card.cardPublicKey?.toHexString() @@ -177,38 +214,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState { newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl)) } is WalletAction.ShowQrCode -> { + val selectedWalletData = newState.getWalletData(newState.selectedWallet) newState = newState.copy( walletDialog = WalletDialog.QrDialog( - newState.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), - newState.walletAddresses?.selectedAddress?.shareUrl, - newState.currencyData.currency + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(), + selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl, + selectedWalletData?.currencyData?.currency ) ) } is WalletAction.HideDialog -> { newState = newState.copy(walletDialog = null) } - is WalletAction.LoadPayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId) - ) - is WalletAction.LoadPayId.NotCreated -> newState = newState.copy( - payIdData = PayIdData(PayIdState.NotCreated, null) - ) - is WalletAction.DisablePayId -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Disabled, null) - ) - is WalletAction.CreatePayId, is WalletAction.CreatePayId.Failure -> - newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.EnterPayId) - ) - is WalletAction.CreatePayId.CompleteCreatingPayId -> newState = newState.copy( - walletDialog = WalletDialog.CreatePayIdDialog(CreatingPayIdState.Waiting) - ) - is WalletAction.CreatePayId.Success -> newState = newState.copy( - payIdData = PayIdData(PayIdState.Created, action.payId), - walletDialog = null - ) - is WalletAction.CreatePayId.Cancel -> newState = newState.copy(walletDialog = null) is WalletAction.Send.ChooseCurrency -> { newState = newState.copy( walletDialog = WalletDialog.SelectAmountToSendDialog(action.amounts) @@ -216,43 +233,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState { } is WalletAction.Send.Cancel -> newState = newState.copy(walletDialog = null) is WalletAction.SetWarnings -> newState = newState.copy(mainWarningsList = action.warningList) - is WalletAction.NeedToCheckHashesCountOnline -> - newState = newState.copy(hashesCountVerified = false) - is WalletAction.ConfirmHashesCount -> - newState = newState.copy(hashesCountVerified = true) - is WalletAction.TopUpAction -> { - newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState)) - } + is WalletAction.TopUpAction -> return newState is WalletAction.ChangeSelectedAddress -> { - val walletAddresses = newState.walletAddresses ?: return newState + val selectedWalletData = newState.getWalletData(newState.selectedWallet) + + val walletAddresses = newState.getWalletData(selectedWalletData?.currencyData?.currencySymbol)?.walletAddresses + ?: return newState val address = walletAddresses.list.firstOrNull { it.type == action.type } ?: return newState - - newState = newState.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) - } - is WalletAction.TwinsAction.SetTwinCard -> { - newState = newState.copy( - twinCardsState = TwinCardsState( - secondCardId = action.secondCardId, - cardNumber = action.number, - showTwinOnboarding = newState.twinCardsState?.showTwinOnboarding - ?: false, - isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed - ) - ) - } - is WalletAction.TwinsAction.ShowOnboarding -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = true) - ?: TwinCardsState(null, null, - showTwinOnboarding = true, - isCreatingTwinCardsAllowed = false) - ) - } - is WalletAction.TwinsAction.SetOnboardingShown -> { - newState = newState.copy( - twinCardsState = newState.twinCardsState?.copy(showTwinOnboarding = false) + val wallets = newState.replaceWalletInWallets( + selectedWalletData?.copy(walletAddresses = WalletAddresses(address, walletAddresses.list)) ) + newState = newState.copy(wallets = wallets) } } return newState @@ -281,56 +273,89 @@ fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null) return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData) } -private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState { +private fun handleCheckSignedHashesActions(action: WalletAction.CheckSignedHashes, state: WalletState): WalletState { return when (action) { - is WalletAction.TopUpAction.TopUp -> state + WalletAction.CheckSignedHashes.CheckHashesCountOnline -> state + WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> state + WalletAction.CheckSignedHashes.ConfirmHashesCount -> state.copy(hashesCountVerified = true) + WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline -> state.copy(hashesCountVerified = false) + WalletAction.CheckSignedHashes.SaveCardId -> state } } -private fun onWalletLoaded( - wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null + +private fun setNewFiatRate( + fiatRate: Pair, + appCurrency: FiatCurrencyName, state: WalletState ): WalletState { - val fiatCurrencySymbol = store.state.globalState.appCurrency + val rate = fiatRate.second ?: return state + val rateFormatted = rate.toFormattedCurrencyString(2, appCurrency) + val currency = fiatRate.first + + if (!state.isMultiwalletAllowed) { + return setSingeWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } else { + return setMultiWalletFiatRate(rate, rateFormatted, currency, appCurrency, state) + } +} + +private fun setMultiWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val walletData = state.getWalletData(currency) ?: return state + val wallet = state.walletManagers.find { it.wallet.blockchain.currency == currency }?.wallet + if (wallet != null) { + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency)), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } else { + val ethereumWallet = state.walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum }?.wallet + val token = ethereumWallet?.getTokens()?.find { it.symbol == currency } ?: return state + val tokenFiatAmount = ethereumWallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val newWalletData = state.getWalletData(currency)?.copy( + currencyData = walletData.currencyData.copy( + fiatAmount = tokenFiatAmount), + fiatRate = rate, fiatRateString = rateFormatted + ) + return state.copy(wallets = state.replaceWalletInWallets(newWalletData)) + } +} + +private fun setSingeWalletFiatRate( + rate: BigDecimal, rateFormatted: String, currency: CryptoCurrencyName, + appCurrency: FiatCurrencyName, state: WalletState +): WalletState { + val wallet = state.walletManagers[0].wallet val token = wallet.getFirstToken() - val tokenData = if (token != null) { - val tokenAmount = wallet.getTokenAmount(token) - if (tokenAmount != null) { - val tokenFiatRate = store.state.globalState.conversionRates.getRate(tokenAmount.currencySymbol) - val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } - TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", - tokenAmount.currencySymbol, tokenFiatAmount) - } else { - null - } - } else { - null - } - val amount = wallet.amounts[AmountType.Coin]?.value - val formattedAmount = amount?.toFormattedCurrencyString( - wallet.blockchain.decimals(), - wallet.blockchain.currency) - val fiatRate = store.state.globalState.conversionRates.getRate(wallet.blockchain.currency) - val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } - val pendingTransactions = wallet.recentTransactions - .toPendingTransactions(wallet.address) + if (currency == state.primaryWallet?.currencyData?.currencySymbol) { + val fiatAmount = wallet.amounts[AmountType.Coin]?.value + ?.toFiatString(rate, appCurrency) + val walletData = state.primaryWallet.copy( + currencyData = state.primaryWallet.currencyData.copy(fiatAmount = fiatAmount), + fiatRate = rate, + fiatRateString = rateFormatted + ) + return state.copy(wallets = listOf(walletData)) + } else if (currency == token?.symbol) { + val tokenFiatAmount = wallet.getTokenAmount(token)?.value?.toFiatString(rate, appCurrency) + val tokenData = state.primaryWallet?.currencyData?.token?.copy( + fiatAmount = tokenFiatAmount, + fiatRate = rate, + fiatRateString = rateFormatted + ) - val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() - val balanceStatus = if (pendingTransactions.isNotEmpty()) { - BalanceStatus.TransactionInProgress - } else { - BalanceStatus.VerifiedOnline + val walletData = state.primaryWallet?.copy( + currencyData = state.primaryWallet.currencyData.copy( + token = tokenData + )) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return state.copy(wallets = wallets) } - return walletState.copy( - state = ProgressState.Done, wallet = wallet, - currencyData = BalanceWidgetData( - balanceStatus, wallet.blockchain.fullName, - currencySymbol = wallet.blockchain.currency, - formattedAmount, - token = tokenData, - fiatAmount = fiatAmount - ), - pendingTransactions = pendingTransactions.removeUnknownTransactions(), - mainButton = WalletMainButton.SendButton(sendButtonEnabled) - ) + return state } \ No newline at end of file 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 1b541930c7..2958717c4a 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 @@ -1,47 +1,98 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Wallet +import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.extensions.isAboveZero import com.tangem.tap.common.entities.Button import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.extensions.toSendableAmounts import com.tangem.tap.domain.twins.TwinCardNumber import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.store import org.rekotlin.StateType +import java.math.BigDecimal data class WalletState( val state: ProgressState = ProgressState.Done, val error: ErrorType? = null, val cardImage: Artwork? = null, - val wallet: Wallet? = null, - val pendingTransactions: List = emptyList(), val hashesCountVerified: Boolean? = null, - val walletAddresses: WalletAddresses? = null, - val currencyData: BalanceWidgetData = BalanceWidgetData(), - val payIdData: PayIdData = PayIdData(), val walletDialog: WalletDialog? = null, - val updatingWallet: Boolean = false, - val mainButton: WalletMainButton = WalletMainButton.SendButton(false), - val topUpState: TopUpState = TopUpState(), val twinCardsState: TwinCardsState? = null, - val mainWarningsList: List = mutableListOf() + val mainWarningsList: List = mutableListOf(), + val wallets: List = emptyList(), + val walletManagers: List = emptyList(), + val isMultiwalletAllowed: Boolean = false, + val cardCurrency: CryptoCurrencyName? = null, + val selectedWallet: CryptoCurrencyName? = null, + val primaryBlockchain: Blockchain? = null, + val primaryToken: Token? = null ) : StateType { - val showDetails: Boolean = - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard && - currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain - val showMultipleAddress: Boolean - get() { - val listOfAddresses = walletAddresses?.list ?: return false - return (wallet?.blockchain == Blockchain.Bitcoin || - wallet?.blockchain == Blockchain.BitcoinTestnet || - wallet?.blockchain == Blockchain.CardanoShelley) && - listOfAddresses.size > 1 + val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null +// val primaryWalletManager = if (walletManagers.isNotEmpty()) walletManagers[0] else null + + val shouldShowDetails: Boolean = + primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard && + primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain + + fun getWalletManager(currencyName: CryptoCurrencyName?): WalletManager? { + if (currencyName == null) return null + val walletManager = walletManagers.find { it.wallet.blockchain.currency == currencyName } + if (walletManager != null) return walletManager + + val ethereumWalletManager = walletManagers.find { it.wallet.blockchain == Blockchain.Ethereum } + return if (ethereumWalletManager?.presetTokens?.find { it.symbol == currencyName } != null) { + ethereumWalletManager + } else { + null } + } + + fun getWalletData(currencyName: CryptoCurrencyName?): WalletData? { + if (currencyName == null) return null + return wallets.find { it.currencyData.currencySymbol == currencyName } + } + + fun getSelectedWalletData(): WalletData? { + return wallets.find { it.currencyData.currencySymbol == selectedWallet } + } + + fun canBeRemoved(walletData: WalletData?): Boolean { + if (walletData == null) return false + + if (walletData.blockchain != store.state.walletState.primaryBlockchain + && (walletData.token == null || walletData.token != store.state.walletState.primaryToken)) { + val walletManager = getWalletManager(walletData.currencyData.currencySymbol) + ?: return true + val wallet = walletManager.wallet + if (walletData.blockchain != null) { + return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() && + wallet.amounts.toSendableAmounts().isEmpty() + } else if (walletData.token != null) ( + return wallet.recentTransactions.toPendingTransactionsForToken( + walletData.token, wallet.address).isEmpty() + && wallet.amounts[AmountType.Token(token = walletData.token)]?.isAboveZero() != true + ) + } + return false + } + + fun replaceWalletInWallets(walletData: WalletData?): List { + if (walletData == null) return wallets + return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData + } + + fun replaceSomeWallets(newWallets: List): List { + val currencies = newWallets.map { it.currencyData.currency } + return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets + } + } sealed class WalletDialog { @@ -49,24 +100,13 @@ sealed class WalletDialog { val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName? ) : WalletDialog() - data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog() data class SelectAmountToSendDialog(val amounts: List?) : WalletDialog() - data class TwinsOnboardingFragment(val secondCardId: String): WalletDialog() } enum class ProgressState { Loading, Done, Error } enum class ErrorType { NoInternetConnection } -enum class PayIdState { Disabled, Loading, NotCreated, Created, ErrorLoading } - -data class PayIdData( - val payIdState: PayIdState = PayIdState.Loading, - val payId: String? = null -) - -enum class CreatingPayIdState { EnterPayId, Waiting } - sealed class WalletMainButton(enabled: Boolean) : Button(enabled) { class SendButton(enabled: Boolean) : WalletMainButton(enabled) class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled) @@ -81,7 +121,8 @@ data class AddressData( val address: String, val type: AddressType, val shareUrl: String, - val exploreUrl: String + val exploreUrl: String, + val qrCode: Bitmap? = null ) data class Artwork( @@ -110,4 +151,27 @@ data class TwinCardsState( val cardNumber: TwinCardNumber?, val showTwinOnboarding: Boolean, val isCreatingTwinCardsAllowed: Boolean -) \ No newline at end of file +) + +data class WalletData( + val pendingTransactions: List = emptyList(), + val hashesCountVerified: Boolean? = null, + val walletAddresses: WalletAddresses? = null, + val currencyData: BalanceWidgetData = BalanceWidgetData(), + val updatingWallet: Boolean = false, + val topUpState: TopUpState = TopUpState(), + val allowToSend: Boolean = true, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, + val mainButton: WalletMainButton = WalletMainButton.SendButton(false), + val token: Token? = null, + val blockchain: Blockchain? = null, +) { + fun shouldShowMultipleAddress(): Boolean { + val listOfAddresses = walletAddresses?.list ?: return false + return (currencyData.currencySymbol == Blockchain.Bitcoin.currency || + currencyData.currencySymbol == Blockchain.BitcoinTestnet.currency || + currencyData.currencySymbol == Blockchain.CardanoShelley.currency) && + listOfAddresses.size > 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt new file mode 100644 index 0000000000..9ae002d4ad --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/CheckSignedHashesMiddleware.kt @@ -0,0 +1,103 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.SignatureCountValidator +import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.commands.common.card.Card +import com.tangem.commands.common.card.CardType +import com.tangem.common.extensions.getType +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage +import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager +import com.tangem.tap.domain.tasks.isMultiwalletAllowed +import com.tangem.tap.domain.twins.isTwinCard +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.network.NetworkConnectivity +import com.tangem.tap.preferencesStorage +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CheckSignedHashesMiddleware { + fun handle(action: WalletAction.CheckSignedHashes, globalState: GlobalState?) { + when (action) { + is WalletAction.CheckSignedHashes.CheckIfWarningNeeded -> { + val validator = globalState?.scanNoteResponse?.walletManager as? SignatureCountValidator + globalState?.scanNoteResponse?.card?.let { card -> + globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local) + if (card.getType() != CardType.Release) addWarningMessage(WarningMessagesManager.devCardWarning()) + if (!preferencesStorage.wasCardScannedBefore(card.cardId)) { + checkIfWarningNeeded(card, validator)?.let { addWarningMessage(it) } + } + updateWarningMessages() + } + + } + is WalletAction.CheckSignedHashes.CheckHashesCountOnline -> checkHashesCountOnline() + is WalletAction.CheckSignedHashes.SaveCardId -> { + val cardId = globalState?.scanNoteResponse?.card?.cardId + cardId?.let { preferencesStorage.saveScannedCardId(it) } + } + } + } + + private fun checkIfWarningNeeded( + card: Card, signatureCountValidator: SignatureCountValidator? = null, + ): WarningMessage? { + if (card.isTwinCard() || card.isMultiwalletAllowed) return null + + return if (signatureCountValidator == null) { + if (card.walletSignedHashes ?: 0 > 0) { + WarningMessagesManager.alreadySignedHashesWarning() + } else { + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) + null + } + } else { + store.dispatch(WalletAction.CheckSignedHashes.NeedToCheckHashesCountOnline) + null + } + } + + private fun checkHashesCountOnline() { + if (store.state.walletState.hashesCountVerified != false) return + if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return + + val card = store.state.globalState.scanNoteResponse?.card + if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) return + + if (card.isTwinCard() || card.isMultiwalletAllowed) return + + val validator = store.state.globalState.scanNoteResponse?.walletManager + as? SignatureCountValidator + scope.launch { + val result = validator?.validateSignatureCount(card.walletSignedHashes ?: 0) + withContext(Dispatchers.Main) { + when (result) { + SimpleResult.Success -> { + store.dispatch(WalletAction.CheckSignedHashes.ConfirmHashesCount) + store.dispatch(WalletAction.CheckSignedHashes.SaveCardId) + } + is SimpleResult.Failure -> + if (result.error is BlockchainSdkError.SignatureCountNotMatched) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } else if (card.walletSignedHashes ?: 0 > 0) { + addWarningMessage(WarningMessagesManager.alreadySignedHashesWarning(), true) + } + } + } + } + } + + private fun addWarningMessage(warning: WarningMessage, autoUpdate: Boolean = false) { + store.state.globalState.warningManager?.addWarning(warning) + if (autoUpdate) updateWarningMessages() + } + + private fun updateWarningMessages() { + val warningManager = store.state.globalState.warningManager ?: return + store.dispatch(WalletAction.SetWarnings(warningManager.getWarnings(WarningMessage.Location.MainScreen))) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..bf1cceeed7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -0,0 +1,72 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.TokenManager +import com.tangem.blockchain.extensions.Result +import com.tangem.tap.common.redux.global.GlobalState +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.currenciesRepository +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.scope +import com.tangem.tap.store +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class MultiWalletMiddleware { + fun handle( + action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState? + ) { + when (action) { + is WalletAction.MultiWallet.SelectWallet -> { + if (action.walletData != null) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletDetails)) + } + } + is WalletAction.MultiWallet.AddToken -> { + globalState?.scanNoteResponse?.card?.cardId?.let { + currenciesRepository.saveAddedToken(it, action.token) + } + addToken(action.token, walletState) + } + is WalletAction.MultiWallet.AddTokens -> { + action.tokens.map { addToken(it, walletState) } + } + is WalletAction.MultiWallet.AddBlockchain -> { + globalState?.scanNoteResponse?.card?.cardId?.let { + currenciesRepository.saveAddedBlockchain(it, action.blockchain) + } + store.dispatch(WalletAction.LoadFiatRate(currency = action.blockchain.currency)) + } + is WalletAction.MultiWallet.RemoveWallet -> { + val cardId = globalState?.scanNoteResponse?.card?.cardId + if (action.walletData.token != null) { + cardId?.let { currenciesRepository.removeToken(it, action.walletData.token) } + } else if (action.walletData.blockchain != null) { + cardId?.let { currenciesRepository.removeBlockchain(it, action.walletData.blockchain) } + } + } + } + } + + private fun addToken(token: Token, walletState: WalletState?) { + val tokenManager = + (walletState?.getWalletManager(Blockchain.Ethereum.currency) as? TokenManager) + scope.launch { + val result = tokenManager?.addToken(token) + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> { + store.dispatch(WalletAction.LoadFiatRate(currency = token.symbol)) + store.dispatch(WalletAction.MultiWallet.TokenLoaded(result.data)) + } + is Result.Failure -> null + null -> null + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt new file mode 100644 index 0000000000..c155519eae --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TopUpMiddleware.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.net.Uri +import androidx.browser.customtabs.CustomTabsIntent +import com.tangem.tap.domain.TopUpHelper +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.store + +class TopUpMiddleware { + fun handle(action: WalletAction.TopUpAction) { + when (action) { + is WalletAction.TopUpAction.TopUp -> { + val selectedWalletData = store.state.walletState.getSelectedWalletData() + val config = store.state.globalState.configManager?.config ?: return + val defaultAddress = selectedWalletData?.walletAddresses!!.list[0].address + val url = TopUpHelper.getUrl( + selectedWalletData.currencyData.currencySymbol!!, + defaultAddress, + config.moonPayApiKey, + config.moonPayApiSecretKey + ) + val customTabsIntent = CustomTabsIntent.Builder() + .setToolbarColor(action.toolbarColor) + .build() + customTabsIntent.launchUrl(action.context, Uri.parse(url)); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt new file mode 100644 index 0000000000..88ba60d1d1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TwinsMiddleware.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.preferencesStorage +import com.tangem.tap.store + +class TwinsMiddleware { + fun handle(action: WalletAction.TwinsAction) { + when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() + if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding) + } + WalletAction.TwinsAction.SetOnboardingShown -> { + preferencesStorage.saveTwinsOnboardingShown() + } + } + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..a82debea08 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt @@ -0,0 +1,230 @@ +package com.tangem.tap.features.wallet.redux.middlewares + +import android.content.Intent +import android.net.Uri +import androidx.core.content.ContextCompat +import com.tangem.blockchain.common.* +import com.tangem.common.CompletionResult +import com.tangem.tap.* +import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler +import com.tangem.tap.common.extensions.copyToClipboard +import com.tangem.tap.common.extensions.shareText +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.extensions.toSendableAmounts +import com.tangem.tap.domain.twins.TwinsHelper +import com.tangem.tap.features.details.redux.DetailsAction +import com.tangem.tap.features.details.redux.twins.CreateTwinWallet +import com.tangem.tap.features.send.redux.PrepareSendScreen +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.network.NetworkStateChanged +import kotlinx.coroutines.launch +import org.rekotlin.Action +import org.rekotlin.Middleware + +class WalletMiddleware { + private val topUpMiddleware = TopUpMiddleware() + private val twinsMiddleware = TwinsMiddleware() + private val checkSignedHashesMiddleware = CheckSignedHashesMiddleware() + private val multiWalletMiddleware = MultiWalletMiddleware() + + val walletMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + val globalState = state()?.globalState + val walletState = state()?.walletState + when (action) { + is WalletAction.TopUpAction -> topUpMiddleware.handle(action) + is WalletAction.TwinsAction -> twinsMiddleware.handle(action) + is WalletAction.CheckSignedHashes -> + checkSignedHashesMiddleware.handle(action, globalState) + is WalletAction.MultiWallet -> + multiWalletMiddleware.handle(action, walletState, globalState) + + is WalletAction.LoadWallet -> { + scope.launch { + if (action.currency == null) { + walletState?.walletManagers?.map { walletManager -> + globalState?.tapWalletManager?.loadWalletData(walletManager) + } + } else { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.loadWalletData(it) } + } + } + } + is WalletAction.LoadFiatRate -> { + scope.launch { + if (action.wallet != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.wallet + ) + } else if (action.currency != null) { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, action.currency + ) + } else { + walletState?.wallets?.filter { it.blockchain != null } + ?.map { walletState.getWalletManager(it.blockchain?.currency) } + ?.mapNotNull { walletManager -> + walletManager?.let { + globalState?.tapWalletManager?.loadFiatRate( + globalState.appCurrency, walletManager.wallet + ) + } + } + } + } + } + is WalletAction.CreateWallet -> { + if (walletState?.twinCardsState != null) { + store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning( + globalState?.scanNoteResponse?.card?.cardId?.let { + TwinsHelper.getTwinCardNumber(it) + }, + CreateTwinWallet.CreateWallet + )) + } else { + scope.launch { + val result = tangemSdkManager.createWallet( + globalState?.scanNoteResponse?.card?.cardId + ) + when (result) { + is CompletionResult.Success -> { + globalState?.tapWalletManager?.onCardScanned(result.data) + } + } + } + } + } + is WalletAction.UpdateWallet -> { + if (action.currency != null) { + scope.launch { + val walletManager = walletState?.getWalletManager(action.currency) + walletManager?.let { globalState?.tapWalletManager?.updateWallet(it) } + } + } else { + scope.launch { + if (walletState?.state == ProgressState.Done) { + walletState.walletManagers.map { walletManager -> + globalState?.tapWalletManager?.updateWallet(walletManager) + } + } + } + } + + } + is WalletAction.LoadWallet.Success -> { + store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline) + } + is WalletAction.Scan -> { + scope.launch { + val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) + when (result) { + is CompletionResult.Success -> { + tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card) + globalState?.tapWalletManager + ?.onCardScanned(result.data, true) + if (walletState?.twinCardsState != null) { + val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown() + if (showOnboarding) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding)) + } + } + } + } + } + } + is WalletAction.LoadData -> { + scope.launch { + val scanNoteResponse = globalState?.scanNoteResponse ?: return@launch + if (!walletState?.wallets.isNullOrEmpty()) { + globalState.tapWalletManager.reloadData(scanNoteResponse) + } else { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is NetworkStateChanged -> { + globalState?.scanNoteResponse?.let { scanNoteResponse -> + store.dispatch(WalletAction.CheckSignedHashes.CheckHashesCountOnline) + scope.launch { + globalState.tapWalletManager.loadData(scanNoteResponse) + } + } + } + is WalletAction.CopyAddress -> { + action.context.copyToClipboard(action.address) + store.dispatch(WalletAction.CopyAddress.Success) + } + is WalletAction.ShareAddress -> { + action.context.shareText(action.address) + } + is WalletAction.ExploreAddress -> { + val uri = Uri.parse(action.exploreUrl) + val intent = Intent(Intent.ACTION_VIEW, uri) + ContextCompat.startActivity(action.context, intent, null) + } + is WalletAction.Send -> { + val newAction = prepareSendAction(action.amount, state()?.walletState) + store.dispatch(newAction) + if (newAction is PrepareSendScreen) { + store.dispatch(NavigationAction.NavigateTo(AppScreen.Send)) + } + } + } + next(action) + } + } + } + + private fun prepareSendAction(amount: Amount?, state: WalletState?): Action { + val selectedWalletData = state?.getSelectedWalletData() + val currency = selectedWalletData?.currencyData?.currencySymbol + val wallet = currency?.let { state.getWalletManager(currency)?.wallet } + + return if (amount != null) { + if (amount.type is AmountType.Token) { + prepareSendActionForToken(amount, state, selectedWalletData, wallet) + } else { + PrepareSendScreen(amount, selectedWalletData?.fiatRate) + } + } else { + val amounts = wallet?.amounts?.toSendableAmounts() + if (currency != null && state.isMultiwalletAllowed) { + val amountToSend = amounts?.find { it.currencySymbol == currency } + ?: return WalletAction.Send.ChooseCurrency(amounts) + if (amountToSend.type is AmountType.Token) { + prepareSendActionForToken(amount, state, selectedWalletData, wallet) + } else { + PrepareSendScreen(amountToSend, selectedWalletData.fiatRate) + } + } else { + if (amounts?.size ?: 0 > 1) { + WalletAction.Send.ChooseCurrency(amounts) + } else { + val amountToSend = amounts?.first() + PrepareSendScreen(amountToSend, selectedWalletData?.fiatRate) + } + } + } + } + + private fun prepareSendActionForToken( + amount: Amount?, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet? + ): PrepareSendScreen { + val coinRate = state?.getWalletData(wallet?.blockchain?.currency)?.fiatRate + val tokenRate = if (state?.isMultiwalletAllowed == true) { + selectedWalletData?.fiatRate + } else { + selectedWalletData?.currencyData?.token?.fiatRate + } + return PrepareSendScreen( + wallet?.amounts?.get(AmountType.Coin), coinRate, + amount, tokenRate) + } +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt new file mode 100644 index 0000000000..b88adcb40d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -0,0 +1,160 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store + +class MultiWalletReducer { + fun reduce(action: WalletAction.MultiWallet, state: WalletState): WalletState { + return when (action) { + is WalletAction.MultiWallet.AddWalletManagers -> { + state.copy( + walletManagers = action.walletManagers + ) + } + is WalletAction.MultiWallet.AddBlockchains -> { + val wallets = action.blockchains.map { blockchain -> + val wallet = state.getWalletManager(blockchain.currency)?.wallet + val cardToken = if (!state.isMultiwalletAllowed) { + wallet?.getFirstToken()?.symbol?.let { TokenData("", tokenSymbol = it) } + } else { + null + } + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + blockchain.fullName, + currencySymbol = blockchain.currency, + token = cardToken + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = blockchain + ) + } + + val selectedWallet = if (!state.isMultiwalletAllowed) { + wallets[0].currencyData.currencySymbol + } else { + state.selectedWallet + } + state.copy( + wallets = wallets, + selectedWallet = selectedWallet + ) + } + is WalletAction.MultiWallet.AddBlockchain -> { + val wallet = state.getWalletManager(action.blockchain.currency)?.wallet + val walletData = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + action.blockchain.fullName, + currencySymbol = action.blockchain.currency, + ), + walletAddresses = createAddressList(wallet), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + blockchain = action.blockchain + ) + val newState = state.copy(wallets = state.replaceWalletInWallets(walletData)) + if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) { + OnWalletLoadedReducer().reduce(wallet, newState) + } else { + newState + } + } + is WalletAction.MultiWallet.AddTokens -> { + if (!state.isMultiwalletAllowed) return state + val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet) + val wallets = action.tokens.map { token -> + WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = token.name, + currencySymbol = token.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = token + ) + } + state.copy(wallets = state.wallets + wallets) + } + is WalletAction.MultiWallet.AddToken -> { + if (!state.isMultiwalletAllowed) return state + val walletAddresses = createAddressList(state.getWalletManager(Blockchain.Ethereum.currency)?.wallet) + val wallet = WalletData( + currencyData = BalanceWidgetData( + BalanceStatus.Loading, + currency = action.token.name, + currencySymbol = action.token.symbol + ), + walletAddresses = walletAddresses, + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = false), + token = action.token + ) + val wallets = state.replaceWalletInWallets(wallet) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.TokenLoaded -> { + val pendingTransactions = state.getWalletManager(Blockchain.Ethereum.currency) + ?.wallet?.let { wallet -> + wallet.recentTransactions.toPendingTransactions(wallet.address) + } ?: emptyList() + + val sendButtonEnabled = action.amount.value?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val tokenWalletData = state.getWalletData(action.amount.currencySymbol) + val newTokenWalletData = tokenWalletData?.copy( + currencyData = tokenWalletData.currencyData.copy( + status = balanceStatus, + amount = action.amount.value?.toFormattedCurrencyString( + action.amount.decimals, action.amount.currencySymbol + ), + fiatAmount = tokenWalletData.fiatRate?.let { + action.amount.value + ?.toFiatString(it, store.state.globalState.appCurrency) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = state.replaceWalletInWallets(newTokenWalletData) + state.copy(wallets = wallets) + } + is WalletAction.MultiWallet.SetIsMultiwalletAllowed -> + state.copy(isMultiwalletAllowed = action.isMultiwalletAllowed) + + is WalletAction.MultiWallet.SelectWallet -> + state.copy(selectedWallet = action.walletData?.currencyData?.currencySymbol) + + is WalletAction.MultiWallet.RemoveWallet -> { + state.copy(wallets = state.wallets.filterNot { + it.currencyData.currencySymbol == action.walletData.currencyData.currencySymbol + }) + } + is WalletAction.MultiWallet.SetPrimaryBlockchain -> + state.copy(primaryBlockchain = action.blockchain) + + is WalletAction.MultiWallet.SetPrimaryToken -> + state.copy(primaryToken = action.token) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt new file mode 100644 index 0000000000..59edbdd896 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -0,0 +1,182 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Wallet +import com.tangem.common.extensions.isZero +import com.tangem.tap.common.extensions.toFiatString +import com.tangem.tap.common.extensions.toFormattedCurrencyString +import com.tangem.tap.common.extensions.toFormattedString +import com.tangem.tap.domain.getFirstToken +import com.tangem.tap.features.wallet.models.removeUnknownTransactions +import com.tangem.tap.features.wallet.models.toPendingTransactions +import com.tangem.tap.features.wallet.redux.ProgressState +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletMainButton +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.BalanceWidgetData +import com.tangem.tap.features.wallet.ui.TokenData +import com.tangem.tap.store +import java.math.RoundingMode + +class OnWalletLoadedReducer { + fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState { + if (!walletState.isMultiwalletAllowed) { + return onSingleWalletLoaded(wallet, walletState, topUpAllowed) + } else { + val fiatCurrencySymbol = store.state.globalState.appCurrency + val amount = wallet.amounts[AmountType.Coin]?.value + if (walletState.getWalletData(wallet.blockchain.currency) == null && amount?.isZero() != false) { + return walletState + } + if (wallet.blockchain != Blockchain.Ethereum) { + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val walletData = walletState.getWalletData(wallet.blockchain.currency) + ?: WalletData() + val newWalletData = walletData.copy( + currencyData = walletData.currencyData.copy( + status = balanceStatus, currency = wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + amount = formattedAmount, + fiatAmount = walletData.fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = walletState.replaceWalletInWallets(newWalletData) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + return walletState.copy( + state = state, wallets = wallets + ) + } else { + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + + val ethereumWalletData = walletState.getWalletData(wallet.blockchain.currency) + ?: WalletData() + val newEthereumWalletData = ethereumWalletData.copy( + currencyData = ethereumWalletData.currencyData.copy( + status = balanceStatus, + amount = formattedAmount, + currency = wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + fiatAmount = ethereumWalletData.fiatRate?.let { + amount?.toFiatString(it, fiatCurrencySymbol) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + + val tokens = wallet.getTokens().mapNotNull { token -> + val tokenWalletData = walletState.getWalletData(token.symbol) + tokenWalletData?.copy( + currencyData = tokenWalletData.currencyData.copy( + status = balanceStatus, + amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString( + token.decimals, token.symbol + ), + fiatAmount = tokenWalletData.fiatRate?.let { + wallet.getTokenAmount(token)?.value + ?.toFiatString(it, fiatCurrencySymbol) + } + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + } + val wallets = walletState.replaceSomeWallets((tokens + newEthereumWalletData).filterNotNull()) + val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) { + ProgressState.Loading + } else { + ProgressState.Done + } + return walletState.copy( + state = state, wallets = wallets + ) + } + } + } + + private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null + ): WalletState { + if (wallet.blockchain != walletState.primaryBlockchain) return walletState + + val fiatCurrencySymbol = store.state.globalState.appCurrency + val token = wallet.getFirstToken() + val tokenData = if (token != null) { + val tokenAmount = wallet.getTokenAmount(token) + if (tokenAmount != null) { + val tokenFiatRate = walletState.primaryWallet?.currencyData?.token?.fiatRate + val tokenFiatAmount = tokenFiatRate?.let { tokenAmount.value?.toFiatString(it, fiatCurrencySymbol) } + TokenData(tokenAmount.value?.toFormattedString(tokenAmount.decimals) ?: "", + tokenAmount.currencySymbol, tokenFiatAmount) + } else { + null + } + } else { + null + } + val amount = wallet.amounts[AmountType.Coin]?.value + val formattedAmount = amount?.toFormattedCurrencyString( + wallet.blockchain.decimals(), + wallet.blockchain.currency) + val fiatRate = walletState.primaryWallet?.fiatRate + val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN) + val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) } + + val pendingTransactions = wallet.recentTransactions + .toPendingTransactions(wallet.address) + + val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty() + val balanceStatus = if (pendingTransactions.isNotEmpty()) { + BalanceStatus.TransactionInProgress + } else { + BalanceStatus.VerifiedOnline + } + val walletData = walletState.primaryWallet?.copy( + currencyData = BalanceWidgetData( + balanceStatus, wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + formattedAmount, + token = tokenData, + fiatAmount = fiatAmount, + fiatAmountRaw = fiatAmountRaw + ), + pendingTransactions = pendingTransactions.removeUnknownTransactions(), + mainButton = WalletMainButton.SendButton(sendButtonEnabled) + ) + val wallets = walletData?.let { listOf(walletData) } ?: emptyList() + return walletState.copy( + state = ProgressState.Done, wallets = wallets + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt new file mode 100644 index 0000000000..79025ca6cc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/TwinsReducer.kt @@ -0,0 +1,36 @@ +package com.tangem.tap.features.wallet.redux.reducers + +import com.tangem.tap.features.wallet.redux.TwinCardsState +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState + +class TwinsReducer { + fun reduce(action: WalletAction.TwinsAction, state: WalletState): WalletState { + return when (action) { + is WalletAction.TwinsAction.SetTwinCard -> { + state.copy( + twinCardsState = TwinCardsState( + secondCardId = action.secondCardId, + cardNumber = action.number, + showTwinOnboarding = state.twinCardsState?.showTwinOnboarding + ?: false, + isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed + ) + ) + } + is WalletAction.TwinsAction.ShowOnboarding -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true) + ?: TwinCardsState(null, null, + showTwinOnboarding = true, + isCreatingTwinCardsAllowed = false) + ) + } + is WalletAction.TwinsAction.SetOnboardingShown -> { + state.copy( + twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = false) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index ce7fec6959..ed446c3ee5 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -8,6 +8,7 @@ import com.tangem.wallet.R import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.layout_balance.* import kotlinx.android.synthetic.main.layout_balance_error.* +import java.math.BigDecimal enum class BalanceStatus { VerifiedOnline, @@ -25,6 +26,7 @@ data class BalanceWidgetData( val currencySymbol: String? = null, val amount: String? = null, val fiatAmount: String? = null, + val fiatAmountRaw: BigDecimal? = null, val token: TokenData? = null, val amountToCreateAccount: String? = null, val errorMessage: String? = null @@ -33,14 +35,16 @@ data class BalanceWidgetData( data class TokenData( val amount: String, val tokenSymbol: String, - val fiatAmount: String? = null + val fiatAmount: String? = null, + val fiatRateString: String? = null, + val fiatRate: BigDecimal? = null, ) class BalanceWidget( val fragment: Fragment, val data: BalanceWidgetData, - val isTwinCard: Boolean, + private val isTwinCard: Boolean, ) { fun setup() { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt new file mode 100644 index 0000000000..6570cee834 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/MultipleAddressUiHelper.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.features.wallet.ui + +import android.view.View +import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType +import com.tangem.blockchain.blockchains.cardano.CardanoAddressType +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.AddressType +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.wallet.R + +class MultipleAddressUiHelper { + companion object { + fun typeToId(type: AddressType): Int { + return when (type) { + is BitcoinAddressType.Legacy -> R.id.chip_legacy + is BitcoinAddressType.Segwit -> R.id.chip_default + is CardanoAddressType.Byron -> R.id.chip_legacy + is CardanoAddressType.Shelley -> R.id.chip_default + else -> View.NO_ID + } + } + + fun idToType(id: Int, currencyName: CryptoCurrencyName?): AddressType? { + val blockchain = currencyName?.let { currency -> + Blockchain.fromCurrency(currency) + } + return idToType(id, blockchain) + } + + fun idToType(id: Int, blockchain: Blockchain?): AddressType? { + return when (id) { + R.id.chip_default -> { + when (blockchain) { + Blockchain.Bitcoin -> BitcoinAddressType.Segwit + Blockchain.CardanoShelley -> CardanoAddressType.Shelley + else -> null + } + } + R.id.chip_legacy -> { + when (blockchain) { + Blockchain.Bitcoin -> BitcoinAddressType.Legacy + Blockchain.CardanoShelley -> CardanoAddressType.Byron + else -> null + } + } + else -> null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt new file mode 100644 index 0000000000..bbdc67d33f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -0,0 +1,289 @@ +package com.tangem.tap.features.wallet.ui + +import android.app.Dialog +import android.os.Bundle +import android.view.* +import androidx.activity.OnBackPressedCallback +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionInflater +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.redux.WalletDialog +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_details_twin_cards.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.fragment_wallet_details.* +import kotlinx.android.synthetic.main.fragment_wallet_details.rv_pending_transaction +import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar +import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import kotlinx.android.synthetic.main.layout_balance_error.* +import kotlinx.android.synthetic.main.layout_balance_wallet_details.* +import kotlinx.android.synthetic.main.layout_wallet_details.* +import org.rekotlin.StoreSubscriber + +class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreSubscriber { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var dialog: Dialog? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + } + }) + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.slide_right) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.walletState == newState.walletState + }.select { it.walletState } + } + store.dispatch(WalletAction.UpdateWallet()) + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + + + setupTransactionsRecyclerView() + setupButtons() + } + + + private fun setupTransactionsRecyclerView() { + pendingTransactionAdapter = PendingTransactionsAdapter() + rv_pending_transaction.layoutManager = LinearLayoutManager(requireContext()) + rv_pending_transaction.adapter = pendingTransactionAdapter + } + + private fun setupButtons() { + btn_confirm.text = getString(R.string.wallet_button_send) + btn_confirm.setOnClickListener { store.dispatch(WalletAction.Send()) } + + btn_share.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) } + + btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) + ) + } + } + + override fun newState(state: WalletState) { + if (activity == null) return + if (state.selectedWallet == null) return + val selectedWallet = state.getSelectedWalletData() ?: return + + tv_currency_title.text = selectedWallet.currencyData.currency + + showPendingTransactionsIfPresent(selectedWallet.pendingTransactions) + setupAddressCard(selectedWallet) + setupBalanceData(selectedWallet.currencyData) + + btn_confirm.isEnabled = selectedWallet.mainButton.enabled + btn_copy.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, requireContext())) + } + } + btn_share.setOnClickListener { + selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.ShareAddress(addressString, requireContext())) + } + } + + handleDialogs(state.walletDialog) + handleCurrencyIcon(selectedWallet) + + srl_wallet_details.setOnRefreshListener { + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + store.dispatch(WalletAction.LoadWallet( + selectedWallet.topUpState.allowed, + selectedWallet.currencyData.currencySymbol + )) + } + } + + if (selectedWallet.currencyData.status != BalanceStatus.Loading) { + srl_wallet_details.isRefreshing = false + } + } + + private fun handleCurrencyIcon(wallet: WalletData) { + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + tv_token_letter.text = null + iv_currency.colorFilter = null + iv_currency.setImageResource(blockchain.getIconRes()) + } else { + tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { iv_currency.setColorFilter(it) } + iv_currency.setImageResource(R.drawable.shape_circle) + } + } + + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + private fun setupAddressCard(state: WalletData) { + if (state.walletAddresses != null) { + if (state.shouldShowMultipleAddress()) { + (card_balance as? ViewGroup)?.beginDelayedTransition() + chip_group_address_type.show() + chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) + + chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + chip_group_address_type.hide() + } + tv_address.text = state.walletAddresses.selectedAddress.address + tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + requireContext())) + } + iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.address.toQrCode()) + } + } + + private fun setupBalanceData(data: BalanceWidgetData) { + when (data.status) { + BalanceStatus.Loading -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + tv_amount.text = "" + + showStatus(R.id.tv_status_loading) + showBalanceWithoutToken(data, false) + } + BalanceStatus.VerifiedOnline, BalanceStatus.TransactionInProgress -> { + l_balance.show() + l_balance_error.hide() + val statusView = if (data.status == BalanceStatus.VerifiedOnline) { + R.id.tv_status_verified + } else { + tv_status_error.text = + getText(R.string.wallet_balance_tx_in_progress) + R.id.group_error + } + showStatus(statusView) + tv_status_error_message.hide() + showBalanceWithoutToken(data, true) + } + BalanceStatus.Unreachable -> { + l_balance.show() + l_balance_error.hide() + tv_fiat_amount.hide() + + tv_amount.text = "" + + tv_status_error_message.text = data.errorMessage + tv_status_error.text = + getString(R.string.wallet_balance_blockchain_unreachable) + + showStatus(R.id.group_error) + tv_status_error_message.show(!data.errorMessage.isNullOrBlank()) + } + + BalanceStatus.NoAccount -> { + l_balance.hide() + l_balance_error.show() + tv_error_title.text = getText(R.string.wallet_error_no_account) + tv_error_descriptions.text = + getString( + R.string.wallet_error_no_account_subtitle_format, + data.amountToCreateAccount, data.currencySymbol + ) + } + } + } + + private fun showStatus(@IdRes viewRes: Int) { + group_error.show(viewRes == R.id.group_error) + tv_status_loading.show(viewRes == R.id.tv_status_loading) + tv_status_verified.show(viewRes == R.id.tv_status_verified) + } + + private fun showBalanceWithoutToken(data: BalanceWidgetData, showAmount: Boolean) { + tv_amount.text = if (showAmount) data.amount else "" + if (showAmount) { + tv_fiat_amount.show() + tv_fiat_amount.text = data.fiatAmount + } + } + + private fun handleDialogs(walletDialog: WalletDialog?) { + when (walletDialog) { + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { + this.show(walletDialog.amounts) + } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.meny_remove -> { + store.state.walletState.getSelectedWalletData()?.let { walletData -> + store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData)) + store.dispatch(WalletAction.MultiWallet.SelectWallet(null)) + store.dispatch(NavigationAction.PopBackTo()) + true + } + false + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + if (store.state.walletState.canBeRemoved(store.state.walletState.getSelectedWalletData())) { + inflater.inflate(R.menu.wallet_details, menu) + } + } + +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 749486e3fe..b7582f4972 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -1,9 +1,11 @@ package com.tangem.tap.features.wallet.ui -import android.app.Dialog import android.os.Bundle -import android.view.* +import android.view.Menu import android.view.Menu.NONE +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment @@ -12,39 +14,32 @@ import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionInflater import com.google.android.material.snackbar.Snackbar import com.squareup.picasso.Picasso -import com.tangem.blockchain.blockchains.bitcoin.BitcoinAddressType -import com.tangem.blockchain.blockchains.cardano.CardanoAddressType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.address.AddressType import com.tangem.tangem_sdk_new.extensions.dpToPx -import com.tangem.tap.common.extensions.* +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.twins.TwinCardNumber +import com.tangem.tap.domain.configurable.warningMessage.WarningMessage import com.tangem.tap.features.details.redux.DetailsAction import com.tangem.tap.features.wallet.redux.* -import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog -import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog -import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration +import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter +import com.tangem.tap.features.wallet.ui.wallet.MultiWalletView +import com.tangem.tap.features.wallet.ui.wallet.SingleWalletView +import com.tangem.tap.features.wallet.ui.wallet.WalletView import com.tangem.tap.store import com.tangem.wallet.R -import kotlinx.android.synthetic.main.card_balance.* import kotlinx.android.synthetic.main.fragment_wallet.* -import kotlinx.android.synthetic.main.layout_address.* -import kotlinx.android.synthetic.main.layout_send_fee.* -import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* -import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* import org.rekotlin.StoreSubscriber class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - private var dialog: Dialog? = null private var snackbar: Snackbar? = null - private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter private lateinit var warningsAdapter: WarningMessagesAdapter + private var walletView: WalletView = SingleWalletView() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setHasOptionsMenu(true) @@ -65,12 +60,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber - tv_twin_card_number.show() - iv_twin_card.show() - val number = when (cardNumber) { - TwinCardNumber.First -> "1" - TwinCardNumber.Second -> "2" - } - tv_twin_card_number.text = getString(R.string.wallet_twins_chip_format, number) - } - if (state.twinCardsState?.cardNumber == null) { - tv_twin_card_number.hide() - iv_twin_card.hide() + if (state.isMultiwalletAllowed && walletView is SingleWalletView) { + walletView = MultiWalletView() + walletView.changeWalletView(this) + } else if (!state.isMultiwalletAllowed && walletView is MultiWalletView) { + walletView = SingleWalletView() + walletView.changeWalletView(this) } + walletView.onNewState(state) - if (!state.showDetails) { + if (!state.shouldShowDetails) { toolbar.menu.removeItem(R.id.details_menu) } else if (toolbar.menu.findItem(R.id.details_menu) == null) { toolbar.menu.add(R.menu.wallet, R.id.details_menu, NONE, R.string.details_title) } - srl_wallet.setOnRefreshListener { - store.dispatch(WalletAction.LoadData) - } - setupNoInternetHandling(state) - - setupButtons(state) - setupAddressCard(state) setupCardImage(state.cardImage) - warningsAdapter.submitList(state.mainWarningsList) - rv_warning_messages.show(state.mainWarningsList.isNotEmpty()) + showWarningsIfPresent(state.mainWarningsList) - pendingTransactionAdapter.submitList(state.pendingTransactions) - rv_pending_transaction.show(state.pendingTransactions.isNotEmpty()) + srl_wallet.setOnRefreshListener { + if (state.state != ProgressState.Loading) { + store.dispatch(WalletAction.LoadData) + } + } - handleDialogs(state.walletDialog) - - l_balance.show() - BalanceWidget(this, state.currencyData, state.twinCardsState != null).setup() - - if (state.state == ProgressState.Done) { + if (state.state != ProgressState.Loading) { srl_wallet.isRefreshing = false } + } - when (state.payIdData.payIdState) { - PayIdState.Disabled, PayIdState.Loading -> group_payid.hide() - PayIdState.NotCreated -> { - group_payid.show() - tv_create_payid.show() - tv_create_payid.setOnClickListener { store.dispatch(WalletAction.CreatePayId) } - tv_payid_address.hide() - } - PayIdState.Created -> { - group_payid.show() - tv_create_payid.hide() - tv_payid_address.show() - tv_payid_address.text = state.payIdData.payId - - } - PayIdState.ErrorLoading -> { - } - } + private fun showWarningsIfPresent(warnings: List) { + warningsAdapter.submitList(warnings) + rv_warning_messages.show(warnings.isNotEmpty()) } private fun setupNoInternetHandling(state: WalletState) { @@ -174,98 +136,14 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.string.wallet_button_send - is WalletMainButton.CreateWalletButton -> { - if (state.twinCardsState == null) { - R.string.wallet_button_create_wallet - } else { - R.string.wallet_button_create_twin_wallet - } - } - } - btnConfirm.text = getString(buttonTitle) - btnConfirm.isEnabled = state.mainButton.enabled - - btnConfirm.setOnClickListener { - when (state.mainButton) { - is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) - is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) - } - } - btn_top_up.setOnClickListener { - store.dispatch( - WalletAction.TopUpAction.TopUp(requireContext(), R.color.backgroundLightGray) - ) - } - } - - private fun setupAddressCard(state: WalletState) { - if (state.walletAddresses != null) { - l_address?.show() - if (state.showMultipleAddress) { - (l_address as? ViewGroup)?.beginDelayedTransition() - chip_group_address_type.show() - chip_group_address_type.fitChipsByGroupWidth() - - val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) - if (checkedId != View.NO_ID) chip_group_address_type.check(checkedId) - - chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> - if (checkedId == -1) return@setOnCheckedChangeListener - - MultipleAddressUiHelper.idToType(checkedId, state.wallet?.blockchain)?.let { - store.dispatch(WalletAction.ChangeSelectedAddress(it)) - } - } - } else { - chip_group_address_type.hide() - } - tv_address.text = state.walletAddresses.selectedAddress.address - tv_explore?.setOnClickListener { - store.dispatch(WalletAction.ExploreAddress(requireContext())) - } - } else { - l_address?.hide() - } - } - private fun setupCardImage(cardImage: Artwork?) { Picasso.get() .load(cardImage?.artworkId) @@ -274,47 +152,13 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { - if (dialog == null) dialog = QrDialog(requireContext()).apply { - this.showQr( - walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName - ) - } - } - } - is WalletDialog.CreatePayIdDialog -> { - when (walletDialog.creatingPayIdState) { - CreatingPayIdState.EnterPayId -> { - if (dialog == null) dialog = PayIdDialog(requireContext()).apply { - this.show() - } - (dialog as? PayIdDialog)?.stopProgress() - } - CreatingPayIdState.Waiting -> (dialog as? PayIdDialog)?.showProgress() - } - } - is WalletDialog.SelectAmountToSendDialog -> { - if (dialog == null) dialog = AmountToSendDialog(requireContext()).apply { - this.show(walletDialog.amounts) - } - } - null -> { - dialog?.dismiss() - dialog = null - } - } - } - override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.details_menu -> { store.state.globalState.scanNoteResponse?.let { scanNoteResponse -> store.dispatch(DetailsAction.PrepareScreen( scanNoteResponse.card, scanNoteResponse, - store.state.walletState.wallet, + store.state.walletState.walletManagers.map { it.wallet }, store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed, store.state.globalState.appCurrency )) @@ -328,41 +172,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.id.chip_legacy - is BitcoinAddressType.Segwit -> R.id.chip_default - is CardanoAddressType.Byron -> R.id.chip_legacy - is CardanoAddressType.Shelley -> R.id.chip_default - else -> View.NO_ID - } - } - - fun idToType(id: Int, blockchain: Blockchain?): AddressType? { - return when (id) { - R.id.chip_default -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Segwit - Blockchain.CardanoShelley -> CardanoAddressType.Shelley - else -> null - } - } - R.id.chip_legacy -> { - when (blockchain) { - Blockchain.Bitcoin -> BitcoinAddressType.Legacy - Blockchain.CardanoShelley -> CardanoAddressType.Byron - else -> null - } - } - else -> null - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt rename to app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt index 93e0202458..09c6094d38 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/PendingTransactionsAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/PendingTransactionsAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.wallet.ui +package com.tangem.tap.features.wallet.ui.adapters import android.view.LayoutInflater import android.view.View diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt new file mode 100644 index 0000000000..f8c337d1f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -0,0 +1,137 @@ +package com.tangem.tap.features.wallet.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.tap.common.extensions.* +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletData +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_currency_wallet.view.* +import java.math.BigDecimal + +class WalletAdapter + : ListAdapter(DiffUtilCallback) { + + override fun getItemId(position: Int): Long { + return currentList[position].currencyData.currencySymbol?.hashCode()?.toLong() ?: 0 + } + + fun submitList(list: List, primaryBlockchain: Blockchain?, primaryToken: Token? = null) { + val listModified = list.toMutableList() + val primaryBlockchainWallet = when ( + val index = listModified.indexOfFirst { it.blockchain == primaryBlockchain } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + + val primaryTokenWallet = if (primaryToken == null) null else when ( + val index = listModified.indexOfFirst { it.token == primaryToken } + ) { + -1 -> null + else -> listModified.removeAt(index) + } + + if (list.all { it.currencyData.fiatAmount == null }) { + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + listModified + super.submitList(sortedList) + return + } + + val sorted = listModified.sortedWith( + compareByDescending { it.currencyData.fiatAmountRaw ?: BigDecimal.ZERO } + .thenBy { it.currencyData.currencySymbol } + ) + val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + sorted + super.submitList(sortedList) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder { + val layout = LayoutInflater.from(parent.context) + .inflate(R.layout.item_currency_wallet, parent, false) + return WalletsViewHolder(layout) + } + + override fun onBindViewHolder(holder: WalletsViewHolder, position: Int) { + holder.bind(currentList[position]) + } + + object DiffUtilCallback : DiffUtil.ItemCallback() { + override fun areContentsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + + override fun areItemsTheSame( + oldItem: WalletData, newItem: WalletData + ) = oldItem == newItem + } + + class WalletsViewHolder(val view: View) : + RecyclerView.ViewHolder(view) { + + fun bind(wallet: WalletData) { + view.tv_currency.text = wallet.currencyData.currency + view.tv_amount.text = wallet.currencyData.amount + if (view.tv_amount.isEllipsized()) { + val allowedSize = view.tv_amount.text.length - 4 + view.tv_amount.text = wallet.currencyData.amount?.ellipsizeBeforeSpace(allowedSize) + } + view.tv_amount_fiat.text = wallet.currencyData.fiatAmount + view.tv_exchange_rate.text = wallet.fiatRateString + view.card_wallet.setOnClickListener { + store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet)) + } + val blockchain = wallet.currencyData.currencySymbol?.let { Blockchain.fromCurrency(it) } + if (blockchain != null && blockchain != Blockchain.Unknown) { + view.tv_token_letter.text = null + view.iv_currency.colorFilter = null + view.iv_currency.setImageResource(blockchain.getIconRes()) + } else { + view.tv_token_letter.text = wallet.currencyData.currencySymbol?.take(1) + wallet.token?.getColor()?.let { view.iv_currency.setColorFilter(it) } + view.iv_currency.setImageResource(R.drawable.shape_circle) + } + when (wallet.currencyData.status) { + BalanceStatus.VerifiedOnline -> hideWarning() + BalanceStatus.Loading -> { + hideWarning() + if (wallet.currencyData.amount == null) { + view.tv_exchange_rate.text = view.context.getText(R.string.wallet_balance_loading) + } + } + BalanceStatus.TransactionInProgress -> + showWarning(view.context.getString(R.string.wallet_balance_tx_in_progress)) + BalanceStatus.Unreachable -> + showWarning(view.context.getString(R.string.wallet_balance_blockchain_unreachable)) + + BalanceStatus.NoAccount -> + showWarning(view.context.getString(R.string.wallet_error_no_account)) + else -> { + } + } + } + + private fun showWarning(message: String) { + toggleWarning(true) + view.tv_status_error_message.text = message + } + + private fun hideWarning() { + toggleWarning(false) + } + + private fun toggleWarning(show: Boolean) { + view.tv_exchange_rate.show(!show) + view.tv_status_error_message.show(show) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt similarity index 98% rename from app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt rename to app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index e7a2a559e3..3200ac34ec 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -1,4 +1,4 @@ -package com.tangem.tap.features.wallet.ui +package com.tangem.tap.features.wallet.ui.adapters import android.graphics.Rect import android.view.LayoutInflater diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt deleted file mode 100644 index dd1944ac74..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/PayIdDialog.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.wallet.ui.dialogs - -import android.app.Dialog -import android.content.Context -import android.view.View -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.notificationsHandler -import com.tangem.tap.store -import com.tangem.wallet.R -import kotlinx.android.synthetic.main.dialog_create_payid.* - -class PayIdDialog(context: Context) : Dialog(context) { - - init { - this.setContentView(R.layout.dialog_create_payid) - } - - override fun show() { - notificationsHandler?.replaceBaseLayout(this.cl_payid_dialog) - this.setOnDismissListener { - notificationsHandler?.returnBaseLayout() - store.dispatch(WalletAction.CreatePayId.Cancel) - } - this.btn_create_payid?.setOnClickListener { - if (this.et_payid.text.isNullOrBlank()) { - store.dispatch(WalletAction.CreatePayId.EmptyField) - } else { - val payid = this.et_payid.text!!.toString() + - this.context.getString(R.string.wallet_create_payid_domain) - store.dispatch(WalletAction.CreatePayId.CompleteCreatingPayId(payid)) - } - } - super.show() - } - - fun showProgress() { - this.btn_create_payid?.visibility = View.INVISIBLE - this.pb_create_payid?.show() - } - - fun stopProgress() { - this.btn_create_payid?.show() - this.pb_create_payid?.hide() - } -} \ No newline at end of file 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 new file mode 100644 index 0000000000..2fb7ab8876 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -0,0 +1,126 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import androidx.recyclerview.widget.LinearLayoutManager +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.features.tokens.redux.TokensAction +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.BalanceStatus +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_balance_error.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* + + +class MultiWalletView : WalletView { + + private var fragment: WalletFragment? = null + private lateinit var walletsAdapter: WalletAdapter + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showMultiWalletView() + setupButtons() + } + + + private fun showMultiWalletView() { + val fragment = fragment ?: return + fragment.tv_twin_card_number.hide() + fragment.iv_twin_card.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.hide() + fragment.l_address.hide() + fragment.l_buttons_short.hide() + fragment.l_buttons_long.hide() + fragment.btn_scan_multiwallet?.show() + fragment.rv_multiwallet.show() + fragment.btn_add_token.show() + } + + private fun setupButtons() { + val fragment = fragment ?: return + fragment.btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) } + } + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + this.fragment = null + } + + override fun onViewCreated() { + setupWalletsRecyclerView() + } + + private fun setupWalletsRecyclerView() { + val fragment = fragment ?: return + walletsAdapter = WalletAdapter() + walletsAdapter.setHasStableIds(true) + fragment.rv_multiwallet.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_multiwallet.adapter = walletsAdapter + } + + override fun onNewState(state: WalletState) { + val fragment = fragment ?: return + + walletsAdapter.submitList(state.wallets, state.primaryBlockchain, state.primaryToken) + + fragment.btn_add_token?.setOnClickListener { + store.dispatch(TokensAction.LoadCurrencies) + store.dispatch(TokensAction.SetAddedCurrencies(state.wallets)) + store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens)) + } + handleErrorStates(state, fragment) + } + + private fun handleErrorStates(state: WalletState, fragment: WalletFragment) { + when (state.primaryWallet?.currencyData?.status) { + BalanceStatus.EmptyCard -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_empty_card), + fragment.getString(R.string.wallet_error_empty_card_subtitle) + ) + configureButtonsForEmptyWalletState(fragment) + } + BalanceStatus.UnknownBlockchain -> { + showErrorState( + fragment, + fragment.getText(R.string.wallet_error_unsupported_blockchain), + fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle) + ) + } + } + } + + private fun showErrorState( + fragment: WalletFragment, errorTitle: CharSequence, errorDescription: CharSequence + ) { + fragment.l_card_balance.show() + fragment.l_balance.hide() + fragment.l_balance_error.show() + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.tv_error_title.text = errorTitle + fragment.tv_error_descriptions.text = errorDescription + } + + private fun configureButtonsForEmptyWalletState(fragment: WalletFragment) { + fragment.btn_scan_multiwallet.hide() + fragment.l_buttons_long.show() + fragment.btn_scan_long.setOnClickListener { store.dispatch(WalletAction.Scan) } + fragment.btn_confirm_long.setOnClickListener { store.dispatch(WalletAction.CreateWallet) } + fragment.btn_confirm_long.text = fragment.getText(R.string.wallet_button_create_wallet) + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..34cab5caf0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -0,0 +1,236 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import android.app.Dialog +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.tap.common.extensions.* +import com.tangem.tap.domain.twins.TwinCardNumber +import com.tangem.tap.features.wallet.models.PendingTransaction +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.ui.BalanceWidget +import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper +import com.tangem.tap.features.wallet.ui.WalletFragment +import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter +import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog +import com.tangem.tap.features.wallet.ui.dialogs.QrDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.card_balance.* +import kotlinx.android.synthetic.main.fragment_wallet.* +import kotlinx.android.synthetic.main.layout_address.* +import kotlinx.android.synthetic.main.layout_wallet_long_buttons.* +import kotlinx.android.synthetic.main.layout_wallet_short_buttons.* + +class SingleWalletView : WalletView { + + private lateinit var pendingTransactionAdapter: PendingTransactionsAdapter + private var fragment: WalletFragment? = null + private var dialog: Dialog? = null + + override fun setFragment(fragment: WalletFragment) { + this.fragment = fragment + } + + override fun removeFragment() { + fragment = null + } + + override fun changeWalletView(fragment: WalletFragment) { + setFragment(fragment) + onViewCreated() + showSingleWalletView() + } + + private fun showSingleWalletView() { + val fragment = fragment ?: return + fragment.rv_multiwallet.hide() + fragment.btn_add_token.hide() + fragment.btn_scan_multiwallet?.hide() + fragment.rv_pending_transaction.hide() + fragment.l_card_balance.show() + fragment.l_address.show() + } + + override fun onViewCreated() { + setupTransactionsRecyclerView() + } + + + private fun setupTransactionsRecyclerView() { + val fragment = fragment ?: return + pendingTransactionAdapter = PendingTransactionsAdapter() + fragment.rv_pending_transaction.layoutManager = LinearLayoutManager(fragment.requireContext()) + fragment.rv_pending_transaction.adapter = pendingTransactionAdapter + } + + override fun onNewState(state: WalletState) { + setupTwinCards(state.twinCardsState) + state.primaryWallet ?: return + setupButtons(state.primaryWallet, state.twinCardsState != null) + setupAddressCard(state.primaryWallet) + showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions) + setupBalance(state, state.primaryWallet) + handleDialogs(state.walletDialog) + } + + private fun showPendingTransactionsIfPresent(pendingTransactions: List) { + pendingTransactionAdapter.submitList(pendingTransactions) + fragment?.rv_pending_transaction?.show(pendingTransactions.isNotEmpty()) + } + + + private fun setupBalance(state: WalletState, primaryWallet: WalletData) { + fragment?.apply { + this.l_balance.show() + BalanceWidget(this, primaryWallet.currencyData, state.twinCardsState != null).setup() + } + } + + private fun setupTwinCards(twinCardsState: TwinCardsState?) { + fragment?.apply { + twinCardsState?.cardNumber?.let { cardNumber -> + this.tv_twin_card_number.show() + this.iv_twin_card.show() + val number = when (cardNumber) { + TwinCardNumber.First -> "1" + TwinCardNumber.Second -> "2" + } + this.tv_twin_card_number.text = + this.getString(R.string.wallet_twins_chip_format, number) + } + if (twinCardsState?.cardNumber == null) { + this.tv_twin_card_number.hide() + this.iv_twin_card.hide() + } + } + + } + + private fun setupButtons(state: WalletData, isTwinsWallet: Boolean) { + val fragment = fragment ?: return + + setupButtonsType(state, fragment) + + val btnConfirm = if (state.topUpState.allowed) { + fragment.btn_confirm_short + } else { + fragment.btn_confirm_long + } + val btnScan = if (state.topUpState.allowed) { + fragment.btn_scan_short + } else { + fragment.btn_scan_long + } + + setupConfirmButton(state, btnConfirm, fragment, isTwinsWallet) + + btnScan.setOnClickListener { + store.dispatch(WalletAction.Scan) + } + + fragment.btn_copy.setOnClickListener { + state.walletAddresses?.selectedAddress?.address?.let { addressString -> + store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext())) + } + } + fragment.btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) } + + fragment.btn_top_up.setOnClickListener { + store.dispatch( + WalletAction.TopUpAction.TopUp(fragment.requireContext(), R.color.backgroundLightGray) + ) + } + } + + private fun setupButtonsType(state: WalletData, fragment: WalletFragment) { + if (state.topUpState.allowed) { + fragment.l_buttons_long.hide() + fragment.l_buttons_short.show() + } else { + fragment.l_buttons_long.show() + fragment.l_buttons_short.hide() + } + } + + private fun setupConfirmButton( + state: WalletData, btnConfirm: Button, fragment: WalletFragment, isTwinsWallet: Boolean + ) { + val buttonTitle = when (state.mainButton) { + is WalletMainButton.SendButton -> R.string.wallet_button_send + is WalletMainButton.CreateWalletButton -> { + if (!isTwinsWallet) { + R.string.wallet_button_create_wallet + } else { + R.string.wallet_button_create_twin_wallet + } + } + } + btnConfirm.text = fragment.getString(buttonTitle) + btnConfirm.isEnabled = state.mainButton.enabled + btnConfirm.setOnClickListener { + when (state.mainButton) { + is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send()) + is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet) + } + } + } + + + private fun setupAddressCard(state: WalletData) { + val fragment = fragment ?: return + if (state.walletAddresses != null) { + fragment.l_address?.show() + if (state.shouldShowMultipleAddress()) { + (fragment.l_address as? ViewGroup)?.beginDelayedTransition() + fragment.chip_group_address_type.show() + fragment.chip_group_address_type.fitChipsByGroupWidth() + + val checkedId = MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type) + if (checkedId != View.NO_ID) fragment.chip_group_address_type.check(checkedId) + + fragment.chip_group_address_type.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == -1) return@setOnCheckedChangeListener + val type = MultipleAddressUiHelper.idToType(checkedId, state.currencyData.currencySymbol) + type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) } + } + } else { + fragment.chip_group_address_type.hide() + } + fragment.tv_address.text = state.walletAddresses.selectedAddress.address + fragment.tv_explore?.setOnClickListener { + store.dispatch(WalletAction.ExploreAddress( + state.walletAddresses.selectedAddress.exploreUrl, + fragment.requireContext())) + } + } else { + fragment.l_address?.hide() + } + } + + private fun handleDialogs(walletDialog: WalletDialog?) { + val fragment = fragment ?: return + when (walletDialog) { + is WalletDialog.QrDialog -> { + if (walletDialog.qrCode != null && walletDialog.shareUrl != null) { + if (dialog == null) dialog = QrDialog(fragment.requireContext()).apply { + this.showQr( + walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName + ) + } + } + } + is WalletDialog.SelectAmountToSendDialog -> { + if (dialog == null) dialog = AmountToSendDialog(fragment.requireContext()).apply { + this.show(walletDialog.amounts) + } + } + null -> { + dialog?.dismiss() + dialog = null + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt new file mode 100644 index 0000000000..ab49865c02 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/WalletView.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.features.wallet.ui.wallet + +import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.ui.WalletFragment + +interface WalletView { + + fun setFragment(fragment: WalletFragment) + + fun removeFragment() + + fun changeWalletView(fragment: WalletFragment) + + fun onViewCreated() + + fun onNewState(state: WalletState) + +} \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_binance.png b/app/src/main/res/drawable-hdpi/ic_binance.png new file mode 100644 index 0000000000..edce12fa14 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_btc.png b/app/src/main/res/drawable-hdpi/ic_btc.png new file mode 100644 index 0000000000..be16f5a51b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_btc_cash.png b/app/src/main/res/drawable-hdpi/ic_btc_cash.png new file mode 100644 index 0000000000..ec0864fdb8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_cardano.png b/app/src/main/res/drawable-hdpi/ic_cardano.png new file mode 100644 index 0000000000..8a4ac01ad4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_eth.png b/app/src/main/res/drawable-hdpi/ic_eth.png new file mode 100644 index 0000000000..9db27f814e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_ltc.png b/app/src/main/res/drawable-hdpi/ic_ltc.png new file mode 100644 index 0000000000..6c0fb4ad3c Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_rsk.png b/app/src/main/res/drawable-hdpi/ic_rsk.png new file mode 100644 index 0000000000..be49cda24f Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stellar.png b/app/src/main/res/drawable-hdpi/ic_stellar.png new file mode 100644 index 0000000000..7bb500ef1d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_tezos.png b/app/src/main/res/drawable-hdpi/ic_tezos.png new file mode 100644 index 0000000000..938fc9d3d4 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_xlm.png b/app/src/main/res/drawable-hdpi/ic_xlm.png new file mode 100644 index 0000000000..6155f291ea Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_xrp.png b/app/src/main/res/drawable-hdpi/ic_xrp.png new file mode 100644 index 0000000000..058650f44b Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_binance.png b/app/src/main/res/drawable-ldpi/ic_binance.png new file mode 100644 index 0000000000..18aa82ea4b Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_btc.png b/app/src/main/res/drawable-ldpi/ic_btc.png new file mode 100644 index 0000000000..982bb50ad1 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_btc_cash.png b/app/src/main/res/drawable-ldpi/ic_btc_cash.png new file mode 100644 index 0000000000..3789ae5071 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_cardano.png b/app/src/main/res/drawable-ldpi/ic_cardano.png new file mode 100644 index 0000000000..dd8332ef62 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_eth.png b/app/src/main/res/drawable-ldpi/ic_eth.png new file mode 100644 index 0000000000..b8cd7b26ab Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_ltc.png b/app/src/main/res/drawable-ldpi/ic_ltc.png new file mode 100644 index 0000000000..a9dc421e25 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_rsk.png b/app/src/main/res/drawable-ldpi/ic_rsk.png new file mode 100644 index 0000000000..416da64bb7 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_stellar.png b/app/src/main/res/drawable-ldpi/ic_stellar.png new file mode 100644 index 0000000000..0de7200a90 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_tezos.png b/app/src/main/res/drawable-ldpi/ic_tezos.png new file mode 100644 index 0000000000..502e81b732 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_xlm.png b/app/src/main/res/drawable-ldpi/ic_xlm.png new file mode 100644 index 0000000000..3262fe8e13 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_xrp.png b/app/src/main/res/drawable-ldpi/ic_xrp.png new file mode 100644 index 0000000000..70d484bc18 Binary files /dev/null and b/app/src/main/res/drawable-ldpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_binance.png b/app/src/main/res/drawable-mdpi/ic_binance.png new file mode 100644 index 0000000000..9254cb4a01 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_btc.png b/app/src/main/res/drawable-mdpi/ic_btc.png new file mode 100644 index 0000000000..f02a7a13a1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_btc_cash.png b/app/src/main/res/drawable-mdpi/ic_btc_cash.png new file mode 100644 index 0000000000..e0dea3ea53 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_cardano.png b/app/src/main/res/drawable-mdpi/ic_cardano.png new file mode 100644 index 0000000000..7ff6fc7ec0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_eth.png b/app/src/main/res/drawable-mdpi/ic_eth.png new file mode 100644 index 0000000000..e01e76371f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_ltc.png b/app/src/main/res/drawable-mdpi/ic_ltc.png new file mode 100644 index 0000000000..13f1de9253 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_rsk.png b/app/src/main/res/drawable-mdpi/ic_rsk.png new file mode 100644 index 0000000000..af269e0bda Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stellar.png b/app/src/main/res/drawable-mdpi/ic_stellar.png new file mode 100644 index 0000000000..207f7c21a8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_tezos.png b/app/src/main/res/drawable-mdpi/ic_tezos.png new file mode 100644 index 0000000000..71d699414b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_xlm.png b/app/src/main/res/drawable-mdpi/ic_xlm.png new file mode 100644 index 0000000000..1a35164ed1 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_xrp.png b/app/src/main/res/drawable-mdpi/ic_xrp.png new file mode 100644 index 0000000000..ec5e3bb680 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_binance.png b/app/src/main/res/drawable-xhdpi/ic_binance.png new file mode 100644 index 0000000000..4a1619f750 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_btc.png b/app/src/main/res/drawable-xhdpi/ic_btc.png new file mode 100644 index 0000000000..e58ee8b721 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..4709218591 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_cardano.png b/app/src/main/res/drawable-xhdpi/ic_cardano.png new file mode 100644 index 0000000000..5e9c1be96a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_eth.png b/app/src/main/res/drawable-xhdpi/ic_eth.png new file mode 100644 index 0000000000..5e5807bf8b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_ltc.png b/app/src/main/res/drawable-xhdpi/ic_ltc.png new file mode 100644 index 0000000000..1135186fa5 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_rsk.png b/app/src/main/res/drawable-xhdpi/ic_rsk.png new file mode 100644 index 0000000000..2016162c6d Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stellar.png b/app/src/main/res/drawable-xhdpi/ic_stellar.png new file mode 100644 index 0000000000..d6073f7fc7 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_tezos.png b/app/src/main/res/drawable-xhdpi/ic_tezos.png new file mode 100644 index 0000000000..811d221bdb Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_xlm.png b/app/src/main/res/drawable-xhdpi/ic_xlm.png new file mode 100644 index 0000000000..7ebe6a2fe8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_xrp.png b/app/src/main/res/drawable-xhdpi/ic_xrp.png new file mode 100644 index 0000000000..aa8adabc2c Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_binance.png b/app/src/main/res/drawable-xxhdpi/ic_binance.png new file mode 100644 index 0000000000..dbe9c0805f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_btc.png b/app/src/main/res/drawable-xxhdpi/ic_btc.png new file mode 100644 index 0000000000..87f552a566 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..11d710501e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_cardano.png b/app/src/main/res/drawable-xxhdpi/ic_cardano.png new file mode 100644 index 0000000000..74faea419a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_eth.png b/app/src/main/res/drawable-xxhdpi/ic_eth.png new file mode 100644 index 0000000000..1e7a0e7952 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_ltc.png b/app/src/main/res/drawable-xxhdpi/ic_ltc.png new file mode 100644 index 0000000000..0cf80c3570 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_rsk.png b/app/src/main/res/drawable-xxhdpi/ic_rsk.png new file mode 100644 index 0000000000..3a660c50bc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stellar.png b/app/src/main/res/drawable-xxhdpi/ic_stellar.png new file mode 100644 index 0000000000..8824973627 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_tezos.png b/app/src/main/res/drawable-xxhdpi/ic_tezos.png new file mode 100644 index 0000000000..16dccf95c8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_xlm.png b/app/src/main/res/drawable-xxhdpi/ic_xlm.png new file mode 100644 index 0000000000..0dc1b2ba5f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_xrp.png b/app/src/main/res/drawable-xxhdpi/ic_xrp.png new file mode 100644 index 0000000000..881d6b7b4b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_binance.png b/app/src/main/res/drawable-xxxhdpi/ic_binance.png new file mode 100644 index 0000000000..f18f7ff9d7 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_binance.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_btc.png b/app/src/main/res/drawable-xxxhdpi/ic_btc.png new file mode 100644 index 0000000000..165164f069 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_btc.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png b/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png new file mode 100644 index 0000000000..babd59d8b1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_btc_cash.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_cardano.png b/app/src/main/res/drawable-xxxhdpi/ic_cardano.png new file mode 100644 index 0000000000..1f8b6888a5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_cardano.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_eth.png b/app/src/main/res/drawable-xxxhdpi/ic_eth.png new file mode 100644 index 0000000000..74ead90795 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_eth.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_ltc.png b/app/src/main/res/drawable-xxxhdpi/ic_ltc.png new file mode 100644 index 0000000000..d159e6728f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_ltc.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_rsk.png b/app/src/main/res/drawable-xxxhdpi/ic_rsk.png new file mode 100644 index 0000000000..b442ac65a4 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_rsk.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stellar.png b/app/src/main/res/drawable-xxxhdpi/ic_stellar.png new file mode 100644 index 0000000000..a19f0db146 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stellar.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_tezos.png b/app/src/main/res/drawable-xxxhdpi/ic_tezos.png new file mode 100644 index 0000000000..d91ceab8fc Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_tezos.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_xlm.png b/app/src/main/res/drawable-xxxhdpi/ic_xlm.png new file mode 100644 index 0000000000..6500485826 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_xlm.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_xrp.png b/app/src/main/res/drawable-xxxhdpi/ic_xrp.png new file mode 100644 index 0000000000..22400ff291 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_xrp.png differ diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000000..9b975701fe --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy_chip.xml b/app/src/main/res/drawable/ic_copy_chip.xml new file mode 100644 index 0000000000..83325e0952 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_chip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_inactive.xml b/app/src/main/res/drawable/ic_inactive.xml new file mode 100644 index 0000000000..eb19f0ce6d --- /dev/null +++ b/app/src/main/res/drawable/ic_inactive.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/drawable/ic_no_tokens.xml b/app/src/main/res/drawable/ic_no_tokens.xml new file mode 100644 index 0000000000..f1149e0fad --- /dev/null +++ b/app/src/main/res/drawable/ic_no_tokens.xml @@ -0,0 +1,468 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 0000000000..d698df7efe --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_share_chip.xml b/app/src/main/res/drawable/ic_share_chip.xml new file mode 100644 index 0000000000..e026b36c2e --- /dev/null +++ b/app/src/main/res/drawable/ic_share_chip.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/shape_circle.xml b/app/src/main/res/drawable/shape_circle.xml new file mode 100644 index 0000000000..f857819f4d --- /dev/null +++ b/app/src/main/res/drawable/shape_circle.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_tokens.xml b/app/src/main/res/layout/fragment_add_tokens.xml new file mode 100644 index 0000000000..3ed5cbd260 --- /dev/null +++ b/app/src/main/res/layout/fragment_add_tokens.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 7a99598875..8fe7ff059f 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -116,15 +116,15 @@ android:id="@+id/tv_disclaimer" android:layout_width="match_parent" android:layout_height="wrap_content" + android:drawablePadding="15dp" android:paddingTop="7dp" android:paddingBottom="10dp" - android:drawablePadding="15dp" + android:text="@string/disclaimer_title" android:textColor="@color/darkGray6" android:textSize="16sp" app:drawableEndCompat="@drawable/ic_angle_bracket_right" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" - android:text="@string/disclaimer_title" /> + app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" /> + app:layout_constraintTop_toBottomOf="@id/tv_security_title" /> + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index f3f4bd73a9..47619d0f8c 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -123,6 +123,7 @@ layout="@layout/card_balance" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" /> @@ -131,8 +132,39 @@ layout="@layout/layout_address" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" app:layout_constraintTop_toBottomOf="@id/l_card_balance" /> + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_wallet_details.xml b/app/src/main/res/layout/fragment_wallet_details.xml new file mode 100644 index 0000000000..a848f0df9d --- /dev/null +++ b/app/src/main/res/layout/fragment_wallet_details.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_currency_subtitle.xml b/app/src/main/res/layout/item_currency_subtitle.xml new file mode 100644 index 0000000000..c4ae27800d --- /dev/null +++ b/app/src/main/res/layout/item_currency_subtitle.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_currency_wallet.xml b/app/src/main/res/layout/item_currency_wallet.xml new file mode 100644 index 0000000000..09b7dd4cc9 --- /dev/null +++ b/app/src/main/res/layout/item_currency_wallet.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_popular_token.xml b/app/src/main/res/layout/item_popular_token.xml new file mode 100644 index 0000000000..031069600f --- /dev/null +++ b/app/src/main/res/layout/item_popular_token.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_balance_wallet_details.xml b/app/src/main/res/layout/layout_balance_wallet_details.xml new file mode 100644 index 0000000000..56bf9b2d69 --- /dev/null +++ b/app/src/main/res/layout/layout_balance_wallet_details.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_search_view.xml b/app/src/main/res/layout/layout_search_view.xml new file mode 100644 index 0000000000..65bbfa77d1 --- /dev/null +++ b/app/src/main/res/layout/layout_search_view.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_details.xml b/app/src/main/res/layout/layout_wallet_details.xml new file mode 100644 index 0000000000..8c33b83ce9 --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_details.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_long_buttons.xml b/app/src/main/res/layout/layout_wallet_long_buttons.xml index 3872698afc..d63cb4681a 100644 --- a/app/src/main/res/layout/layout_wallet_long_buttons.xml +++ b/app/src/main/res/layout/layout_wallet_long_buttons.xml @@ -1,8 +1,8 @@ + android:layout_height="match_parent"> + + app:layout_constraintVertical_bias="1" /> + app:layout_constraintVertical_bias="1" /> + app:layout_constraintVertical_bias="1" /> \ No newline at end of file diff --git a/app/src/main/res/menu/popular_tokens.xml b/app/src/main/res/menu/popular_tokens.xml new file mode 100644 index 0000000000..80d52565c9 --- /dev/null +++ b/app/src/main/res/menu/popular_tokens.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/wallet_details.xml b/app/src/main/res/menu/wallet_details.xml new file mode 100644 index 0000000000..9622311e82 --- /dev/null +++ b/app/src/main/res/menu/wallet_details.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index da52a1a514..b6a2be9cc6 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -30,8 +30,13 @@ this wallet. The twin address was successfully created + //Common Start Back + Add + Remove + Search + Memo Destination tag @@ -39,4 +44,35 @@ this wallet. Invalid Memo ID. It won\'t be added to the transaction + All fields are required + + //Manage tokens + No tokens yet + Added tokens + Remove %s token + Are you sure you want to remove this token? + Add Tokens + Add Token + Manage Tokens + Add Custom + Custom tokens + Popular tokens + Add Custom Token + Please, fill all fields + Entered number is not a valid decimals number + Name + Token symbol + Contract address + Decimals + ex. USD Coin + ex. USDC + + //Details + Blockchain + Manage tokens + + Blockchains + Ethereum Tokens + +