Updated on 2026-08-14

This commit is contained in:
Tangem 2020-11-06 15:06:14 +03:00
parent 054b7c88d3
commit 434a8ac74f
26 changed files with 506 additions and 207 deletions

View file

@ -82,6 +82,9 @@ dependencies {
}
}
// WebView
implementation "androidx.browser:browser:1.2.0"
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0"

View file

@ -3,4 +3,6 @@ package com.tangem.tap
object TapConfig {
const val usePayId: Boolean = true
const val coinMarketCapKey = "f6622117-c043-47a0-8975-9d673ce484de"
const val moonPayApiKey = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE"
const val moonPayApiSecretKey = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C"
}

View file

@ -11,6 +11,7 @@ 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.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.topup.TopUpFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
@ -39,6 +40,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.TopUp -> TopUpFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsConfirm -> DetailsConfirmFragment()

View file

@ -12,8 +12,8 @@ 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.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.walletMiddleware
import org.rekotlin.Middleware
import org.rekotlin.StateType
@ -31,7 +31,9 @@ data class AppState(
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware,
HomeMiddleware().homeMiddleware, walletMiddleware, sendMiddleware,
HomeMiddleware().homeMiddleware,
WalletMiddleware().walletMiddleware,
sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware
)

View file

@ -9,4 +9,4 @@ data class NavigationState(
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer }
enum class AppScreen { Home, Wallet, TopUp, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer }

View file

@ -56,7 +56,10 @@ class TapWalletManager {
}
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> store.dispatch(WalletAction.UpdateWallet.Success(result.data))
is Result.Success ->
store.dispatch(WalletAction.UpdateWallet.Success(
result.data, tapWorkarounds?.isStart2Coin != true
))
is Result.Failure ->
store.dispatch(WalletAction.UpdateWallet.Failure(result.error?.localizedMessage))
}

View file

