Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-18 13:34:31 +03:00
parent 01e4ad8abb
commit b840d3c5eb
33 changed files with 404 additions and 155 deletions

View file

@ -16,6 +16,7 @@ import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineExceptionHandler
@ -67,9 +68,8 @@ class MainActivity : AppCompatActivity() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
if (supportFragmentManager.backStackEntryCount == 0) {
store.dispatch(
NavigationAction.NavigateTo(AppScreen.Home)
)
store.dispatch(HomeAction.CheckIfFirstLaunch)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
}

View file

@ -19,11 +19,10 @@ lateinit var preferencesStorage: PreferencesStorage
class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
preferencesStorage = PreferencesStorage(this)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
}
}

View file

@ -48,6 +48,10 @@ fun BigDecimal.toFormattedString(decimals: Int): String {
return df.format(bd)
}
fun BigDecimal.toFormattedCurrencyString(decimals: Int, currency: String): String {
return "${this.toFormattedString(decimals)} $currency"
}
fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrencyName): String? {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.DOWN)

View file

@ -2,8 +2,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.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.wallet.redux.WalletReducer
import org.rekotlin.Action
@ -14,6 +15,7 @@ fun appReducer(action: Action, state: AppState?): AppState {
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state)

View file

@ -6,6 +6,7 @@ import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.home.redux.homeMiddleware
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
@ -17,6 +18,7 @@ import org.rekotlin.StateType
data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState()

View file

@ -12,6 +12,7 @@ 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.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.PayIdState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -43,8 +44,8 @@ class TapWalletManager {
when (result) {
is Result.Success -> {
store.dispatch(WalletAction.LoadArtwork.Success(
artworkId, result.data.byteStream().readBytes())
)
Artwork(artworkId, result.data.byteStream().readBytes())
))
}
is Result.Failure -> store.dispatch(WalletAction.LoadArtwork.Failure)
}
@ -74,21 +75,22 @@ class TapWalletManager {
suspend fun onCardScanned(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
val artworkId = data.verifyResponse?.artworkInfo?.id
if (data.walletManager != null) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
data.verifyResponse?.artworkInfo?.id?.let {
store.dispatch(WalletAction.LoadArtwork(data.card, it))
}
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else if (data.card.status == CardStatus.Empty) {
store.dispatch(WalletAction.EmptyWallet)
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
} else {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
store.dispatch(WalletAction.LoadArtwork(data.card, artworkId))
}
}
}

View file

@ -2,24 +2,16 @@ package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.ReadCommand
import com.tangem.commands.CreateWalletCommand
import com.tangem.common.CompletionResult
import com.tangem.tasks.CreateWalletTask
class CreateWalletAndRescanTask : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
CreateWalletTask().run(session) { result ->
CreateWalletCommand().run(session) { result ->
when (result) {
is CompletionResult.Success -> ReadCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
callback(readResult.data.toScanNoteCompletionResult())
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
is CompletionResult.Success -> ScanNoteTask().run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}

View file

@ -10,6 +10,7 @@ import com.tangem.commands.CommandResponse
import com.tangem.commands.verifycard.VerifyCardCommand
import com.tangem.commands.verifycard.VerifyCardResponse
import com.tangem.common.CompletionResult
import com.tangem.tasks.ScanTask
data class ScanNoteResponse(
val walletManager: WalletManager?,
@ -21,26 +22,32 @@ class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
val card = session.environment.card
card ?: return callback(CompletionResult.Failure(TangemSdkError.CardError()))
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
VerifyCardCommand(true).run(session) { result ->
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, result.data
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
val card = result.data
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
return@run callback(CompletionResult.Success(ScanNoteResponse(null, card)))
}
VerifyCardCommand(true).run(session) { verifyResult ->
when (verifyResult) {
is CompletionResult.Success -> {
callback(CompletionResult.Success(ScanNoteResponse(
walletManager, card, verifyResult.data
)))
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(TangemSdkError.VerificationFailed()))
}
}
}
}
}
}
}
}

View file

