Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-01 07:58:55 +00:00
commit f21c4adfd2
30 changed files with 646 additions and 75 deletions

View file

@ -34,7 +34,8 @@
<activity
android:name="com.tangem.tap.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait">
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View file

@ -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() {

View file

@ -0,0 +1,6 @@
package com.tangem.tap
object TapConfig {
const val usePayId: Boolean = true
const val coinMarketCapKey = "f6622117-c043-47a0-8975-9d673ce484de"
}

View file

@ -6,6 +6,11 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.blockchain.common.Blockchain
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
@ -27,4 +32,23 @@ fun String.toQrCode(): Bitmap {
}
}
return bmp
}
fun BigDecimal.toFormattedString(blockchain: Blockchain): String {
val symbols = DecimalFormatSymbols(Locale.US)
symbols.decimalSeparator = '.'
val df = DecimalFormat()
df.decimalFormatSymbols = symbols
df.maximumFractionDigits = blockchain.decimals()
df.minimumFractionDigits = 0
df.isGroupingUsed = false
val bd = BigDecimal(unscaledValue(), scale())
bd.setScale(blockchain.decimals(), BigDecimal.ROUND_DOWN)
return df.format(bd)
}
fun BigDecimal.toFiatString(rateValue: BigDecimal): String? {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.DOWN)
return "USD $fiatValue"
}

View file

@ -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),
)
}

View file

@ -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 {

View file

@ -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<AppState> = { 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
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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

View file

@ -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<String?> {
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<SetPayIdResponse> {
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<Blockchain> = 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)
}

View file

@ -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)
}

View file

@ -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<ScanNoteResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> 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)))
}
}

View file

@ -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<AppState> = { dispatch, state ->
@ -21,10 +24,15 @@ val homeMiddleware: Middleware<AppState> = { 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))
}
}
}
}

View file

@ -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 {

View file

@ -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<AppState> = { 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<AppState> = { 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)
}
}
}

View file

@ -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
}

View file

@ -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 }
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 }

View file

@ -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()

View file

@ -0,0 +1,47 @@
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() +
this.context.getString(R.string.wallet_pay_id_address)
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()
}
}

View file

@ -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()
}
}

View file

@ -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<WalletState> {
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<Walle
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
if (state.qrCode != null && state.wallet?.shareUrl != null) {
dialog = QrDialog(requireContext())
dialog?.show(state.qrCode, state.wallet.shareUrl)
qrDialog = QrDialog(requireContext())
qrDialog?.showQr(state.qrCode, state.wallet.shareUrl)
} else {
dialog?.dismiss()
qrDialog?.dismiss()
}
when (state.state) {
@ -89,24 +89,37 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
when (state.payIdData.state) {
PayIdState.Loading -> {
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
}
}
}

View file

@ -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<Interceptor> = 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()
}

View file

@ -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/"
}
}

View file

@ -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<PayIdResponse> {
return performRequest { payIdApi.getPayId(cardId, publicKey) }
}
suspend fun setPayId(
cardId: String, publicKey: String, payId: String, address: String, network: String
): Result<SetPayIdResponse> {
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
)

View file

@ -0,0 +1,106 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cl_payid_dialog"
android:layout_width="280dp"
android:layout_height="280dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="280dp"
android:layout_height="280dp"
android:layout_gravity="center">
<TextView
android:id="@+id/tv_dialog_create_payid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:paddingStart="24dp"
android:paddingTop="20dp"
android:paddingEnd="24dp"
android:text="@string/wallet_dialog_pay_id_button"
android:textColor="@color/textGray"
android:textSize="17sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_payid"
android:layout_width="118dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="32dp"
android:hint="@string/wallet_dialog_pay_id_hint"
android:textColorHint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_dialog_create_payid">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_payid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:inputType="textNoSuggestions|textVisiblePassword"
android:paddingStart="4dp"
android:paddingEnd="4dp" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:text="@string/wallet_pay_id_address"
android:textColor="@color/darkGray4"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="@id/til_payid"
app:layout_constraintStart_toEndOf="@id/til_payid"
app:layout_constraintTop_toTopOf="@id/til_payid" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/wallet_dialog_pay_id_description"
android:textColor="@color/darkGray4"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/til_payid" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_create_payid"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:insetTop="0dp"
android:insetBottom="0dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:text="@string/wallet_dialog_pay_id_button"
android:textColor="@color/blue"
android:textSize="16sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ProgressBar
android:id="@+id/pb_create_payid"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:textColor="@color/blue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@id/btn_create_payid"
app:layout_constraintStart_toStartOf="@id/btn_create_payid" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp">
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_address"
@ -49,7 +47,7 @@
android:paddingStart="16dp"
android:paddingTop="6dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:paddingBottom="30dp"
android:text="@string/wallet_explore_address"
android:textColor="@color/darkGray6"
android:textSize="12sp"

View file

@ -29,7 +29,6 @@
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textColor="@color/accent"
android:textSize="12sp"
app:drawableStartCompat="@drawable/ic_ok"
@ -46,7 +45,6 @@
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textColor="@color/warning"
android:textSize="12sp"
app:drawableStartCompat="@drawable/ic_baseline_error_outline_24"
@ -63,7 +61,6 @@
android:paddingStart="16dp"
android:paddingTop="4dp"
android:paddingEnd="16dp"
android:paddingBottom="16dp"
android:textColor="@color/darkGray6"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"

View file

@ -21,6 +21,7 @@
<color name="backgroundGray">#F9F9FA</color>
<color name="buttonGray">#F4F5F6</color>
<color name="textGray">#DE000000</color>
<color name="lightGray1">#F8F8FB</color>

View file

@ -20,6 +20,16 @@
<string name="wallet_account_not_created">Account is not created</string>
<string name="wallet_tokens">Tokens</string>
<string name="wallet_dialog_pay_id_button">Create PayID</string>
<string name="wallet_create_payid_empty">Enter desired PayID first</string>
<string name="wallet_dialog_pay_id_hint">PayID name</string>
<string name="wallet_pay_id_address" translatable="false">$payid.tangem.com</string>
<string name="wallet_dialog_pay_id_description">Your PayID is information unique to you, like your phone number, email or ABN.</string>
<string name="error_payid_already_created">This PayID already exists. Try a different one.</string>
<string name="error_creating_payid">Error response while creating PayID.</string>
<string name="send_address_or_payid">Address or PayID</string>
<string name="send_network_fee">Network fee</string>