@ -4,7 +4,7 @@ import com.tangem.commands.Card
class TapWorkarounds(val card: Card) {
private val isStart2Coin: Boolean = card.cardData?.issuerName == START_2_COIN_ISSUER
val isStart2Coin: Boolean = card.cardData?.issuerName == START_2_COIN_ISSUER
fun isPayIdCreationEnabled(): Boolean {
if (isStart2Coin) return false

View file

@ -0,0 +1,44 @@
package com.tangem.tap.domain
import android.net.Uri
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import org.spongycastle.util.encoders.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TopUpHelper {
companion object {
const val REDIRECT_URL = "https://success.tangem.com"
private const val BASE_URL = "https://buy-staging.moonpay.io"
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
private const val REDIRECT_URL_PATH = "&redirectUrl="
private const val SIGNATURE_PATH = "&signature="
fun getUrl(cryptoCurrencyName: CryptoCurrencyName, walletAddress: String): String {
val originalQuery = API_KEY_PATH + TapConfig.moonPayApiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL.urlEncode()
val signature = createSignature(originalQuery, TapConfig.moonPayApiSecretKey)
return BASE_URL + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
return Base64.toBase64String(sha256Hmac.doFinal(data.toByteArray()))
}
}
}

View file

@ -35,7 +35,7 @@ sealed class WalletAction : Action {
object UpdateWallet : WalletAction() {
object ScheduleUpdatingWallet : WalletAction()
data class Success(val wallet: Wallet) : WalletAction()
data class Success(val wallet: Wallet, val topUpAllowed: Boolean) : WalletAction()
data class Failure(val errorMessage: String? = null) : WalletAction()
}
@ -85,4 +85,10 @@ sealed class WalletAction : Action {
data class ExploreAddress(val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
sealed class TopUpAction : WalletAction() {
data class TopUp(val context: Context, val toolbarColor: Int?) : TopUpAction()
data class Start(val url: String, val redirectUrl: String) : TopUpAction()
data class Finish(val topUpCompleted: Boolean) : TopUpAction()
}
}

View file

@ -18,6 +18,7 @@ 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.extensions.toSendableAmounts
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.models.toPendingTransactions
@ -29,211 +30,239 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
import timber.log.Timber
class WalletMiddleware {
private val topUpMiddleware = TopUpMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (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 -> {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is WalletAction.TopUpAction -> topUpMiddleware.handle(action)
is WalletAction.LoadWallet -> {
scope.launch {
store.state.globalState.tapWalletManager.loadWalletData()
}
}
}
is WalletAction.UpdateWallet -> {
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
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 -> {
scope.launch {
val result = tangemSdkManager.createWallet(
store.state.globalState.scanNoteResponse?.card?.cardId
)
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))
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
}
}
}
is WalletAction.UpdateWallet -> {
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(analytics)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
is WalletAction.Scan -> {
scope.launch {
val result = tangemSdkManager.scanNote(analytics)
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager
.onCardScanned(result.data, true)
}
}
}
}
}
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)
is WalletAction.LoadData -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(scanNoteResponse)
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.tapWalletManager.loadData(it)
}
}
}
}
is WalletAction.CopyAddress -> {
store.state.walletState.addressData?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
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.addressData?.address?.let {
action.context.copyToClipboard(it)
store.dispatch(WalletAction.CopyAddress.Success)
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.addressData?.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 card = store.state.globalState.scanNoteResponse?.card
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
if (card != null && !preferencesStorage.wasCardScannedBefore(card.cardId)) {
val result = checkIfWarningNeeded(card, validator)
if (result != null) store.dispatch(WalletAction.ShowWarning(result))
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(store.state.walletState.addressData?.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 card = store.state.globalState.scanNoteResponse?.card
val validator = store.state.globalState.scanNoteResponse?.walletManager
as? SignatureCountValidator
if (card != null && !preferencesStorage.wasCardScannedBefore(card.cardId)) {
val result = checkIfWarningNeeded(card, validator)
if (result != null) store.dispatch(WalletAction.ShowWarning(result))
}
}
is WalletAction.CheckHashesCountOnline -> checkHashesCountOnline()
is WalletAction.SaveCardId -> {
val cardId = store.state.globalState.scanNoteResponse?.card?.cardId
cardId?.let { preferencesStorage.saveScannedCardId(it) }
}
}
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)
next(action)
}
}
}
}
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type == 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 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 checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null
): WarningType? {
if (card.getType() != CardType.Release) {
return WarningType.DevCard
private fun prepareSendAction(amount: Amount?): Action {
return if (amount != null) {
if (amount.type == 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)
}
}
}
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningType.CardSignedHashesBefore
private fun checkIfWarningNeeded(
card: Card, signatureCountValidator: SignatureCountValidator? = null
): WarningType? {
if (card.getType() != CardType.Release) {
return WarningType.DevCard
}
return if (signatureCountValidator == null) {
if (card.walletSignedHashes ?: 0 > 0) {
WarningType.CardSignedHashesBefore
} else {
store.dispatch(WalletAction.SaveCardId)
null
}
} else {
store.dispatch(WalletAction.SaveCardId)
store.dispatch(WalletAction.NeedToCheckHashesCountOnline)
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
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) {
store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore))
} else if (card.walletSignedHashes ?: 0 > 0) {
store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore))
}
}
}
}
}
}
private fun checkHashesCountOnline() {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
private class TopUpMiddleware {
fun handle(action: WalletAction.TopUpAction) {
when (action) {
is WalletAction.TopUpAction.TopUp -> {
val url = TopUpHelper.getUrl(
store.state.walletState.currencyData.currencySymbol!!,
store.state.walletState.addressData!!.address
)
Timber.d(url)
store.dispatch(WalletAction.TopUpAction.Start(url, TopUpHelper.REDIRECT_URL))
store.dispatch(NavigationAction.NavigateTo(AppScreen.TopUp))
}
is WalletAction.TopUpAction.Start -> {
val card = store.state.globalState.scanNoteResponse?.card
if (card == null || preferencesStorage.wasCardScannedBefore(card.cardId)) 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) {
store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore))
} else if (card.walletSignedHashes ?: 0 > 0) {
store.dispatch(WalletAction.ShowWarning(WarningType.CardSignedHashesBefore))
}
}
is WalletAction.TopUpAction.Finish -> {
if (action.topUpCompleted) store.dispatch(WalletAction.UpdateWallet)
store.dispatch(NavigationAction.PopBackTo())
}
}
}

View file