@ -5,11 +5,43 @@ 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.features.home.redux.HomeState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_home.*
import org.rekotlin.StoreSubscriber
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
}.select { it.homeState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: HomeState) {
if (activity == null) return
requireContext().cacheDir
if (state.firstLaunch) {
tv_home_description.text = getText(R.string.home_do_you_have_card)
btn_shop.text = getText(R.string.home_button_no_card)
btn_yes.text = getText(R.string.home_button_yes)
} else {
tv_home_description.text = getText(R.string.home_welcome_back)
btn_shop.text = getText(R.string.home_button_shop)
btn_yes.text = getText(R.string.home_button_scan)
}
}
class HomeFragment : Fragment(R.layout.fragment_home) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -6,4 +6,7 @@ import org.rekotlin.Action
sealed class HomeAction : Action {
object ReadCard : HomeAction()
data class GoToShop(val context: Context) : HomeAction()
object CheckIfFirstLaunch : HomeAction() {
data class Result(val firstLaunch: Boolean) : HomeAction()
}
}

View file

@ -8,6 +8,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -20,6 +21,11 @@ val homeMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is HomeAction.CheckIfFirstLaunch -> {
store.dispatch(
HomeAction.CheckIfFirstLaunch.Result(preferencesStorage.isFirstLaunch())
)
}
is HomeAction.ReadCard -> {
scope.launch {
val result = tangemSdkManager.scanNote()

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class HomeReducer {
companion object {
fun reduce(action: Action, state: AppState): HomeState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): HomeState {
if (action !is HomeAction) return state.homeState
var homeState = state.homeState
when (action) {
is HomeAction.CheckIfFirstLaunch.Result -> {
homeState = homeState.copy(firstLaunch = action.firstLaunch)
}
}
return homeState
}

View file

@ -0,0 +1,7 @@
package com.tangem.tap.features.home.redux
import org.rekotlin.StateType
data class HomeState(
val firstLaunch: Boolean = true
) : StateType

View file

@ -35,8 +35,8 @@ sealed class WalletAction : Action {
object Failure : WalletAction()
}
data class LoadArtwork(val card: Card, val artworkId: String) : WalletAction() {
data class Success(val artworkId: String, val artwork: ByteArray) : WalletAction()
data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() {
data class Success(val artwork: Artwork) : WalletAction()
object Failure : WalletAction()
}

View file

@ -19,6 +19,7 @@ import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -46,11 +47,19 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
}
is WalletAction.LoadArtwork -> {
scope.launch {
store.state.globalState.tapWalletManager.loadArtwork(
action.card, action.artworkId
)
if (action.artworkId != null) {
scope.launch {
store.state.globalState.tapWalletManager.loadArtwork(
action.card, action.artworkId
)
}
} else {
val artworkRes = R.drawable.card_default
store.dispatch(WalletAction.LoadArtwork.Success(Artwork(
artworkId = artworkRes.toString(), artworkResId = artworkRes
)))
}
}
is WalletAction.CreateWallet -> {
scope.launch {

View file

@ -2,8 +2,8 @@ package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toBitmap
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
@ -98,6 +98,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
null
}
val amount = action.wallet.amounts[AmountType.Coin]?.value
val formattedAmount = amount?.toFormattedCurrencyString(
action.wallet.blockchain.decimals(),
action.wallet.blockchain.currency)
val fiatRate = state.globalState.conversionRates.getRate(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
@ -110,7 +113,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencyData = BalanceWidgetData(
BalanceStatus.VerifiedOnline, action.wallet.blockchain.fullName,
currencySymbol = action.wallet.blockchain.currency,
amount?.toFormattedString(action.wallet.blockchain.decimals()),
formattedAmount,
token = tokenData,
fiatAmount = fiatAmount
),
@ -163,13 +166,14 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
))
}
is WalletAction.LoadArtwork.Success -> {
newState = newState.copy(cardImage = Artwork(action.artworkId, action.artwork.toBitmap()))
newState = newState.copy(cardImage = action.artwork)
}
is WalletAction.ShowQrCode -> {
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
newState.addressData?.shareUrl?.toQrCode(),
newState.addressData?.shareUrl
newState.addressData?.shareUrl,
newState.currencyData.currency
)
)
}

View file

@ -4,6 +4,8 @@ import android.graphics.Bitmap
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.extensions.toBitmap
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.StateType
@ -22,7 +24,10 @@ data class WalletState(
) : StateType
sealed class WalletDialog {
data class QrDialog(val qrCode: Bitmap?, val shareUrl: String?) : WalletDialog()
data class QrDialog(
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
) : WalletDialog()
data class CreatePayIdDialog(val creatingPayIdState: CreatingPayIdState?) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
}
@ -54,5 +59,8 @@ data class AddressData(
data class Artwork(
val artworkId: String,
val artwork: Bitmap
)
val artwork: Bitmap? = null,
val artworkResId: Int? = null
) {
constructor(artworkId: String, artworkBytes: ByteArray) : this(artworkId, artworkBytes.toBitmap())
}

View file

@ -52,7 +52,6 @@ class BalanceWidget(
fragment.l_token.hide()
fragment.tv_currency.text = data.currency
fragment.tv_currency_symbol.text = "-"
fragment.tv_amount.text = ""
showStatus(R.id.tv_status_loading)
@ -62,7 +61,6 @@ 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)
@ -83,7 +81,6 @@ class BalanceWidget(
fragment.tv_fiat_amount.hide()
fragment.tv_currency.text = data.currency
fragment.tv_currency_symbol.text = "-"
fragment.tv_amount.text = ""
fragment.tv_status_error_message.text = data.errorMessage

View file

@ -1,12 +1,13 @@
package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.graphics.Bitmap
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -35,6 +36,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private var dialog: Dialog? = null
private var snackbar: Snackbar? = null
private var artworkId: String? = null
private lateinit var viewAdapter: PendingTransactionsAdapter
@ -99,7 +101,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
setupButtons(state)
setupAddressCard(state)
setupCardImage(state.cardImage?.artwork)
setupCardImage(state.cardImage)
viewAdapter.submitList(state.pendingTransactions)
@ -179,20 +181,36 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
private fun setupCardImage(cardImage: Bitmap?) {
if (cardImage != null) {
iv_card.setImageBitmap(cardImage)
} else {
iv_card.setImageDrawable(getDrawable(R.drawable.card_default))
private fun setupCardImage(cardImage: Artwork?) {
if (cardImage?.artwork != null) {
if (cardImage.artworkId != this.artworkId) {
prepareShowingCardImage(cardImage.artworkId)
iv_card.setImageBitmap(cardImage.artwork)
}
} else if (cardImage?.artworkResId != null) {
if (cardImage.artworkId != this.artworkId) {
prepareShowingCardImage(cardImage.artworkId)
iv_card.setImageDrawable(getDrawable(cardImage.artworkResId))
}
}
}
private fun prepareShowingCardImage(artworkId: String) {
this.artworkId = artworkId
val fadeInAnimation: Animation = AlphaAnimation(0.0f, 1.0f)
fadeInAnimation.duration = 400
iv_card.startAnimation(fadeInAnimation)
iv_card_tangem_logo.hide()
}
private fun handleDialogs(walletDialog: WalletDialog?) {
when (walletDialog) {
is WalletDialog.QrDialog -> {
if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
if (dialog == null) dialog = QrDialog(requireContext()).apply {
this.showQr(walletDialog.qrCode, walletDialog.shareUrl)
this.showQr(
walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
)
}
}
}

View file

@ -3,6 +3,9 @@ package com.tangem.tap.features.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
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
@ -14,12 +17,20 @@ class QrDialog(context: Context) : Dialog(context) {
this.setContentView(R.layout.dialog_qrcode)
}
fun showQr(qrCode: Bitmap, shareUrl: String) {
fun showQr(qrCode: Bitmap, shareUrl: String, currencyName: CryptoCurrencyName?) {
this.setOnDismissListener { store.dispatch(WalletAction.HideQrCode) }
this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideQrCode) }
this.tv_qr_dialog_address?.text = shareUrl
this.iv_qrcode?.setImageBitmap(qrCode)
if (currencyName == null) {
this.tv_qr_dialog_header.hide()
} else {
this.tv_qr_dialog_header.show()
this.tv_qr_dialog_header.text = context.getString(
R.string.wallet_dialog_qr_code_header, currencyName
)
}
super.show()
}
}

View file

@ -45,10 +45,18 @@ class PreferencesStorage(applicationContext: Application) {
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
fun isFirstLaunch(): Boolean {
val isFirst = preferences.contains(FIRST_LAUNCH_CHECK_KEY)
if (isFirst) preferences.edit().putInt(FIRST_LAUNCH_CHECK_KEY, System.currentTimeMillis().toInt()).apply()
return isFirst
}
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
private const val FIRST_LAUNCH_CHECK_KEY = "firstLaunchCheck"
}
}