Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-07 10:27:14 +03:00
parent e03f078120
commit a3cc0001f4
34 changed files with 489 additions and 283 deletions

View file

@ -1,9 +1,9 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.common.redux.navigation.navigationReducer
import com.tangem.tap.common.redux.navigation.NavigationReducer
import com.tangem.tap.features.send.redux.SendReducer
import com.tangem.tap.features.wallet.redux.walletReducer
import com.tangem.tap.features.wallet.redux.WalletReducer
import org.rekotlin.Action
fun appReducer(action: Action, state: AppState?): AppState {
@ -11,9 +11,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = navigationReducer(action, state),
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
walletState = walletReducer(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendReducer.reduce(action, state.sendState)
)
}

View file

@ -1,13 +1,11 @@
package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.Card
import com.tangem.tap.domain.tasks.ScanNoteResponse
import org.rekotlin.Action
import java.math.BigDecimal
sealed class GlobalAction : Action {
data class LoadCard(val card: Card) : GlobalAction()
data class LoadWalletManager(val walletManager: WalletManager) : GlobalAction()
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SetFiatRate(val fiatRates: Pair<String, BigDecimal>) : GlobalAction()
}

View file

@ -10,15 +10,13 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
var newState = state.globalState
when (action) {
is GlobalAction.LoadCard -> newState = newState.copy(card = action.card)
is GlobalAction.SaveScanNoteResponse ->
newState = newState.copy(scanNoteResponse = action.scanNoteResponse)
is GlobalAction.SetFiatRate -> {
val rates = newState.fiatRates.rates.toMutableMap()
rates[action.fiatRates.first] = action.fiatRates.second
newState = newState.copy(fiatRates = FiatRates(rates))
}
is GlobalAction.LoadWalletManager ->
newState = newState.copy(walletManager = action.walletManager)
}
return newState
}

View file

@ -1,14 +1,12 @@
package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.Card
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import org.rekotlin.StateType
import java.math.BigDecimal
data class GlobalState(
val card: Card? = null,
val walletManager: WalletManager? = null,
val scanNoteResponse: ScanNoteResponse? = null,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val fiatRates: FiatRates = FiatRates(emptyMap()),
) : StateType

View file