@ -88,6 +88,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
wallet?.blockchain?.fullName,
currencySymbol = wallet?.blockchain?.currency,
token = wallet?.token?.symbol?.let {
TokenData("", tokenSymbol = it)
}
@ -210,11 +211,25 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = newState.copy(hashesCountVerified = false)
is WalletAction.ConfirmHashesCount ->
newState = newState.copy(hashesCountVerified = true)
is WalletAction.TopUpAction -> {
newState = newState.copy(topUpState = handleTopUpActions(action, newState.topUpState))
}
}
return newState
}
private fun onWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
private fun handleTopUpActions(action: WalletAction.TopUpAction, state: TopUpState): TopUpState {
return when (action) {
is WalletAction.TopUpAction.TopUp -> state
is WalletAction.TopUpAction.Start ->
state.copy(url = action.url, redirectUrl = action.redirectUrl)
is WalletAction.TopUpAction.Finish -> state.copy(url = null)
}
}
private fun onWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val token = wallet.amounts[AmountType.Token]
val tokenData = if (token != null) {
@ -242,6 +257,7 @@ private fun onWalletLoaded(wallet: Wallet, walletState: WalletState): WalletStat
} else {
BalanceStatus.VerifiedOnline
}
val topUpState = topUpAllowed?.let { TopUpState(topUpAllowed) } ?: walletState.topUpState
return walletState.copy(
state = ProgressState.Done, wallet = wallet,
currencyData = BalanceWidgetData(
@ -252,6 +268,7 @@ private fun onWalletLoaded(wallet: Wallet, walletState: WalletState): WalletStat
fiatAmount = fiatAmount
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
topUpState = topUpState
)
}

View file

@ -21,7 +21,8 @@ data class WalletState(
val payIdData: PayIdData = PayIdData(),
val walletDialog: WalletDialog? = null,
val updatingWallet: Boolean = false,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false)
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val topUpState: TopUpState = TopUpState()
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
@ -76,4 +77,10 @@ data class Artwork(
const val SERGIO_CARD_ID = "BC01"
const val MARTA_CARD_ID = "BC02"
}
}
}
data class TopUpState(
val allowed: Boolean = true,
val url: String? = null,
val redirectUrl: String? = null
)

View file

