Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-02 12:52:38 +03:00
parent 400d625df1
commit a6141bbbe5
18 changed files with 175 additions and 59 deletions

View file

@ -60,11 +60,11 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin"
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'com.google.android.material:material:1.2.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'com.tangem:blockchain:1.25.0'
implementation 'com.tangem:blockchain:1.27.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"

View file

@ -4,40 +4,44 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import retrofit2.HttpException
import java.util.*
class PayIdManager {
private val payIdService = PayIdService()
suspend fun getPayId(cardId: String, publicKey: String): Result<String?> {
suspend fun getPayId(cardId: String, publicKey: String): Result<String?> = withContext(Dispatchers.IO) {
val result = payIdService.getPayId(cardId, publicKey)
when (result) {
is Result.Success -> return Result.Success(result.data.payId)
is Result.Success -> return@withContext Result.Success(result.data.payId)
is Result.Failure -> {
(result.error as? HttpException)?.let {
if (it.code() == 404) return Result.Success(null)
if (it.code() == 404) return@withContext Result.Success(null)
}
return result
return@withContext result
}
}
}
suspend fun setPayId(
cardId: String, publicKey: String, payId: String, address: String, blockchain: Blockchain
): Result<SetPayIdResponse> {
): Result<SetPayIdResponse> = withContext(Dispatchers.IO) {
val result = payIdService.setPayId(cardId, publicKey, payId, address, blockchain.getPayIdNetwork())
when (result) {
is Result.Success -> return result
is Result.Success -> return@withContext result
is Result.Failure -> {
(result.error as? HttpException)?.let {
if (it.code() == 409) return Result.Failure(TapError.PayIdAlreadyCreated)
if (it.code() == 409) return@withContext Result.Failure(TapError.PayIdAlreadyCreated)
}
return result
return@withContext result
}
}
}
private fun Blockchain.getPayIdNetwork(): String {
return when (this) {
Blockchain.XRP -> "XRPL"

View file

@ -6,6 +6,7 @@ import com.tangem.commands.CommandResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.tangem_sdk_new.extensions.init
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import kotlinx.coroutines.Dispatchers
@ -23,12 +24,18 @@ class TangemSdkManager(val activity: ComponentActivity) {
return runTaskAsyncReturnOnMain(ScanNoteTask())
}
suspend fun createWallet(): CompletionResult<ScanNoteResponse> {
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask())
}
private suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> =
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.commands.common.network.Result
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.wallet.redux.PayIdState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
@ -47,12 +48,26 @@ class TapWalletManager {
handleFiatRatesResult(results)
}
suspend fun onCardScanned(data: ScanNoteResponse) {
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.LoadCard(data.card))
if (data.walletManager != null) {
store.dispatch(GlobalAction.LoadWalletManager(data.walletManager))
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else {
store.dispatch(WalletAction.EmptyWallet)
}
}
}
private suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exeption: Exception) {
Result.Failure(exeption)
} catch (exception: Exception) {
Result.Failure(exception)
}
handleUpdateWalletResult(result)
}

View file

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

View file

@ -6,11 +6,12 @@ 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.CardStatus
import com.tangem.commands.CommandResponse
import com.tangem.common.CompletionResult
data class ScanNoteResponse(
val walletManager: WalletManager,
val walletManager: WalletManager?,
val card: Card
) : CommandResponse
@ -19,11 +20,16 @@ class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
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, card)))
callback(card.toScanNoteCompletionResult())
}
}
fun Card?.toScanNoteCompletionResult(): CompletionResult<ScanNoteResponse> {
this ?: return CompletionResult.Failure(TangemSdkError.CardError())
if (this.status == CardStatus.Empty) {
return CompletionResult.Success(ScanNoteResponse(null, this))
}
val walletManager = WalletManagerFactory.makeWalletManager(this)
?: return CompletionResult.Failure(TangemSdkError.CardError())
return CompletionResult.Success(ScanNoteResponse(walletManager, this))
}

View file

