Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-22 11:31:16 +00:00
commit 6e14ad22a8
13 changed files with 119 additions and 108 deletions

View file

@ -64,7 +64,7 @@ dependencies {
implementation 'com.google.android.material:material:1.2.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation 'com.tangem:blockchain:1.37.0'
implementation 'com.tangem:blockchain:1.40.0'
//lifecycle
implementation "androidx.lifecycle:lifecycle-runtime:2.2.0"
@ -91,9 +91,12 @@ dependencies {
// Camera
implementation 'com.otaliastudios:cameraview:2.6.3'
// Image Loading - Picasso
implementation 'com.squareup.picasso:picasso:2.71828'
testImplementation 'junit:junit:4.13'
testImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap
import android.app.Application
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.network.NetworkConnectivity
@ -24,5 +25,6 @@ class TapApplication : Application() {
}
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import java.io.File
import java.util.concurrent.TimeUnit
class PicassoHelper {
companion object {
private const val KEEP_CACHE_MAX_DAYS = 7
fun initPicassoWithCaching(application: Application) {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
okHttpBuilder.addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
.build()
val origRequest = chain.request()
val neverExpireRequest = origRequest.newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(neverExpireRequest)
}
return okHttpBuilder.build()
}
}
}

View file

@ -2,17 +2,14 @@ package com.tangem.tap.domain
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.commands.Card
import com.tangem.commands.CardStatus
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.TangemService
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.PayIdState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
@ -26,7 +23,6 @@ import java.math.BigDecimal
class TapWalletManager {
private val payIdManager = PayIdManager()
private val coinMarketCapService = CoinMarketCapService()
private val tangemService = TangemService()
suspend fun loadWalletData() {
val walletManager = store.state.globalState.scanNoteResponse?.walletManager
@ -37,21 +33,6 @@ class TapWalletManager {
updateWallet(walletManager)
}
suspend fun loadArtwork(card: Card, artworkId: String) {
val result =
tangemService.getArtwork(card.cardId, card.cardPublicKey!!.toHexString(), artworkId)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
store.dispatch(WalletAction.LoadArtwork.Success(
Artwork(artworkId, result.data.byteStream().readBytes())
))
}
is Result.Failure -> store.dispatch(WalletAction.LoadArtwork.Failure)
}
}
}
suspend fun loadPayId() {
val result = loadPayIdIfNeeded()
result?.let { handlePayIdResult(it) }

View file

@ -2,17 +2,24 @@ package com.tangem.tap.domain.tasks
import com.tangem.CardSession
import com.tangem.CardSessionRunnable
import com.tangem.commands.CreateWalletCommand
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) {
CreateWalletCommand().run(session) { result ->
CreateWalletTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> ScanNoteTask().run(session, callback)
is CompletionResult.Success -> {
ReadCommand().run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> ScanNoteTask(readResult.data).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -18,7 +18,7 @@ data class ScanNoteResponse(
val verifyResponse: VerifyCardResponse? = null
) : CommandResponse
class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
override val requiresPin2 = false
override fun run(session: CardSession, callback: (result: CompletionResult<ScanNoteResponse>) -> Unit) {
@ -27,7 +27,8 @@ class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val card = result.data
val card = this.card ?: result.data
val walletManager = try {
WalletManagerFactory.makeWalletManager(card)
} catch (exception: Exception) {
@ -49,14 +50,4 @@ class ScanNoteTask : CardSessionRunnable<ScanNoteResponse> {
}
}
}
}
fun Card?.toScanNoteCompletionResult(): CompletionResult<ScanNoteResponse> {
this ?: return CompletionResult.Failure(TangemSdkError.CardError())
val walletManager = try {
WalletManagerFactory.makeWalletManager(this)
} catch (exception: Exception) {
return CompletionResult.Success(ScanNoteResponse(null, this))
}
return CompletionResult.Success(ScanNoteResponse(walletManager, this))
}

View file

@ -19,7 +19,6 @@ import com.tangem.tap.network.NetworkStateChanged
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -46,21 +45,6 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency)
}
}
is WalletAction.LoadArtwork -> {
if (action.artworkId != null) {
scope.launch {
store.state.globalState.tapWalletManager.loadArtwork(
action.card, action.artworkId
)
}
} else {
val artworkRes = R.drawable.card_default
store.dispatch(WalletAction.LoadArtwork.Success(Artwork(
artworkId = artworkRes.toString(), artworkResId = artworkRes
)))
}
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.redux
import com.tangem.blockchain.common.AmountType
import com.tangem.common.extensions.isZero
import com.tangem.common.extensions.toHexString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedString
@ -12,6 +13,8 @@ import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
class WalletReducer {
@ -165,8 +168,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
token = newState.currencyData.token?.copy(fiatAmount = tokenFiatAmount)
))
}
is WalletAction.LoadArtwork.Success -> {
newState = newState.copy(cardImage = action.artwork)
is WalletAction.LoadArtwork -> {
// TODO: form URL in Card SDK
var artworkAddress = "https://verify.tangem.com/card/artwork"
artworkAddress += "?artworkId=${action.artworkId}"
artworkAddress += "&CID=${store.state.globalState.scanNoteResponse?.card?.cardId}"
artworkAddress += "&publicKey=${store.state.globalState.scanNoteResponse?.card?.cardPublicKey?.toHexString()}"
val artwork = if (action.artworkId != null) {
Artwork(artworkId = artworkAddress)
} else {
Artwork(artworkResId = R.drawable.card_default)
}
newState = newState.copy(cardImage = artwork)
}
is WalletAction.ShowQrCode -> {
newState = newState.copy(

View file

@ -21,7 +21,11 @@ data class WalletState(
val payIdData: PayIdData = PayIdData(),
val walletDialog: WalletDialog? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false)
) : StateType
) : StateType {
val showDetails: Boolean =
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
currencyData.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
}
sealed class WalletDialog {
data class QrDialog(
@ -58,7 +62,7 @@ data class AddressData(
)
data class Artwork(
val artworkId: String,
val artworkId: String? = null,
val artwork: Bitmap? = null,
val artworkResId: Int? = null
) {

View file

@ -85,6 +85,7 @@ class BalanceWidget(
fragment.tv_status_error_message.text = data.errorMessage
showStatus(R.id.group_error)
fragment.tv_status_error_message.show(!data.errorMessage.isNullOrBlank())
}
BalanceStatus.EmptyCard -> {
fragment.l_balance.hide()

View file

@ -3,18 +3,17 @@ package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.os.Bundle
import android.view.Menu
import android.view.Menu.NONE
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.extensions.getDrawable
import com.squareup.picasso.Picasso
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
@ -36,7 +35,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private var dialog: Dialog? = null
private var snackbar: Snackbar? = null
private var artworkId: String? = null
private lateinit var viewAdapter: PendingTransactionsAdapter
@ -93,6 +91,12 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null) return
if (!state.showDetails) {
toolbar.menu.removeItem(R.id.details_menu)
} else if (toolbar.menu.findItem(R.id.details_menu) == null) {
toolbar.menu.add(R.menu.wallet, R.id.details_menu, NONE, R.string.details_toolbar_title)
}
srl_wallet.setOnRefreshListener {
store.dispatch(WalletAction.LoadData)
}
@ -182,25 +186,15 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
private fun setupCardImage(cardImage: Artwork?) {
if (cardImage?.artwork != null) {
if (cardImage.artworkId != this.artworkId) {
prepareShowingCardImage(cardImage.artworkId)
iv_card.setImageBitmap(cardImage.artwork)
}
} else if (cardImage?.artworkResId != null) {
if (cardImage.artworkId != this.artworkId) {
prepareShowingCardImage(cardImage.artworkId)
iv_card.setImageDrawable(getDrawable(cardImage.artworkResId))
}
val address = cardImage?.artworkId ?: cardImage?.artworkResId
val picassoRequest = when (address) {
is String -> Picasso.get().load(address)
is Int -> Picasso.get().load(address)
else -> null
}
}
private fun prepareShowingCardImage(artworkId: String) {
this.artworkId = artworkId
val fadeInAnimation: Animation = AlphaAnimation(0.0f, 1.0f)
fadeInAnimation.duration = 400
iv_card.startAnimation(fadeInAnimation)
iv_card_tangem_logo.hide()
picassoRequest?.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card)
}
private fun handleDialogs(walletDialog: WalletDialog?) {
@ -255,7 +249,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.wallet, menu)
if (store.state.walletState.showDetails) inflater.inflate(R.menu.wallet, menu)
}
}

View file

@ -1,19 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle"
>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners
android:radius="12dp"
/>
<size
android:height="174dp"
android:width="328dp"/>
<gradient
android:startColor="#FFFFFF"
android:endColor="#FFFFFF"
/>
</shape>
<corners android:radius="12dp" />
<size
android:width="328dp"
android:height="174dp" />
<gradient
android:endColor="#FFFFFF"
android:startColor="#FFFFFF" />
</shape>
</item>
<item
android:drawable="@drawable/ic_tangem_pale"
android:gravity="center">
<size
android:width="85dp"
android:height="23dp" />
</item>
</layer-list>

View file

@ -60,17 +60,6 @@
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_card_tangem_logo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_tangem_pale"
app:layout_constraintStart_toStartOf="@id/iv_card"
app:layout_constraintEnd_toEndOf="@id/iv_card"
app:layout_constraintTop_toTopOf="@id/iv_card"
app:layout_constraintBottom_toBottomOf="@id/iv_card"
android:elevation="6dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_pending_transaction"