@ -170,6 +170,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
btn_confirm.text = getString(buttonTitle)
btn_confirm.isEnabled = state.mainButton.enabled
btn_top_up.isEnabled = state.topUpState.allowed
btn_confirm.setOnClickListener {
when (state.mainButton) {
@ -177,6 +178,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
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) {

View file

@ -0,0 +1,63 @@
package com.tangem.tap.features.wallet.ui.topup
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.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_topup.*
import org.rekotlin.StoreSubscriber
class TopUpFragment : Fragment(R.layout.fragment_topup), StoreSubscriber<WalletState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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.walletState == newState.walletState
}.select { it.walletState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
web_view.settings.javaScriptEnabled = true
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: WalletState) {
if (activity == null) return
if (state.topUpState.url != null) {
web_view.webViewClient = TopUpWebViewClient(progress_bar, state.topUpState.redirectUrl)
web_view.loadUrl(state.topUpState.url)
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.features.wallet.ui.topup
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.ProgressBar
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.store
class TopUpWebViewClient(
private val progressBar: ProgressBar, private val redirectUrl: String?
) : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (redirectUrl != null && url.contains(redirectUrl)) {
store.dispatch(WalletAction.TopUpAction.Finish(true))
return true
}
view.loadUrl(url)
return true
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
progressBar.hide()
}
init {
progressBar.show()
}
}

View file

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="10"
android:viewportHeight="12">
<group
android:scaleX="0.8"
android:pivotX="5.3"
android:pivotY="6"
android:rotation="180"
>
<path
android:pathData="M5.853,11.6645L9.7814,8.075C10.0495,7.8439 10.0734,7.3871 9.84,7.1191C9.6067,6.8511 9.1615,6.824 8.9084,7.0724L6.0712,9.6665L6.0712,0.8398C6.0712,0.468 5.7781,0.1667 5.4165,0.1667C5.0549,0.1667 4.7618,0.468 4.7618,0.8398L4.7618,9.6665L1.9246,7.0724C1.6715,6.824 1.2271,6.8576 0.9937,7.1256C0.7604,7.3936 0.7835,7.8439 1.0516,8.075L4.98,11.6645C5.2976,11.8989 5.5827,11.8802 5.853,11.6645L5.853,11.6645Z"
android:fillColor="#1C1C1E"
/>
</group>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="19dp"
android:viewportWidth="24"
android:viewportHeight="19">
<path
android:pathData="M11.4765,5.7026L6.7623,10.0101C6.4406,10.2874 6.412,10.8356 6.692,11.1572C6.972,11.4788 7.5063,11.5112 7.8099,11.2131L11.2146,8.1003L11.2146,18.6923C11.2146,19.1384 11.5664,19.5 12.0003,19.5C12.4342,19.5 12.7859,19.1384 12.7859,18.6923L12.7859,8.1003L16.1906,11.2131C16.4942,11.5112 17.0275,11.4709 17.3076,11.1493C17.5876,10.8277 17.5599,10.2874 17.2382,10.0101L12.524,5.7026C12.1429,5.4214 11.8008,5.4438 11.4765,5.7026L11.4765,5.7026Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -1,15 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="13dp"
android:height="18dp"
android:viewportWidth="13"
android:viewportHeight="18">
android:width="24dp"
android:height="21dp"
android:viewportWidth="24"
android:viewportHeight="21"
>
<path
android:pathData="M11.7908,6.0129C10.9896,3.7531 9.3666,1.9247 7.1993,0.8873C5.032,-0.1502 2.5873,-0.2837 0.3275,0.5175L0.2762,0.538C0.1734,0.5791 0.081,0.651 0.0399,0.764C-0.0114,0.8667 -0.0114,0.99 0.0296,1.0927C0.1221,1.329 0.3789,1.442 0.6254,1.3598C2.6695,0.6408 4.8676,0.764 6.809,1.6988C8.7606,2.6335 10.2295,4.277 10.9485,6.3108C11.2566,7.1839 11.4107,8.0878 11.4107,9.002C11.4107,9.9162 11.2566,10.8201 10.9485,11.6932C10.2295,13.7372 8.7606,15.3704 6.809,16.3052C4.8573,17.2399 2.6592,17.3632 0.6151,16.6441C0.3789,16.562 0.1221,16.6749 0.0296,16.9112C-0.0114,17.0242 -0.0114,17.1372 0.0399,17.2399C0.0913,17.3426 0.1734,17.4248 0.2864,17.4659L0.3378,17.4864C1.3136,17.8254 2.3202,18 3.3269,18C4.6519,18 5.977,17.7021 7.2096,17.1166C9.3769,16.0792 11.0101,14.2611 11.801,11.991C12.14,11.0255 12.3146,10.0189 12.3146,9.0122C12.3044,7.9851 12.1297,6.9784 11.7908,6.0129Z"
android:pathData="M17.7908,9.0129C16.9896,6.7531 15.3666,4.9247 13.1993,3.8873C11.032,2.8498 8.5873,2.7163 6.3275,3.5175L6.2762,3.538C6.1734,3.5791 6.081,3.651 6.0399,3.764C5.9886,3.8667 5.9886,3.99 6.0296,4.0927C6.1221,4.329 6.3789,4.442 6.6254,4.3598C8.6695,3.6408 10.8676,3.764 12.809,4.6988C14.7606,5.6335 16.2295,7.277 16.9485,9.3108C17.2566,10.1839 17.4107,11.0878 17.4107,12.002C17.4107,12.9161 17.2566,13.8201 16.9485,14.6932C16.2295,16.7372 14.7606,18.3704 12.809,19.3052C10.8574,20.2399 8.6592,20.3632 6.6151,19.6441C6.3789,19.562 6.1221,19.6749 6.0296,19.9112C5.9886,20.0242 5.9886,20.1372 6.0399,20.2399C6.0913,20.3426 6.1734,20.4248 6.2864,20.4659L6.3378,20.4864C7.3136,20.8254 8.3202,21 9.3269,21C10.6519,21 11.977,20.7021 13.2096,20.1166C15.3769,19.0792 17.0101,17.2611 17.801,14.991C18.14,14.0255 18.3146,13.0189 18.3146,12.0122C18.3044,10.9851 18.1297,9.9784 17.7908,9.0129Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M1.1703,14.0252C1.1183,14.1498 1.1287,14.2848 1.1806,14.4095C1.2326,14.5341 1.3364,14.6172 1.4715,14.6588C2.0532,14.8457 2.6556,14.9392 3.2581,14.9392C3.9644,14.9392 4.6707,14.8146 5.3251,14.5549C6.7689,14.0044 7.9115,12.9137 8.5347,11.5011C8.8671,10.7532 9.0333,9.9742 9.0333,9.1744V9.0082C9.0126,8.2499 8.8464,7.502 8.5347,6.7957C7.9011,5.383 6.7689,4.3028 5.3251,3.7419C4.0787,3.2641 2.6972,3.2329 1.4299,3.6484C1.3157,3.6899 1.2118,3.773 1.1599,3.8873C1.1079,4.0016 1.1079,4.1262 1.1495,4.2405C1.2326,4.4586 1.4819,4.5729 1.7104,4.5001C2.7803,4.147 3.954,4.1677 5.0031,4.5729C6.2184,5.0403 7.1844,5.9543 7.7142,7.1489C7.9946,7.7721 8.1296,8.4265 8.14,9.1017V9.1328C8.14,9.8184 7.9946,10.4831 7.7142,11.1167C7.1844,12.3113 6.2184,13.2253 5.0031,13.6928C3.9436,14.0979 2.7699,14.1186 1.6896,13.7551C1.4819,13.7135 1.243,13.8174 1.1703,14.0252Z"
android:pathData="M7.1702,17.0252C7.1183,17.1498 7.1287,17.2848 7.1806,17.4095C7.2326,17.5341 7.3365,17.6172 7.4715,17.6588C8.0532,17.8457 8.6556,17.9392 9.2581,17.9392C9.9644,17.9392 10.6707,17.8146 11.3251,17.5549C12.7689,17.0044 13.9115,15.9137 14.5347,14.5011C14.8671,13.7532 15.0333,12.9742 15.0333,12.1744V12.0082C15.0126,11.2499 14.8464,10.502 14.5347,9.7957C13.9011,8.383 12.7689,7.3028 11.3251,6.7419C10.0787,6.2641 8.6972,6.2329 7.4299,6.6484C7.3157,6.6899 7.2118,6.773 7.1599,6.8873C7.1079,7.0016 7.1079,7.1262 7.1495,7.2405C7.2326,7.4586 7.4819,7.5729 7.7104,7.5001C8.7803,7.147 9.954,7.1677 11.0031,7.5729C12.2184,8.0403 13.1844,8.9543 13.7142,10.1489C13.9946,10.7721 14.1296,11.4265 14.14,12.1017V12.1328C14.14,12.8184 13.9946,13.4831 13.7142,14.1167C13.1844,15.3113 12.2184,16.2253 11.0031,16.6928C9.9436,17.0979 8.7699,17.1186 7.6896,16.7551C7.4819,16.7135 7.243,16.8174 7.1702,17.0252Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M3.9148,10.5778C3.6136,10.692 3.2812,10.7128 2.9592,10.6297C2.7618,10.5778 2.5437,10.6816 2.471,10.879V10.8894C2.419,11.014 2.4294,11.1594 2.5021,11.2841C2.5645,11.4087 2.6891,11.5022 2.8345,11.5334C3.0111,11.5749 3.1773,11.5853 3.3539,11.5853C3.6551,11.5853 3.9563,11.5334 4.2472,11.4191C5.2028,11.0556 5.8468,10.1207 5.8468,9.0924C5.8468,8.0641 5.2028,7.1396 4.2472,6.7657C3.7694,6.5787 3.25,6.5476 2.741,6.6722C2.6268,6.7034 2.5229,6.7761 2.471,6.8903C2.419,6.9942 2.4086,7.1188 2.4502,7.2331L2.4606,7.2539C2.5437,7.472 2.7722,7.5862 3.0007,7.5447C3.3019,7.472 3.624,7.4928 3.9148,7.607C4.5276,7.8356 4.9327,8.438 4.9327,9.0924C4.9327,9.7468 4.5276,10.3492 3.9148,10.5778Z"
android:pathData="M9.9148,13.5778C9.6136,13.692 9.2812,13.7128 8.9592,13.6297C8.7619,13.5778 8.5437,13.6816 8.471,13.879V13.8894C8.4191,14.014 8.4295,14.1594 8.5022,14.2841C8.5645,14.4087 8.6892,14.5022 8.8346,14.5334C9.0112,14.5749 9.1774,14.5853 9.3539,14.5853C9.6552,14.5853 9.9564,14.5334 10.2472,14.4191C11.2029,14.0556 11.8469,13.1207 11.8469,12.0924C11.8469,11.0641 11.2029,10.1396 10.2472,9.7657C9.7694,9.5787 9.2501,9.5476 8.7411,9.6722C8.6268,9.7034 8.523,9.7761 8.471,9.8903C8.4191,9.9942 8.4087,10.1188 8.4503,10.2331L8.4607,10.2539C8.5437,10.472 8.7723,10.5863 9.0008,10.5447C9.302,10.472 9.624,10.4928 9.9148,10.607C10.5277,10.8356 10.9328,11.438 10.9328,12.0924C10.9328,12.7468 10.5277,13.3492 9.9148,13.5778Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="19dp"
android:viewportWidth="24"
android:viewportHeight="19">
<path
android:pathData="M18.7974,10.9765L14.4899,6.2623C14.2126,5.9406 13.6644,5.912 13.3428,6.192C13.0212,6.472 12.9888,7.0063 13.2869,7.3099L16.3997,10.7146L5.8077,10.7146C5.3616,10.7146 5,11.0664 5,11.5003C5,11.9342 5.3616,12.2859 5.8077,12.2859L16.3997,12.2859L13.2869,15.6906C12.9888,15.9942 13.0291,16.5275 13.3507,16.8076C13.6723,17.0876 14.2126,17.0599 14.4899,16.7382L18.7974,12.024C19.0786,11.6429 19.0562,11.3008 18.7974,10.9765L18.7974,10.9765Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -63,7 +63,6 @@
android:id="@+id/btn_shop"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginBottom="33dp"
android:text="@string/common_no"
@ -85,7 +84,6 @@
android:id="@+id/btn_yes"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp"

View file

@ -104,7 +104,6 @@
android:id="@+id/btnSend"
style="@style/TapButtonWithIcon"
android:layout_width="match_parent"
android:layout_height="48dp"
android:fontFamily="@font/saira_semi_condensed_regular"
android:text="@string/send_title"
app:icon="@drawable/ic_arrow_right" />

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="@drawable/ic_close"
app:title="@string/wallet_button_topup" />
<WebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
<ProgressBar
android:id="@+id/progress_bar"
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminateOnly="true"
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -49,7 +49,6 @@
android:id="@+id/iv_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="215dp"
android:layout_gravity="center"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
@ -57,6 +56,7 @@
android:layout_marginBottom="12dp"
android:adjustViewBounds="true"
android:elevation="3dp"
android:maxHeight="215dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder"
app:layout_constraintTop_toTopOf="parent" />
@ -90,40 +90,49 @@
android:id="@+id/btn_scan"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:text="@string/wallet_button_scan"
app:icon="@drawable/ic_scan"
android:gravity="bottom|center_horizontal"
android:drawableTop="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintEnd_toStartOf="@id/btn_top_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_top_up"
style="@style/TapButton"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="30dp"
android:text="@string/wallet_button_topup"
android:drawableTop="@drawable/ic_arrow_up_short_btn"
app:drawableTint="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
app:layout_constraintEnd_toStartOf="@+id/btn_confirm"
app:layout_constraintStart_toEndOf="@+id/btn_scan"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1"
android:gravity="bottom|center_horizontal"
/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_confirm"
style="@style/TapButtonWithIcon"
style="@style/TapButton"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/wallet_button_send"
app:icon="@drawable/ic_send"
android:drawableTop="@drawable/ic_send_short_btn"
android:gravity="bottom|center_horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
app:layout_constraintStart_toEndOf="@+id/btn_top_up"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />

View file

@ -3,5 +3,6 @@
<string name="xtz_withdrawal_message_warning" translatable="false">To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ</string>
<string name="xtz_withdrawal_message_reduce" translatable="false">Reduce by %s XTZ</string>
<string name="xtz_withdrawal_message_ignore" translatable="false">No, send all</string>
<string name="wallet_button_topup" translatable="false">Top up</string>
</resources>

View file

@ -36,7 +36,7 @@
<item name="cornerRadius">@dimen/btn_corner_radius</item>
<item name="android:fontFamily">@font/saira_semi_condensed_regular</item>
<item name="android:layout_width">93dp</item>
<item name="android:layout_height">48dp</item>
<item name="android:layout_height">52dp</item>
<item name="elevation">3dp</item>
</style>