diff --git a/app/build.gradle b/app/build.gradle index 1b06c221fa..5d7b82f929 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -74,6 +74,9 @@ dependencies { implementation 'com.tangem:core:1.79.0' implementation 'com.tangem:sdk:1.79.0' + // 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" diff --git a/app/src/main/java/com/tangem/tap/TapConfig.kt b/app/src/main/java/com/tangem/tap/TapConfig.kt index 483f9e8306..9470f2935d 100644 --- a/app/src/main/java/com/tangem/tap/TapConfig.kt +++ b/app/src/main/java/com/tangem/tap/TapConfig.kt @@ -2,5 +2,8 @@ package com.tangem.tap object TapConfig { const val usePayId: Boolean = true + const val useTopUp: Boolean = true const val coinMarketCapKey = "f6622117-c043-47a0-8975-9d673ce484de" + const val moonPayApiKey = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE" + const val moonPayApiSecretKey = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C" } \ 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 bb69887342..588342e8f7 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 @@ -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() 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 5f23825839..e60ae97fa7 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,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> { return listOf( logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware, - HomeMiddleware().homeMiddleware, walletMiddleware, sendMiddleware, + HomeMiddleware().homeMiddleware, + WalletMiddleware().walletMiddleware, + sendMiddleware, DetailsMiddleware().detailsMiddleware, DisclaimerMiddleware().disclaimerMiddleware ) 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 5edfad540b..33d2b18a16 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 @@ -9,4 +9,4 @@ data class NavigationState( val activity: WeakReference? = null ) : StateType -enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer } \ No newline at end of file +enum class AppScreen { Home, Wallet, TopUp, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer } \ 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 99c1864c96..47ea2833f3 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -56,7 +56,8 @@ 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)) is Result.Failure -> store.dispatch(WalletAction.UpdateWallet.Failure(result.error?.localizedMessage)) } @@ -100,7 +101,10 @@ class TapWalletManager { store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection)) return@withContext } - store.dispatch(WalletAction.LoadWallet) + store.dispatch(WalletAction.LoadWallet( + data.walletManager.wallet, data.verifyResponse?.artworkInfo?.id, + tapWorkarounds?.isStart2Coin != true && TapConfig.useTopUp + )) store.dispatch(WalletAction.LoadArtwork(data.card, artworkId)) store.dispatch(WalletAction.LoadFiatRate) store.dispatch(WalletAction.LoadPayId) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index 7764650537..9dc11ee062 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt new file mode 100644 index 0000000000..6f715de06b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/TopUpHelper.kt @@ -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 = "¤cyCode=" + 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())) + } + } +} \ 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 1a573776e6..5ac957f1f4 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 @@ -20,7 +20,9 @@ sealed class WalletAction : Action { data class Failure(val error: TapError) : WalletAction() } - object LoadWallet : WalletAction() { + data class LoadWallet( + val wallet: Wallet, val artworkId: String?, val allowTopUp: Boolean + ) : WalletAction() { data class Success(val wallet: Wallet) : WalletAction() data class NoAccount(val amountToCreateAccount: String) : WalletAction() data class Failure(val errorMessage: String? = null) : WalletAction() @@ -50,7 +52,7 @@ sealed class WalletAction : Action { object Failure : WalletAction() } - object DisablePayId: WalletAction() + object DisablePayId : WalletAction() data class LoadArtwork(val card: Card, val artworkId: String?) : WalletAction() { data class Success(val artwork: Artwork) : WalletAction() @@ -85,4 +87,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() + } } \ No newline at end of file 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 index 69a69a8509..71d07e315d 100644 --- 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 @@ -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 @@ -33,211 +34,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 = { 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 = { 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(FirebaseAnalyticsHandler) - when (result) { - is CompletionResult.Success -> { - store.state.globalState.tapWalletManager - .onCardScanned(result.data, true) + is WalletAction.Scan -> { + scope.launch { + val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler) + 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()) } } } 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 ffd86c6469..358fe8cffc 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 @@ -36,7 +36,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState { is WalletAction.EmptyWallet -> newState = newState.copy( state = ProgressState.Done, currencyData = BalanceWidgetData(BalanceStatus.EmptyCard), - mainButton = WalletMainButton.CreateWalletButton(true) + mainButton = WalletMainButton.CreateWalletButton(true), + topUpState = TopUpState(false) ) is WalletAction.LoadData.Failure -> { when (action.error) { @@ -57,27 +58,23 @@ private fun internalReduce(action: Action, state: AppState): WalletState { token = wallet?.token?.symbol?.let { TokenData("", tokenSymbol = it) }), - mainButton = WalletMainButton.SendButton(false) + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(false) ) } is TapError.UnknownBlockchain -> { newState = newState.copy( state = ProgressState.Done, - currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain) + currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain), + topUpState = TopUpState(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) { + val wallet = action.wallet + val cardImage = if (newState.cardImage?.artworkId == action.artworkId) { newState.cardImage } else { null @@ -87,14 +84,15 @@ private fun internalReduce(action: Action, state: AppState): WalletState { cardImage = cardImage, currencyData = BalanceWidgetData( BalanceStatus.Loading, - wallet?.blockchain?.fullName, - token = wallet?.token?.symbol?.let { + wallet.blockchain.fullName, + currencySymbol = wallet.blockchain.currency, + token = wallet.token?.symbol?.let { TokenData("", tokenSymbol = it) } ), - addressData = addressData, - mainButton = WalletMainButton.SendButton(false) - + addressData = AddressData(wallet.address, wallet.shareUrl, wallet.exploreUrl), + mainButton = WalletMainButton.SendButton(false), + topUpState = TopUpState(allowed = action.allowTopUp) ) } is WalletAction.LoadWallet.Success -> newState = onWalletLoaded(action.wallet, newState) @@ -115,7 +113,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState { currencyData = newState.currencyData.copy( status = BalanceStatus.Unreachable, errorMessage = action.errorMessage - ) + ), + topUpState = TopUpState(false) ) is WalletAction.UpdateWallet -> newState = newState.copy(updatingWallet = true) is WalletAction.UpdateWallet.ScheduleUpdatingWallet -> @@ -210,11 +209,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) { 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 be5c2fad71..d6c916d79e 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 @@ -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" } -} \ No newline at end of file +} + +data class TopUpState( + val allowed: Boolean = true, + val url: String? = null, + val redirectUrl: String? = null +) \ No newline at end of file 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 c3e7f6c8fe..25f00a2e49 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 @@ -29,6 +29,8 @@ 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.* import org.rekotlin.StoreSubscriber @@ -39,7 +41,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber R.string.wallet_button_send is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet } - btn_confirm.text = getString(buttonTitle) - btn_confirm.isEnabled = state.mainButton.enabled + btnConfirm.text = getString(buttonTitle) + btnConfirm.isEnabled = state.mainButton.enabled - btn_confirm.setOnClickListener { + 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) { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpFragment.kt new file mode 100644 index 0000000000..20a844418f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpFragment.kt @@ -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 { + + 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) + } + } + +} diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpWebViewClient.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpWebViewClient.kt new file mode 100644 index 0000000000..d728b16128 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/topup/TopUpWebViewClient.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml new file mode 100644 index 0000000000..34f1d028fe --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_up_short_btn.xml b/app/src/main/res/drawable/ic_arrow_up_short_btn.xml new file mode 100644 index 0000000000..b739da9ccb --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_up_short_btn.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml new file mode 100644 index 0000000000..16d6d37dd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_close.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_group_1243.xml b/app/src/main/res/drawable/ic_group_1243.xml new file mode 100644 index 0000000000..c1e2907eb0 --- /dev/null +++ b/app/src/main/res/drawable/ic_group_1243.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_scan.xml b/app/src/main/res/drawable/ic_scan.xml index 7fcb008d1d..476424f743 100644 --- a/app/src/main/res/drawable/ic_scan.xml +++ b/app/src/main/res/drawable/ic_scan.xml @@ -1,15 +1,16 @@ + android:width="24dp" + android:height="21dp" + android:viewportWidth="24" + android:viewportHeight="21" + > diff --git a/app/src/main/res/drawable/ic_scan_for_short_btn.xml b/app/src/main/res/drawable/ic_scan_for_short_btn.xml new file mode 100644 index 0000000000..1c281ecd9c --- /dev/null +++ b/app/src/main/res/drawable/ic_scan_for_short_btn.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_send_short_btn.xml b/app/src/main/res/drawable/ic_send_short_btn.xml new file mode 100644 index 0000000000..273ed56f76 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_short_btn.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 5bff544835..33157a5fe6 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -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" diff --git a/app/src/main/res/layout/fragment_send.xml b/app/src/main/res/layout/fragment_send.xml index 35b5a31030..fca84784bc 100644 --- a/app/src/main/res/layout/fragment_send.xml +++ b/app/src/main/res/layout/fragment_send.xml @@ -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" /> diff --git a/app/src/main/res/layout/fragment_topup.xml b/app/src/main/res/layout/fragment_topup.xml new file mode 100644 index 0000000000..8069c307f0 --- /dev/null +++ b/app/src/main/res/layout/fragment_topup.xml @@ -0,0 +1,31 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index b09f9cb769..1f139adb02 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -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" /> @@ -85,47 +85,21 @@ android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/l_card_balance" /> - - - - + android:visibility="gone"/> + + - diff --git a/app/src/main/res/layout/layout_wallet_long_buttons.xml b/app/src/main/res/layout/layout_wallet_long_buttons.xml new file mode 100644 index 0000000000..3872698afc --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_long_buttons.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_wallet_short_buttons.xml b/app/src/main/res/layout/layout_wallet_short_buttons.xml new file mode 100644 index 0000000000..4971b42309 --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_short_buttons.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + \ 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 f11f7450a8..834037ecad 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -1,3 +1,3 @@ - + Top Up diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 03b88ac43d..5c6ee06378 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -36,7 +36,7 @@ @dimen/btn_corner_radius @font/saira_semi_condensed_regular 93dp - 48dp + 52dp 3dp