@ -4,7 +4,13 @@ import com.tangem.tap.common.extensions.getPreviousScreen
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
fun navigationReducer(action: Action, state: AppState): NavigationState {
class NavigationReducer {
companion object {
fun reduce(action: Action, state: AppState): NavigationState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): NavigationState {
val navigationAction = action as? NavigationAction ?: return state.navigationState
val navState = state.navigationState
@ -15,7 +21,7 @@ fun navigationReducer(action: Action, state: AppState): NavigationState {
}
is NavigationAction.PopBackTo -> {
val screen =
navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
}

View file

@ -7,4 +7,5 @@ sealed class TapError(@StringRes val localizedMessage: Int): Throwable() {
object PayIdAlreadyCreated: TapError(R.string.error_payid_already_created)
object PayIdCreatingError: TapError(R.string.error_creating_payid)
object PayIdEmptyField: TapError(R.string.wallet_create_payid_empty)
object UnknownBlockchain: TapError(R.string.wallet_unknown_blockchain)
}

View file

@ -24,7 +24,7 @@ class TapWalletManager {
private val tangemService = TangemService()
suspend fun loadWalletData() {
val walletManager = store.state.globalState.walletManager
val walletManager = store.state.globalState.scanNoteResponse?.walletManager
if (walletManager == null) {
store.dispatch(WalletAction.LoadWallet.Failure())
return
@ -38,7 +38,9 @@ class TapWalletManager {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
store.dispatch(WalletAction.LoadArtwork.Success(result.data.byteStream().readBytes()))
store.dispatch(WalletAction.LoadArtwork.Success(
artworkId, result.data.byteStream().readBytes())
)
}
is Result.Failure -> store.dispatch(WalletAction.LoadArtwork.Failure)
}
@ -51,8 +53,9 @@ class TapWalletManager {
}
suspend fun loadFiatRate() {
val blockchainCurrency = store.state.globalState.walletManager?.wallet?.blockchain?.currency
val tokenCurrency = store.state.globalState.walletManager?.wallet?.token?.symbol
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val blockchainCurrency = wallet?.blockchain?.currency
val tokenCurrency = wallet?.token?.symbol
val blockchainRate = blockchainCurrency?.let { coinMarketCapService.getRate(it) }
val tokenRate = tokenCurrency?.let { coinMarketCapService.getRate(it) }
@ -66,9 +69,8 @@ class TapWalletManager {
suspend fun onCardScanned(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.LoadCard(data.card))
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
if (data.walletManager != null) {
store.dispatch(GlobalAction.LoadWalletManager(data.walletManager))
data.verifyResponse?.artworkInfo?.id?.let {
store.dispatch(WalletAction.LoadArtwork(data.card, it))
}
@ -113,13 +115,14 @@ class TapWalletManager {
private suspend fun loadPayIdIfNeeded(): Result<String?>? {
val scanNoteResponse = store.state.globalState.scanNoteResponse
if (!TapConfig.usePayId ||
store.state.walletState.payIdData.payIdState == PayIdState.Disabled ||
store.state.globalState.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
scanNoteResponse?.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) {
return null
}
val cardId = store.state.globalState.card?.cardId
val publicKey = store.state.globalState.card?.cardPublicKey
val cardId = scanNoteResponse?.card?.cardId
val publicKey = scanNoteResponse?.card?.cardPublicKey
if (cardId == null || publicKey == null) {
return null
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.home
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -17,5 +18,11 @@ class HomeFragment : Fragment(R.layout.fragment_home) {
btn_shop?.setOnClickListener { store.dispatch(HomeAction.GoToShop(requireContext())) }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
}
}

View file

@ -37,7 +37,7 @@ private fun handleSendAction(action: Action) {
internal class AddressPayIdHandler {
fun handle(data: String?) {
val walletManager = store.state.globalState.walletManager ?: return
val walletManager = store.state.globalState.scanNoteResponse?.walletManager ?: return
val clipboardData = data ?: return
if (PayIdManager.isPayId(clipboardData)) {
@ -61,7 +61,7 @@ internal class AddressPayIdHandler {
private suspend fun verifyPayID(walletManager: WalletManager, payID: String?): String? {
val cardId = walletManager.cardId
val publicKey = store.state.globalState.card?.cardPublicKey
val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey
// val result = PayIdManager().getPayId(cardId, publicKey.toHexString())
// withContext(Dispatchers.Main) {

View file

@ -12,6 +12,8 @@ import java.math.BigDecimal
sealed class WalletAction : Action {
object LoadData : WalletAction()
object LoadWallet : WalletAction() {
data class Success(val wallet: Wallet): WalletAction()
data class NoAccount(val amountToCreateAccount: Int): WalletAction()
@ -27,7 +29,7 @@ sealed class WalletAction : Action {
object Failure: WalletAction()
}
data class LoadArtwork(val card: Card, val artworkId: String) : WalletAction() {
data class Success(val artwork: ByteArray) : WalletAction()
data class Success(val artworkId: String, val artwork: ByteArray) : WalletAction()
object Failure: WalletAction()
}
object Scan : WalletAction()

View file

@ -58,9 +58,9 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.card?.cardId
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
val wallet = store.state.walletState.wallet
val publicKey = store.state.globalState.card?.cardPublicKey
val publicKey = store.state.globalState.scanNoteResponse?.card?.cardPublicKey
if (cardId != null && wallet != null && publicKey != null) {
val result = PayIdManager().setPayId(
cardId, publicKey.toHexString(),
@ -90,14 +90,21 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
}
}
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.onCardScanned(it)
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.wallet?.address?.let {
store.state.walletState.addressData?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.wallet?.exploreUrl)
val uri = Uri.parse(store.state.walletState.addressData?.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}

View file

@ -12,7 +12,13 @@ import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import org.rekotlin.Action
fun walletReducer(action: Action, state: AppState): WalletState {
class WalletReducer {
companion object {
fun reduce(action: Action, state: AppState): WalletState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): WalletState {
if (action !is WalletAction) return state.walletState
@ -24,15 +30,31 @@ fun walletReducer(action: Action, state: AppState): WalletState {
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true)
)
is WalletAction.LoadWallet -> newState = WalletState(
state = ProgressState.Loading,
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
state.globalState.walletManager?.wallet?.blockchain?.fullName
),
mainButton = WalletMainButton.SendButton(false)
is WalletAction.LoadWallet -> {
val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet
val addressData = if (wallet == null) {
null
} else {
AddressData(wallet.address, wallet.shareUrl, wallet.exploreUrl)
}
val currentArtworkId = state.globalState.scanNoteResponse?.verifyResponse?.artworkInfo?.id
val cardImage = if (newState.cardImage?.artworkId == currentArtworkId) {
newState.cardImage
} else {
null
}
newState = WalletState(
state = ProgressState.Loading,
cardImage = cardImage,
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
wallet?.blockchain?.fullName
),
addressData = addressData,
mainButton = WalletMainButton.SendButton(false)
)
)
}
is WalletAction.LoadWallet.Success -> {
val token = action.wallet.amounts[AmountType.Token]
val tokenData = if (token != null) {
@ -62,7 +84,7 @@ fun walletReducer(action: Action, state: AppState): WalletState {
)
}
is WalletAction.LoadWallet.NoAccount -> {
val wallet = state.globalState.walletManager?.wallet
val wallet = state.globalState.scanNoteResponse?.walletManager?.wallet
newState = newState.copy(
state = ProgressState.Done, wallet = wallet,
currencyData = BalanceWidgetData(
@ -85,26 +107,26 @@ fun walletReducer(action: Action, state: AppState): WalletState {
val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) {
newState.wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatString(rate)
} else {
null
newState.currencyData.fiatAmount
}
val tokenFiatAmount = if (currency == newState.wallet?.token?.symbol) {
newState.wallet?.amounts?.get(AmountType.Token)?.value?.toFiatString(rate)
} else {
null
newState.currencyData.token?.fiatAmount
}
newState = newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = fiatAmount,
token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount)
))
}
is WalletAction.LoadArtwork -> {
newState = newState.copy(cardImage = null)
}
// is WalletAction.LoadArtwork -> {
// newState = newState.copy(cardImage = null)
// }
is WalletAction.LoadArtwork.Success -> {
newState = newState.copy(cardImage = action.artwork.toBitmap())
newState = newState.copy(cardImage = Artwork(action.artworkId, action.artwork.toBitmap()))
}
is WalletAction.ShowQrCode -> {
newState = newState.copy(qrCode = newState.wallet?.shareUrl?.toQrCode())
newState = newState.copy(qrCode = newState.addressData?.shareUrl?.toQrCode())
}
is WalletAction.HideQrCode -> {
newState = newState.copy(qrCode = null)

View file

@ -8,8 +8,9 @@ import org.rekotlin.StateType
data class WalletState(
val state: ProgressState = ProgressState.Done,
val cardImage: Bitmap? = null,
val cardImage: Artwork? = null,
val wallet: Wallet? = null,
val addressData: AddressData? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val qrCode: Bitmap? = null,
@ -32,4 +33,15 @@ enum class CreatingPayIdState { EnterPayId, Waiting }
sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class SendButton(enabled: Boolean) : WalletMainButton(enabled)
class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled)
}
}
data class AddressData(
val address: String,
val shareUrl: String,
val exploreUrl: String
)
data class Artwork(
val artworkId: String,
val artwork: Bitmap
)

View file

@ -55,7 +55,8 @@ class BalanceWidget(
fragment.l_balance.show()
fragment.l_balance_error.hide()
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = "-"
fragment.tv_currency_symbol.text = "-"
fragment.tv_amount.text = ""
fragment.tv_fiat_amount.hide()
showStatus(R.id.tv_status_loading)
fragment.l_token.hide()
@ -65,6 +66,7 @@ class BalanceWidget(
fragment.l_balance_error.hide()
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = data.amount
fragment.tv_currency_symbol.text = data.currencySymbol
fragment.tv_fiat_amount.show()
fragment.tv_fiat_amount.text = data.fiatAmount
showStatus(R.id.tv_status_verified)

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.wallet.ui
import android.graphics.Bitmap
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
@ -18,6 +20,7 @@ import kotlinx.android.synthetic.main.fragment_wallet.*
import kotlinx.android.synthetic.main.layout_address.*
import org.rekotlin.StoreSubscriber
class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<WalletState> {
private var qrDialog: QrDialog? = null
@ -30,6 +33,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
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() {
@ -62,59 +68,26 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null) return
if (state.wallet?.address != null) {
l_address?.show()
tv_address.text = state.wallet.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
} else {
l_address?.hide()
srl_wallet.setOnRefreshListener {
store.dispatch(WalletAction.LoadData)
}
if (state.cardImage != null) {
iv_card.setImageBitmap(state.cardImage)
} else {
iv_card.setImageDrawable(getDrawable(R.drawable.card_default))
}
setupButtons(state)
setupAddressCard(state)
setupCardImage(state.cardImage?.artwork)
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
}
btn_main.text = getString(buttonTitle)
btn_main.isEnabled = state.mainButton.enabled
btn_main.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
if (state.qrCode != null && state.wallet?.shareUrl != null) {
qrDialog = QrDialog(requireContext())
qrDialog?.showQr(state.qrCode, state.wallet.shareUrl)
if (state.qrCode != null && state.addressData?.shareUrl != null) {
if (qrDialog == null) qrDialog = QrDialog(requireContext())
qrDialog?.showQr(state.qrCode, state.addressData.shareUrl)
} else {
qrDialog?.dismiss()
}
when (state.state) {
ProgressState.Loading -> {
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
}
ProgressState.Done -> {
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
}
ProgressState.Error -> {
l_balance.show()
BalanceWidget(this, state.currencyData).setup()
}
if (state.state == ProgressState.Done) {
srl_wallet.isRefreshing = false
}
when (state.payIdData.payIdState) {
@ -150,5 +123,43 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
private fun setupButtons(state: WalletState) {
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
}
btn_main.text = getString(buttonTitle)
btn_main.isEnabled = state.mainButton.enabled
btn_main.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
}
private fun setupAddressCard(state: WalletState) {
if (state.addressData != null) {
l_address?.show()
tv_address.text = state.addressData.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
} else {
l_address?.hide()
}
}
private fun setupCardImage(cardImage: Bitmap?) {
if (cardImage != null) {
iv_card.setImageBitmap(cardImage)
} else {
iv_card.setImageDrawable(getDrawable(R.drawable.card_default))
}
}
}