From 0b75473cb6a401aedece48e3cfa9e87a62e6356f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 1 Sep 2020 10:21:18 +0300 Subject: [PATCH] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 3 +- .../main/java/com/tangem/tap/MainActivity.kt | 11 +- app/src/main/java/com/tangem/tap/TapConfig.kt | 6 + .../com/tangem/tap/common/redux/AppReducer.kt | 7 +- .../com/tangem/tap/common/redux/AppState.kt | 6 +- .../common/redux/NotificationsMiddleware.kt | 23 ++-- .../tap/common/redux/global/GlobalAction.kt | 11 ++ .../tap/common/redux/global/GlobalReducer.kt | 19 ++++ .../tap/common/redux/global/GlobalState.kt | 13 +++ .../com/tangem/tap/domain/PayIdManager.kt | 67 +++++++++++ .../java/com/tangem/tap/domain/TapErrors.kt | 10 ++ .../tangem/tap/domain/tasks/ScanNoteTask.kt | 13 ++- .../tap/features/home/redux/HomeMiddleware.kt | 16 ++- .../tap/features/wallet/redux/WalletAction.kt | 19 +++- .../features/wallet/redux/WalletMiddleware.kt | 80 ++++++++++++- .../features/wallet/redux/WalletReducer.kt | 34 +++++- .../tap/features/wallet/redux/WalletState.kt | 17 ++- .../tap/features/wallet/ui/BalanceWidget.kt | 10 +- .../tap/features/wallet/ui/PayIdDialog.kt | 46 ++++++++ .../tangem/tap/features/wallet/ui/QrDialog.kt | 3 +- .../tap/features/wallet/ui/WalletFragment.kt | 41 ++++--- .../java/com/tangem/tap/network/Retrofit.kt | 49 ++++++++ .../com/tangem/tap/network/payid/PayIdApi.kt | 26 +++++ .../tangem/tap/network/payid/PayIdService.kt | 38 +++++++ .../main/res/layout/dialog_create_payid.xml | 106 ++++++++++++++++++ app/src/main/res/layout/layout_address.xml | 8 +- app/src/main/res/layout/layout_balance.xml | 3 - app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 6 + 29 files changed, 617 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/TapConfig.kt create mode 100644 app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt create mode 100644 app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt create mode 100644 app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/PayIdManager.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/TapErrors.kt create mode 100644 app/src/main/java/com/tangem/tap/features/wallet/ui/PayIdDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/network/Retrofit.kt create mode 100644 app/src/main/java/com/tangem/tap/network/payid/PayIdApi.kt create mode 100644 app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt create mode 100644 app/src/main/res/layout/dialog_create_payid.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3fe3cc77a6..0b2ede6c22 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,8 @@ + android:screenOrientation="portrait" + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index d5c3a5b75d..cbf1d9ccbb 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.tangem.CardFilter import com.tangem.Config +import com.tangem.Log import com.tangem.TangemSdk import com.tangem.common.extensions.CardType import com.tangem.tangem_sdk_new.extensions.init @@ -18,6 +19,8 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import java.io.PrintWriter +import java.io.StringWriter import java.lang.ref.WeakReference import java.util.* import kotlin.coroutines.CoroutineContext @@ -32,7 +35,13 @@ val scope = CoroutineScope(coroutineContext) private fun initCoroutineExceptionHandler(): CoroutineExceptionHandler { - return CoroutineExceptionHandler { _, throwable -> throw throwable } + return CoroutineExceptionHandler { _, throwable -> + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + val exceptionAsString: String = sw.toString() + Log.e("TangemSdk", exceptionAsString) + throw throwable + } } class MainActivity : AppCompatActivity() { diff --git a/app/src/main/java/com/tangem/tap/TapConfig.kt b/app/src/main/java/com/tangem/tap/TapConfig.kt new file mode 100644 index 0000000000..483f9e8306 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/TapConfig.kt @@ -0,0 +1,6 @@ +package com.tangem.tap + +object TapConfig { + const val usePayId: Boolean = true + const val coinMarketCapKey = "f6622117-c043-47a0-8975-9d673ce484de" +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 57539883be..1431c3b890 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -1,5 +1,6 @@ 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.wallet.redux.walletReducer import org.rekotlin.Action @@ -8,9 +9,9 @@ fun appReducer(action: Action, state: AppState?): AppState { requireNotNull(state) if (action is AppAction.RestoreState) return action.state return AppState( - navigationState = navigationReducer(action, state), -// homeState = homeReducer(action, state), - walletState = walletReducer(action, state), + navigationState = navigationReducer(action, state), + globalState = globalReducer(action, state), + walletState = walletReducer(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 397b2dad27..8b147b94f0 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 @@ -1,5 +1,6 @@ package com.tangem.tap.common.redux +import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.NavigationState import com.tangem.tap.common.redux.navigation.navigationMiddleware import com.tangem.tap.features.home.redux.homeMiddleware @@ -9,8 +10,9 @@ import org.rekotlin.Middleware import org.rekotlin.StateType data class AppState( - val navigationState: NavigationState = NavigationState(), - val walletState: WalletState = WalletState() + val navigationState: NavigationState = NavigationState(), + val globalState: GlobalState = GlobalState(), + val walletState: WalletState = WalletState() ) : StateType { companion object { diff --git a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt index 60f216a815..9511364f8a 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt @@ -2,24 +2,33 @@ package com.tangem.tap.common.redux import androidx.coordinatorlayout.widget.CoordinatorLayout import com.google.android.material.snackbar.Snackbar -import com.tangem.TangemError +import com.tangem.tap.domain.TapError import com.tangem.tap.notificationsHandler import org.rekotlin.Action import org.rekotlin.Middleware import java.lang.ref.WeakReference class NotificationsHandler(coordinatorLayout: CoordinatorLayout) { - private val coordinatorLayoutWeak = WeakReference(coordinatorLayout) + private val basicCoordinatorLayout = WeakReference(coordinatorLayout) + private var baseLayout = basicCoordinatorLayout + + fun replaceBaseLayout(coordinatorLayout: CoordinatorLayout) { + baseLayout = WeakReference(coordinatorLayout) + } + + fun returnBaseLayout() { + baseLayout = basicCoordinatorLayout + } fun showNotification(message: String) { - coordinatorLayoutWeak.get()?.let { layout -> + baseLayout.get()?.let { layout -> Snackbar.make(layout, message, Snackbar.LENGTH_LONG) - .also { snackbar -> snackbar.show() } + .also { snackbar -> snackbar.show() } } } fun showNotification(message: Int) { - coordinatorLayoutWeak.get()?.let { + baseLayout.get()?.let { showNotification(it.context.getString(message)) } } @@ -32,7 +41,7 @@ val notificationsMiddleware: Middleware = { dispatch, state -> notificationsHandler?.showNotification(action.messageResource) } if (action is ErrorAction) { - notificationsHandler?.showNotification(action.error.customMessage) + notificationsHandler?.showNotification(action.error.localizedMessage) } next(action) } @@ -44,5 +53,5 @@ interface NotificationAction : Action { } interface ErrorAction : Action { - val error: TangemError + val error: TapError } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt new file mode 100644 index 0000000000..69c71cca29 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.common.redux.global + +import com.tangem.blockchain.common.WalletManager +import com.tangem.commands.Card +import org.rekotlin.Action + +sealed class GlobalAction : Action { + + data class LoadCard(val card: Card) : GlobalAction() + data class LoadWalletManager(val walletManager: WalletManager) : GlobalAction() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt new file mode 100644 index 0000000000..fb3feefe6d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -0,0 +1,19 @@ +package com.tangem.tap.common.redux.global + +import com.tangem.tap.common.redux.AppState +import org.rekotlin.Action + +fun globalReducer(action: Action, state: AppState): GlobalState { + + if (action !is GlobalAction) return state.globalState + + var newState = state.globalState + + when (action) { + is GlobalAction.LoadCard -> newState = newState.copy(card = action.card) + is GlobalAction.LoadWalletManager -> + newState = newState.copy(walletManager = action.walletManager) + + } + return newState +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt new file mode 100644 index 0000000000..d8bf5b2c3e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.common.redux.global + +import com.tangem.blockchain.common.WalletManager +import com.tangem.commands.Card +import org.rekotlin.StateType + +data class GlobalState( + val card: Card? = null, + val walletManager: WalletManager? = null, +) : StateType + + + diff --git a/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt new file mode 100644 index 0000000000..50cb79aca2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/PayIdManager.kt @@ -0,0 +1,67 @@ +package com.tangem.tap.domain + +import com.tangem.blockchain.common.Blockchain +import com.tangem.commands.common.network.Result +import com.tangem.tap.network.payid.PayIdService +import com.tangem.tap.network.payid.SetPayIdResponse +import retrofit2.HttpException +import java.util.* + +class PayIdManager { + private val payIdService = PayIdService() + + suspend fun getPayId(cardId: String, publicKey: String): Result { + val result = payIdService.getPayId(cardId, publicKey) + when (result) { + is Result.Success -> return Result.Success(result.data.payId) + is Result.Failure -> { + (result.error as? HttpException)?.let { + if (it.code() == 404) return Result.Success(null) + } + return result + } + } + } + + suspend fun setPayId( + cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain + ): Result { + val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork()) + when (result) { + is Result.Success -> return result + is Result.Failure -> { + (result.error as? HttpException)?.let { + if (it.code() == 409) return Result.Failure(TapError.PayIdAlreadyCreated) + } + return result + } + } + } + + private fun Blockchain.getPayIdNetwork(): String { + return when (this) { + Blockchain.XRP -> "XRPL" + Blockchain.RSK -> "RSK" + else -> this.currency + } + } + + companion object { + val payIdSupported: EnumSet = EnumSet.of( + Blockchain.XRP, + Blockchain.Ethereum, + Blockchain.Bitcoin, + Blockchain.Litecoin, + Blockchain.Stellar, + Blockchain.Cardano, + Blockchain.Ducatus, + Blockchain.BitcoinCash, + Blockchain.Binance, + Blockchain.RSK, + ) + } +} + +fun Blockchain.isPayIdSupported(): Boolean { + return PayIdManager.payIdSupported.contains(this) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt new file mode 100644 index 0000000000..99368af1ab --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.domain + +import androidx.annotation.StringRes +import com.tangem.wallet.R + +sealed class TapError(@StringRes val localizedMessage: Int): Throwable() { + object PayIdAlreadyCreated: TapError(R.string.error_payid_already_created) + object PayIdCreatingError: TapError(R.string.error_creating_payid) + object PayIdEmptyField: TapError(R.string.wallet_create_payid_empty) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt index 797fb2ccd6..2d57a07a75 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/ScanNoteTask.kt @@ -5,20 +5,25 @@ import com.tangem.CardSessionRunnable import com.tangem.TangemSdkError import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory +import com.tangem.commands.Card import com.tangem.commands.CommandResponse import com.tangem.common.CompletionResult -data class ScanNoteResponse(val walletManager: WalletManager) : CommandResponse +data class ScanNoteResponse( + val walletManager: WalletManager, + val card: Card +) : CommandResponse class ScanNoteTask : CardSessionRunnable { override val requiresPin2 = false override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { - val walletManager = session.environment.card?.let { WalletManagerFactory.makeWalletManager(it) } - if (walletManager == null) { + val card = session.environment.card + val walletManager = card?.let { WalletManagerFactory.makeWalletManager(it) } + if (card == null || walletManager == null) { callback(CompletionResult.Failure(TangemSdkError.CardError())) return } - callback(CompletionResult.Success(ScanNoteResponse(walletManager))) + callback(CompletionResult.Success(ScanNoteResponse(walletManager, card))) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index 4fc79cdca4..70273c894c 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -5,13 +5,16 @@ import android.net.Uri import androidx.core.content.ContextCompat.startActivity import com.tangem.common.CompletionResult 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.features.wallet.redux.WalletAction import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.rekotlin.Middleware val homeMiddleware: Middleware = { dispatch, state -> @@ -21,10 +24,15 @@ val homeMiddleware: Middleware = { dispatch, state -> is HomeAction.ReadCard -> { scope.launch { val result = tangemSdkManager.scanNote() - when (result) { - is CompletionResult.Success -> { - store.dispatch(WalletAction.LoadWallet(result.data.walletManager)) - store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) + withContext(Dispatchers.Main) { + when (result) { + is CompletionResult.Success -> { + store.dispatch(GlobalAction.LoadCard(result.data.card)) + store.dispatch(GlobalAction.LoadWalletManager(result.data.walletManager)) + store.dispatch(WalletAction.LoadWallet) + store.dispatch(WalletAction.LoadPayId) + store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet)) + } } } } 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 e6b811ce27..9b38d5f4c5 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 @@ -2,25 +2,32 @@ package com.tangem.tap.features.wallet.redux import android.content.Context import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager +import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.common.redux.NotificationAction +import com.tangem.tap.domain.TapError import com.tangem.wallet.R import org.rekotlin.Action sealed class WalletAction : Action { - data class LoadWallet(val walletManager: WalletManager) : WalletAction() { + object LoadWallet : WalletAction() { data class Success(val wallet: Wallet): WalletAction() object Failure: WalletAction() } - data class LoadPayId(val address: String) : WalletAction() { - object Success: WalletAction() + object LoadPayId : WalletAction() { + data class Success(val payId: String): WalletAction() + object NotCreated: WalletAction() object Failure: WalletAction() } object Scan : WalletAction() object Send : WalletAction() object CreatePayId : WalletAction() { - object Success: WalletAction() - object Failure: WalletAction() + data class CompleteCreatingPayId(val payId: String): WalletAction() + data class Success(val payId: String): WalletAction() + object EmptyField: WalletAction(), ErrorAction { + override val error = TapError.PayIdEmptyField + } + class Failure(override val error: TapError) : WalletAction(), ErrorAction + object Cancel: WalletAction() } data class CopyAddress(val context: Context) : WalletAction() { object Success : WalletAction(), NotificationAction { 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 6aae47add8..f89d07818f 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 @@ -3,9 +3,16 @@ package com.tangem.tap.features.wallet.redux import android.content.Intent import android.net.Uri import androidx.core.content.ContextCompat +import com.tangem.commands.common.network.Result import com.tangem.common.CompletionResult +import com.tangem.common.extensions.toHexString +import com.tangem.tap.TapConfig import com.tangem.tap.common.extensions.copyToClipboard import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.domain.PayIdManager +import com.tangem.tap.domain.TapError +import com.tangem.tap.domain.isPayIdSupported import com.tangem.tap.scope import com.tangem.tap.store import com.tangem.tap.tangemSdkManager @@ -21,15 +28,73 @@ val walletMiddleware: Middleware = { dispatch, state -> when (action) { is WalletAction.LoadWallet -> { scope.launch { - try { - action.walletManager.update() - } catch (ex: Exception) { + val walletManager = store.state.globalState.walletManager + if (walletManager == null) { store.dispatch(WalletAction.LoadWallet.Failure) -// callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(ex))) return@launch } + try { + walletManager.update() + } catch (ex: Exception) { + withContext(Dispatchers.Main) { + store.dispatch(WalletAction.LoadWallet.Failure) +// callback(CompletionResult.Failure(BlockchainInternalErrorConverter.convert(ex))) + next(action) + } + } withContext(Dispatchers.Main) { - store.dispatch(WalletAction.LoadWallet.Success(action.walletManager.wallet)) + store.dispatch(WalletAction.LoadWallet.Success(walletManager.wallet)) + } + } + } + is WalletAction.LoadPayId -> { + if (!TapConfig.usePayId || + store.state.walletState.payIdData.payIdState == PayIdState.Disabled || + store.state.globalState.walletManager?.wallet?.blockchain?.isPayIdSupported() == false) { + next(action) + } + scope.launch { + val cardId = store.state.globalState.card?.cardId + val publicKey = store.state.globalState.card?.cardPublicKey + if (cardId != null && publicKey != null) { + val result = PayIdManager().getPayId(cardId, publicKey.toHexString()) + withContext(Dispatchers.Main) { + when (result) { + is Result.Success -> { + val payId = result.data + if (payId == null) { + store.dispatch(WalletAction.LoadPayId.NotCreated) + } else { + store.dispatch(WalletAction.LoadPayId.Success(payId)) + } + } + is Result.Failure -> store.dispatch(WalletAction.LoadPayId.Failure) + } + } + } + } + } + is WalletAction.CreatePayId.CompleteCreatingPayId -> { + scope.launch { + val cardId = store.state.globalState.card?.cardId + val wallet = store.state.walletState.wallet + val publicKey = store.state.globalState.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)) + } + } + } } } } @@ -39,7 +104,10 @@ val walletMiddleware: Middleware = { dispatch, state -> when (result) { is CompletionResult.Success -> { withContext(Dispatchers.Main) { - store.dispatch(WalletAction.LoadWallet(result.data.walletManager)) + store.dispatch(GlobalAction.LoadCard(result.data.card)) + store.dispatch(GlobalAction.LoadWalletManager(result.data.walletManager)) + store.dispatch(WalletAction.LoadWallet) + store.dispatch(WalletAction.LoadPayId) } } } 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 ad6fe3db94..472b46b2af 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletReducer.kt @@ -1,6 +1,7 @@ package com.tangem.tap.features.wallet.redux import com.tangem.blockchain.common.AmountType +import com.tangem.tap.common.extensions.toFormattedString import com.tangem.tap.common.extensions.toQrCode import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.wallet.ui.BalanceStatus @@ -16,24 +17,28 @@ fun walletReducer(action: Action, state: AppState): WalletState { when (action) { is WalletAction.LoadWallet -> newState = WalletState( - state = ProgressState.Loading, walletManager = action.walletManager, + state = ProgressState.Loading, currencyData = BalanceWidgetData( - BalanceStatus.Loading, action.walletManager.wallet.blockchain.fullName + BalanceStatus.Loading, + state.globalState.walletManager?.wallet?.blockchain?.fullName ) ) is WalletAction.LoadWallet.Success -> { val token = action.wallet.amounts[AmountType.Token] val tokenData = if (token != null) { - TokenData(token.value.toString(), token.currencySymbol) + TokenData( + token.value?.toFormattedString(action.wallet.blockchain) ?: "", + token.currencySymbol) } else { null } + val amount = action.wallet.amounts[AmountType.Coin]?.value newState = newState.copy( state = ProgressState.Done, wallet = action.wallet, currencyData = BalanceWidgetData( BalanceStatus.VerifiedOnline, action.wallet.blockchain.fullName, - action.wallet.amounts[AmountType.Coin]?.value?.toString(), - token = tokenData + amount?.toFormattedString(action.wallet.blockchain), + token = tokenData, ) ) } @@ -47,7 +52,24 @@ fun walletReducer(action: Action, state: AppState): WalletState { is WalletAction.HideQrCode -> { newState = newState.copy(qrCode = null) } - + is WalletAction.LoadPayId.Success -> newState = newState.copy( + payIdData = PayIdData(PayIdState.Created, action.payId) + ) + is WalletAction.LoadPayId.NotCreated -> newState = newState.copy( + payIdData = PayIdData(PayIdState.NotCreated, null) + ) + is WalletAction.CreatePayId, is WalletAction.CreatePayId.Failure -> + newState = newState.copy(creatingPayIdState = CreatingPayIdState.EnterPayId) + is WalletAction.CreatePayId.CompleteCreatingPayId -> newState = newState.copy( + creatingPayIdState = CreatingPayIdState.Waiting + ) + is WalletAction.CreatePayId.Success -> newState = newState.copy( + payIdData = PayIdData(PayIdState.Created, action.payId), + creatingPayIdState = null + ) + is WalletAction.CreatePayId.Cancel -> newState = newState.copy( + creatingPayIdState = null + ) } return newState } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 3ecc29fa9e..e7e0ca4c64 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 @@ -2,20 +2,27 @@ package com.tangem.tap.features.wallet.redux import android.graphics.Bitmap import com.tangem.blockchain.common.Wallet -import com.tangem.blockchain.common.WalletManager import com.tangem.tap.features.wallet.ui.BalanceWidgetData -import com.tangem.tap.features.wallet.ui.PayIdData import org.rekotlin.StateType data class WalletState( val state: ProgressState = ProgressState.Done, val cardImage: Bitmap? = null, - val walletManager: WalletManager? = null, val wallet: Wallet? = null, val currencyData: BalanceWidgetData = BalanceWidgetData(), val payIdData: PayIdData = PayIdData(), - val qrCode: Bitmap? = null + val qrCode: Bitmap? = null, + val creatingPayIdState: CreatingPayIdState? = null ) : StateType -enum class ProgressState { Loading, Done, Error } \ No newline at end of file +enum class ProgressState { Loading, Done, Error } + +enum class PayIdState { Disabled, Loading, NotCreated, Created, ErrorLoading } + +data class PayIdData( + val payIdState: PayIdState = PayIdState.Loading, + val payId: String? = null +) + +enum class CreatingPayIdState { EnterPayId, Waiting } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index a0733186cb..5338921e09 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -15,11 +15,6 @@ enum class PayIdState { Error } -data class PayIdData( - val address: String? = null, - val state: PayIdState? = null -) - enum class BalanceStatus { VerifiedOnline, Unreachable, @@ -36,7 +31,8 @@ data class BalanceWidgetData( data class TokenData( val amount: String, - val tokenSymbol: String + val tokenSymbol: String, + val fiatAmount: String? = null ) @@ -58,6 +54,7 @@ class BalanceWidget( BalanceStatus.VerifiedOnline -> { fragment.tv_currency.text = data.currency fragment.tv_amount.text = data.amount + fragment.tv_fiat_amount.show() fragment.tv_fiat_amount.text = data.fiatAmount showStatus(R.id.tv_status_verified) @@ -65,6 +62,7 @@ class BalanceWidget( fragment.l_token.show() fragment.l_token.tv_token_symbol.text = data.token.tokenSymbol fragment.l_token.tv_token_amount.text = data.token.amount + fragment.l_token.tv_token_fiat_amount.text = data.token.fiatAmount } else { fragment.l_token.hide() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/PayIdDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/PayIdDialog.kt new file mode 100644 index 0000000000..ace5406826 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/PayIdDialog.kt @@ -0,0 +1,46 @@ +package com.tangem.tap.features.wallet.ui + +import android.app.Dialog +import android.content.Context +import android.view.View +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.notificationsHandler +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.dialog_create_payid.* + +class PayIdDialog(context: Context) : Dialog(context) { + + init { + this.setContentView(R.layout.dialog_create_payid) + } + + override fun show() { + notificationsHandler?.replaceBaseLayout(this.cl_payid_dialog) + this.setOnDismissListener { + notificationsHandler?.returnBaseLayout() + store.dispatch(WalletAction.CreatePayId.Cancel) + } + this.btn_create_payid?.setOnClickListener { + if (this.et_payid.text.isNullOrBlank()) { + store.dispatch(WalletAction.CreatePayId.EmptyField) + } else { + val payid = this.et_payid.text!!.toString() + "\$payid.tangem.com" + store.dispatch(WalletAction.CreatePayId.CompleteCreatingPayId(payid)) + } + } + super.show() + } + + fun showProgress() { + this.btn_create_payid?.visibility = View.INVISIBLE + this.pb_create_payid?.show() + } + + fun stopProgress() { + this.btn_create_payid?.show() + this.pb_create_payid?.hide() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/QrDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/QrDialog.kt index e3073fcdcf..6d9f968fea 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/QrDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/QrDialog.kt @@ -14,7 +14,7 @@ class QrDialog(context: Context) : Dialog(context) { this.setContentView(R.layout.dialog_qrcode) } - fun show(qrCode: Bitmap, shareUrl: String) { + fun showQr(qrCode: Bitmap, shareUrl: String) { this.setOnDismissListener { store.dispatch(WalletAction.HideQrCode) } this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideQrCode) } @@ -22,5 +22,4 @@ class QrDialog(context: Context) : Dialog(context) { this.iv_qrcode?.setImageBitmap(qrCode) super.show() } - } \ 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 bd2baa29e2..72617cc32a 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 @@ -7,9 +7,8 @@ import androidx.fragment.app.Fragment import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.navigation.NavigationAction -import com.tangem.tap.features.wallet.redux.ProgressState -import com.tangem.tap.features.wallet.redux.WalletAction -import com.tangem.tap.features.wallet.redux.WalletState +import com.tangem.tap.features.wallet.redux.* +import com.tangem.tap.features.wallet.redux.PayIdState import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.card_balance.* @@ -19,7 +18,8 @@ import org.rekotlin.StoreSubscriber class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - var dialog: QrDialog? = null + private var qrDialog: QrDialog? = null + private var payIdDialog: PayIdDialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -69,10 +69,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber { - group_payid.hide() - } + when (state.payIdData.payIdState) { + PayIdState.Disabled, PayIdState.Loading -> group_payid.hide() PayIdState.NotCreated -> { group_payid.show() tv_create_payid.show() + tv_create_payid.setOnClickListener { store.dispatch(WalletAction.CreatePayId) } tv_payid_address.hide() } - PayIdState.Loaded -> { + PayIdState.Created -> { group_payid.show() tv_create_payid.hide() tv_payid_address.show() - tv_payid_address.text = state.payIdData.address + tv_payid_address.text = state.payIdData.payId + + } + PayIdState.ErrorLoading -> { } - PayIdState.Error -> group_payid.hide() } + when (state.creatingPayIdState) { + CreatingPayIdState.EnterPayId -> { + if (payIdDialog == null) payIdDialog = PayIdDialog(requireContext()) + payIdDialog?.show() + payIdDialog?.stopProgress() + } + CreatingPayIdState.Waiting -> payIdDialog?.showProgress() + null -> { + payIdDialog?.dismiss() + payIdDialog = null + } + } } diff --git a/app/src/main/java/com/tangem/tap/network/Retrofit.kt b/app/src/main/java/com/tangem/tap/network/Retrofit.kt new file mode 100644 index 0000000000..169baa076b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/Retrofit.kt @@ -0,0 +1,49 @@ +package com.tangem.tap.network + +import com.squareup.moshi.FromJson +import com.squareup.moshi.Moshi +import com.squareup.moshi.ToJson +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.wallet.BuildConfig +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import java.math.BigDecimal + +fun createRetrofitInstance( + baseUrl: String, + interceptors: List = emptyList(), +): Retrofit { + val okHttpBuilder = OkHttpClient.Builder() + interceptors.forEach { okHttpBuilder.addInterceptor(it) } + + if (BuildConfig.DEBUG) okHttpBuilder.addInterceptor(createHttpLoggingInterceptor()) + return Retrofit.Builder() + .baseUrl(baseUrl) + .addConverterFactory(createMoshiConverterFactory()) + .client(okHttpBuilder.build()) + .build() +} + +fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.create(createMoshi()) + +fun createMoshi(): Moshi = Moshi.Builder() + .add(BigDecimalAdapter) + .add(KotlinJsonAdapterFactory()).build() + +private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { + val logging = HttpLoggingInterceptor() + logging.level = HttpLoggingInterceptor.Level.BODY + return logging +} + +private object BigDecimalAdapter { + @FromJson + fun fromJson(string: String) = BigDecimal(string) + + @ToJson + fun toJson(value: BigDecimal) = value.toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdApi.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdApi.kt new file mode 100644 index 0000000000..8b00ecefd7 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdApi.kt @@ -0,0 +1,26 @@ +package com.tangem.tap.network.payid + +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Query + +interface PayIdApi { + @GET(API_PAY_ID_TANGEM) + suspend fun getPayId( + @Query("cid") cardId: String, + @Query("key") publicKey: String + ): PayIdResponse + + @POST(API_PAY_ID_TANGEM) + suspend fun setPayId( + @Query("cid") cardId: String, + @Query("key") publicKey: String, + @Query("payid") payId: String, + @Query("address") address: String, + @Query("network") network: String + ): SetPayIdResponse + + companion object { + const val API_PAY_ID_TANGEM = "https://payid.tangem.com/" + } +} diff --git a/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt new file mode 100644 index 0000000000..2e691228ea --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/payid/PayIdService.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.network.payid + +import com.squareup.moshi.JsonClass +import com.tangem.commands.common.network.Result +import com.tangem.commands.common.network.performRequest +import com.tangem.tap.network.createRetrofitInstance +import retrofit2.Retrofit + +class PayIdService { + + private val payIdApi: PayIdApi by lazy { + provideRetrofit() + .create(PayIdApi::class.java) + } + + suspend fun getPayId(cardId: String, publicKey: String): Result { + return performRequest { payIdApi.getPayId(cardId, publicKey) } + } + + suspend fun setPayId( + cardId: String, publicKey: String, payId: String, address: String, network: String + ): Result { + return performRequest { payIdApi.setPayId(cardId, publicKey, payId, address, network) } + } + + private fun provideRetrofit(): Retrofit = createRetrofitInstance("https://tangem.com/") + +} + +@JsonClass(generateAdapter = true) +data class PayIdResponse( + val payId: String +) + +@JsonClass(generateAdapter = true) +data class SetPayIdResponse( + val success: Boolean +) diff --git a/app/src/main/res/layout/dialog_create_payid.xml b/app/src/main/res/layout/dialog_create_payid.xml new file mode 100644 index 0000000000..0029635909 --- /dev/null +++ b/app/src/main/res/layout/dialog_create_payid.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_address.xml b/app/src/main/res/layout/layout_address.xml index 2c2ccf9e00..859cfc42ce 100644 --- a/app/src/main/res/layout/layout_address.xml +++ b/app/src/main/res/layout/layout_address.xml @@ -16,13 +16,11 @@ android:layout_marginEnd="16dp" android:layout_marginBottom="4dp" android:background="@android:color/white" - android:elevation="3dp" - android:padding="16dp"> + android:elevation="3dp"> + android:layout_height="wrap_content"> #F9F9FA #F4F5F6 + #DE000000 #F8F8FB diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c52903a64..ffc03b87eb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,12 @@ Account is not created Tokens + Create PayID + Enter desired PayID first + + This PayID already exists. Try a different one. + Error response while creating PayID. + Address or PayID Network fee