@ -5,10 +5,8 @@ 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
@ -27,11 +25,7 @@ val homeMiddleware: Middleware<AppState> = { dispatch, state ->
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.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
store.state.globalState.tapWalletManager.onCardScanned(result.data)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -43,4 +43,5 @@ sealed class WalletAction : Action {
object HideQrCode : WalletAction()
data class ExploreAddress(val context: Context) : WalletAction()
object CreateWallet : WalletAction()
object EmptyWallet : WalletAction()
}

View file

@ -8,7 +8,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.extensions.toHexString
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.scope
@ -39,6 +38,17 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
store.state.globalState.tapWalletManager.loadFiatRate()
}
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet()
when (result) {
is CompletionResult.Success -> {
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
}
}
}
is WalletAction.CreatePayId.CompleteCreatingPayId -> {
scope.launch {
val cardId = store.state.globalState.card?.cardId
@ -68,13 +78,7 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
val result = tangemSdkManager.scanNote()
when (result) {
is CompletionResult.Success -> {
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.LoadCard(result.data.card))
store.dispatch(GlobalAction.LoadWalletManager(result.data.walletManager))
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
}
store.state.globalState.tapWalletManager.onCardScanned(result.data)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.extensions.toQrCode
@ -17,12 +18,19 @@ fun walletReducer(action: Action, state: AppState): WalletState {
var newState = state.walletState
when (action) {
is WalletAction.EmptyWallet -> newState = WalletState(
state = ProgressState.Done,
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true)
)
is WalletAction.LoadWallet -> newState = WalletState(
state = ProgressState.Loading,
currencyData = BalanceWidgetData(
BalanceStatus.Loading,
state.globalState.walletManager?.wallet?.blockchain?.fullName
)
),
mainButton = WalletMainButton.SendButton(false)
)
is WalletAction.LoadWallet.Success -> {
val token = action.wallet.amounts[AmountType.Token]
@ -38,6 +46,8 @@ fun walletReducer(action: Action, state: AppState): WalletState {
val amount = action.wallet.amounts[AmountType.Coin]?.value
val fiatRate = state.globalState.fiatRates.getRateForCryptoCurrency(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it) }
val sendButtonEnabled = amount?.isZero() == false || token?.value?.isZero() == false
newState = newState.copy(
state = ProgressState.Done, wallet = action.wallet,
currencyData = BalanceWidgetData(
@ -45,14 +55,15 @@ fun walletReducer(action: Action, state: AppState): WalletState {
amount?.toFormattedString(action.wallet.blockchain),
token = tokenData,
fiatAmount = fiatAmount
)
),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
)
}
is WalletAction.LoadFiatRate.Success -> {
val rate = action.fiatRates.second
val currency = action.fiatRates.first
val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) {
newState.wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatString(rate)
newState.wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatString(rate)
} else {
null
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.entities.Button
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import org.rekotlin.StateType
@ -12,7 +13,8 @@ data class WalletState(
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val payIdData: PayIdData = PayIdData(),
val qrCode: Bitmap? = null,
val creatingPayIdState: CreatingPayIdState? = null
val creatingPayIdState: CreatingPayIdState? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false)
) : StateType
@ -25,4 +27,9 @@ data class PayIdData(
val payId: String? = null
)
enum class CreatingPayIdState { EnterPayId, Waiting }
enum class CreatingPayIdState { EnterPayId, Waiting }
sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
class SendButton(enabled: Boolean) : WalletMainButton(enabled)
class CreateWalletButton(enabled: Boolean) : WalletMainButton(enabled)
}

View file

@ -5,7 +5,9 @@ import androidx.fragment.app.Fragment
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.card_balance.*
import kotlinx.android.synthetic.main.layout_balance.*
import kotlinx.android.synthetic.main.layout_balance_error.*
import kotlinx.android.synthetic.main.layout_token.view.*
enum class PayIdState {
@ -18,7 +20,8 @@ enum class PayIdState {
enum class BalanceStatus {
VerifiedOnline,
Unreachable,
Loading
Loading,
EmptyCard
}
data class BalanceWidgetData(
@ -45,6 +48,8 @@ class BalanceWidget(
when (data.status) {
BalanceStatus.Loading -> {
fragment.l_balance.show()
fragment.l_balance_error.hide()
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = "-"
fragment.tv_fiat_amount.hide()
@ -52,6 +57,8 @@ class BalanceWidget(
fragment.l_token.hide()
}
BalanceStatus.VerifiedOnline -> {
fragment.l_balance.show()
fragment.l_balance_error.hide()
fragment.tv_currency.text = data.currency
fragment.tv_amount.text = data.amount
fragment.tv_fiat_amount.show()
@ -70,9 +77,16 @@ class BalanceWidget(
}
BalanceStatus.Unreachable -> {
fragment.l_balance.show()
fragment.l_balance_error.hide()
fragment.l_token.hide()
showStatus(R.id.tv_status_error)
}
BalanceStatus.EmptyCard -> {
fragment.l_balance.hide()
fragment.l_balance_error.show()
fragment.tv_error_title.text = fragment.getText(R.string.wallet_empty_card)
fragment.tv_error_descriptions.text = fragment.getText(R.string.wallet_empty_card_description)
}
}

View file

@ -60,14 +60,34 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null) return
state.wallet?.address?.let { tv_address.text = it }
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
if (state.wallet?.address != null) {
l_address?.show()
tv_address.text = state.wallet.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(requireContext()))
}
} else {
l_address?.hide()
}
btn_copy.setOnClickListener { store.dispatch(WalletAction.CopyAddress(requireContext())) }
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowQrCode) }
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
}
btn_main.text = getString(buttonTitle)
btn_main.isEnabled = state.mainButton.enabled
btn_main.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> TODO()
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}
}
if (state.qrCode != null && state.wallet?.shareUrl != null) {
qrDialog = QrDialog(requireContext())
qrDialog?.showQr(state.qrCode, state.wallet.shareUrl)

View file

@ -2,14 +2,16 @@ package com.tangem.tap.network.coinmarketcap
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.performRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class CoinMarketCapService {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create() }
suspend fun getRate(currency: String): Result<BigDecimal> {
suspend fun getRate(currency: String): Result<BigDecimal> = withContext(Dispatchers.IO) {
val response = performRequest { api.getRateInfo(1, currency) }
return when (response) {
return@withContext when (response) {
is Result.Success -> Result.Success(response.data.data.quote.usd.price)
is Result.Failure -> response
}

View file

@ -2,8 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="24dp">
android:layout_height="match_parent">
<ImageView

View file

@ -56,7 +56,7 @@
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send"
android:id="@+id/btn_main"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_height="48dp"

View file

@ -9,14 +9,15 @@
android:id="@+id/iv_balance_error"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:layout_width="26dp"
android:layout_height="26dp"
android:scaleType="fitXY"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:src="@drawable/ic_baseline_error_outline_24" />
<TextView
android:id="@+id/tv_error_description"
android:id="@+id/tv_error_title"
app:layout_constraintStart_toEndOf="@id/iv_balance_error"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/wallet_account_not_created"
@ -29,9 +30,9 @@
android:textStyle="bold" />
<TextView
android:id="@+id/tv_fiat_amount"
android:id="@+id/tv_error_descriptions"
app:layout_constraintStart_toEndOf="@id/iv_balance_error"
app:layout_constraintTop_toBottomOf="@id/tv_error_description"
app:layout_constraintTop_toBottomOf="@id/tv_error_title"
tools:text="Load10+ XLM to create account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -12,12 +12,15 @@
<string name="wallet_toolbar_title" translatable="false">Tangem Tap</string>
<string name="wallet_button_scan">Scan</string>
<string name="wallet_button_send">Send</string>
<string name="wallet_button_create_wallet">Create Wallet</string>
<string name="wallet_explore_address">Explore address</string>
<string name="wallet_create_payid">Create PayID</string>
<string name="wallet_verified_balance">Verified Balance</string>
<string name="wallet_blockchain_is_unreachable">Blockchain is unreachable</string>
<string name="wallet_balance_is_loading">Balance is loading…</string>
<string name="wallet_account_not_created">Account is not created</string>
<string name="wallet_empty_card">Empty Card</string>
<string name="wallet_empty_card_description">Create wallet to start using Tangem card</string>
<string name="wallet_tokens">Tokens</string>
<string name="wallet_dialog_pay_id_button">Create PayID</string>