Updated on 2026-08-14

This commit is contained in:
Tangem 2022-03-28 12:58:24 +03:00
commit 9b5bc7bdff
31 changed files with 382 additions and 251 deletions

View file

@ -81,8 +81,8 @@ dependencies {
implementation 'com.tangem:blockchain:develop-66'
// implementation 'com.tangem:blockchain:0.0.1'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-139'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-139'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-140'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-140'
// WebView
implementation "androidx.browser:browser:1.3.0"

@ -1 +1 @@
Subproject commit b3fb1483a37bf1fb7d9da3f9bd9424f693c0ec5b
Subproject commit 64ae04d2086e5a605dd4da3cba4af32a60d46c79

View file

@ -35,6 +35,8 @@ val store = Store(
middleware = AppState.getMiddleware(),
state = AppState()
)
val logConfig = LogConfig()
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
@ -99,4 +101,14 @@ class TapApplication : Application() {
val analyticsHandler = GlobalAnalyticsHandler.createDefaultAnalyticHandlers(this)
store.dispatch(GlobalAction.SetAnanlyticHandlers(analyticsHandler))
}
}
}
data class LogConfig(
// disables both [internal, http]
val picasso: Boolean = BuildConfig.DEBUG,
val picassoInternal: Boolean = true,
val picassoHttp: Boolean = true,
val storeAction: Boolean = BuildConfig.DEBUG,
)

View file

@ -0,0 +1,64 @@
package com.tangem.tap.common
/**
[REDACTED_AUTHOR]
*/
open class Throttler<T>(
private val duration: Long,
) : Throttle<T> {
private val items: MutableMap<T, Long> = mutableMapOf()
override fun isStillThrottled(item: T): Boolean {
val inThrottlingUpTo = items[item] ?: return false
val diff = System.currentTimeMillis() - inThrottlingUpTo
return diff < 0
}
override fun updateThrottlingTo(item: T): T {
val now = System.currentTimeMillis()
val throttledUpTo = items[item] ?: 0L
if (throttledUpTo == 0L || throttledUpTo < now) {
val newTime = now + duration
items[item] = newTime
}
return item
}
open fun clear() {
items.clear()
}
}
class ThrottlerWithValues<T, V>(
duration: Long
) : Throttler<T>(duration), ValuesHolder<T, V> {
private val valuesHolder: MutableMap<T, V?> = mutableMapOf()
override fun setValue(item: T, value: V) {
valuesHolder[item] = value
}
override fun geValue(item: T): V? = valuesHolder[item]
override fun remove(item: T) {
valuesHolder.remove(item)
}
override fun clear() {
valuesHolder.clear()
super.clear()
}
}
interface Throttle<T> {
fun isStillThrottled(item: T): Boolean
fun updateThrottlingTo(item: T): T
}
interface ValuesHolder<K, V> {
fun setValue(item: K, value: V)
fun geValue(item: K): V?
fun remove(item: K)
}

View file

@ -4,6 +4,7 @@ enum class AnalyticsEvent(val event: String) {
CARD_IS_SCANNED("card_is_scanned"),
TRANSACTION_IS_SENT("transaction_is_sent"),
READY_TO_SCAN("ready_to_scan"),
DEMO_MODE_ACTIVATED("demo_mode_activated"),
APP_RATING_DISPLAYED("rate_app_warning_displayed"),
APP_RATING_DISMISS("dismiss_rate_app_warning"),
@ -20,6 +21,7 @@ enum class AnalyticsEvent(val event: String) {
enum class AnalyticsParam(val param: String) {
BLOCKCHAIN("blockchain"),
CARD_ID("cardId"),
BATCH_ID("batch_id"),
FIRMWARE("firmware"),
ACTION("action"),

View file

@ -13,7 +13,7 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
blockchain: String?,
params: Map<String, String>
) {
analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain) }
analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain, params) }
}
override fun triggerEvent(event: String, params: Map<String, String>) {

View file

@ -7,10 +7,6 @@ import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.Main, block)
}
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.Main, block)
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.IO, block)
}
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T): T = withContext(Dispatchers.IO, block)

View file

@ -18,6 +18,10 @@ fun Store<*>.dispatchOnMain(action: Action) {
}
}
suspend fun dispatchOnMain(vararg actions: Action) {
withMainContext { actions.forEach { store.dispatch(it) } }
}
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import com.tangem.tap.logConfig
import com.tangem.wallet.BuildConfig
import okhttp3.Cache
import okhttp3.CacheControl
@ -18,42 +19,45 @@ class PicassoHelper {
fun initPicassoWithCaching(application: Application) {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = BuildConfig.DEBUG
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = logIsEnabled()
picasso.setIndicatorsEnabled(BuildConfig.DEBUG)
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
okHttpBuilder.callTimeout(15000, TimeUnit.MILLISECONDS)
return OkHttpClient.Builder().apply {
cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
callTimeout(15000, TimeUnit.MILLISECONDS)
okHttpBuilder.addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
if (logIsEnabled()) {
addDebugInterceptors(this)
}
addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
.build()
val origRequest = chain.request()
val neverExpireRequest = origRequest.newBuilder()
val origRequest = chain.request()
val neverExpireRequest = origRequest.newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(neverExpireRequest)
}
addDebugInterceptors(okHttpBuilder)
chain.proceed(neverExpireRequest)
}
}.build()
return okHttpBuilder.build()
}
private fun addDebugInterceptors(okHttpBuilder: OkHttpClient.Builder) {
if (!BuildConfig.DEBUG) return
val picassoInterceptor = HttpLoggingInterceptor(PicassoOkHttpLogger()).apply {
level = HttpLoggingInterceptor.Level.BODY
}
okHttpBuilder.addInterceptor(picassoInterceptor)
}
private fun logIsEnabled(): Boolean = BuildConfig.DEBUG && logConfig.picasso
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.tap.logConfig
import org.rekotlin.Middleware
import timber.log.Timber
@ -9,7 +10,7 @@ import timber.log.Timber
val logMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
Timber.d("Dispatch action: $action")
if (logConfig.storeAction) Timber.d("Dispatch action: $action")
nextDispatch(action)
}
}

View file

@ -1,10 +1,12 @@
package com.tangem.tap.common.shop.shopify
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ShopifyShop(
val domain: String,
@Json(name = "storefrontApiKeyAndroid")
val storefrontApiKey: String,
val merchantID: String,
)

View file

@ -26,11 +26,13 @@ import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
@ -70,12 +72,21 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
result: CompletionResult<ScanResponse>
) {
when (result) {
is CompletionResult.Success ->
is CompletionResult.Success -> {
analyticsHandler?.triggerEvent(
event = AnalyticsEvent.CARD_IS_SCANNED,
card = result.data.card,
blockchain = result.data.walletData?.blockchain
)
if (DemoHelper.isDemoCard(result.data)) {
analyticsHandler?.triggerEvent(
event = AnalyticsEvent.DEMO_MODE_ACTIVATED,
card = result.data.card,
blockchain = result.data.walletData?.blockchain,
params = mapOf(AnalyticsParam.CARD_ID.param to result.data.card.cardId)
)
}
}
is CompletionResult.Failure ->
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler?.logCardSdkError(

View file

@ -3,9 +3,11 @@ package com.tangem.tap.domain
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.common.services.Result
import com.tangem.tap.common.ThrottlerWithValues
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
@ -23,51 +25,63 @@ import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class TapWalletManager {
private val coinMarketCapService = CoinMarketCapService()
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
private val coinMarketCapService = CoinMarketCapService()
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private val walletManagersThrottler = ThrottlerWithValues<Blockchain, Result<Wallet>>(10000)
private val fiatRatesThrottler = ThrottlerWithValues<Currency, Result<BigDecimal>?>(60000)
suspend fun loadWalletData(walletManager: WalletManager) {
val result = walletManager.safeUpdate()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> {
checkForRentWarning(walletManager)
store.dispatch(WalletAction.LoadWallet.Success(result.data))
}
is Result.Failure -> {
when (result.error) {
is TapError.WalletManagerUpdate.NoAccountError -> {
store.dispatch(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
))
}
else -> {
store.dispatch(WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
))
}
val blockchain = walletManager.wallet.blockchain
val result = if (walletManagersThrottler.isStillThrottled(blockchain)) {
walletManagersThrottler.geValue(blockchain)!!
} else {
updateThrottlingForWalletManager(walletManager)
}
when (result) {
is Result.Success -> {
checkForRentWarning(walletManager)
dispatchOnMain(WalletAction.LoadWallet.Success(result.data))
}
is Result.Failure -> {
when (result.error) {
is TapError.WalletManagerUpdate.NoAccountError -> {
dispatchOnMain(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
(result.error as TapError.WalletManagerUpdate.NoAccountError).customMessage
))
}
else -> {
dispatchOnMain(WalletAction.LoadWallet.Failure(
walletManager.wallet,
result.error.localizedMessage
))
}
}
}
}
}
private suspend fun updateThrottlingForWalletManager(walletManager: WalletManager): Result<Wallet> {
val newResult = walletManager.safeUpdate()
val blockchain = walletManager.wallet.blockchain
walletManagersThrottler.updateThrottlingTo(blockchain)
walletManagersThrottler.setValue(blockchain, newResult)
return newResult
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
val currencies = wallet.getTokens()
.map { Currency.Token(it) }
@ -76,18 +90,30 @@ class TapWalletManager {
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, currencies: List<Currency>) {
val results = mutableListOf<Pair<Currency, Result<BigDecimal>?>>()
currencies.forEach {
results.add(it to coinMarketCapService.getRate(it.currencySymbol, fiatCurrency))
// get and submit previous result of equivalents.
val throttledResult = currencies.filter { fiatRatesThrottler.isStillThrottled(it) }.map {
Pair(it, fiatRatesThrottler.geValue(it))
}
if (throttledResult.isNotEmpty()) handleFiatRatesResult(throttledResult)
val toUpdate = currencies.filter { !fiatRatesThrottler.isStillThrottled(it) }
toUpdate.forEach {
val result = coinMarketCapService.getRate(it.currencySymbol, fiatCurrency)
if (result is Result.Success) {
fiatRatesThrottler.updateThrottlingTo(it)
fiatRatesThrottler.setValue(it, result)
}
handleFiatRatesResult(listOf(it to result))
}
handleFiatRatesResult(results)
}
suspend fun onCardScanned(data: ScanResponse) {
walletManagersThrottler.clear()
// fiatRatesThrottler.clear()
store.state.globalState.feedbackManager?.infoHolder?.setCardInfo(data)
updateConfigManager(data)
withContext(Dispatchers.Main) {
withMainContext {
store.dispatch(WalletAction.ResetState)
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
@ -113,42 +139,43 @@ class TapWalletManager {
}
suspend fun loadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadCardInfo(data.card))
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, null)
}
}
store.dispatch(WalletAction.LoadWallet())
store.dispatch(WalletAction.LoadFiatRate())
dispatchOnMain(WalletAction.LoadCardInfo(data.card))
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
dispatchOnMain(it)
return
}
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
dispatchOnMain(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
dispatchOnMain(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, primaryWalletManager)
} else {
dispatchOnMain(
WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager),
WalletAction.MultiWallet.AddBlockchains(listOf(blockchain))
)
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data, blockchain, null)
}
}
dispatchOnMain(
WalletAction.LoadWallet(),
WalletAction.LoadFiatRate()
)
}
private fun loadMultiWalletData(
private suspend fun loadMultiWalletData(
scanResponse: ScanResponse, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
@ -156,25 +183,31 @@ class TapWalletManager {
if (savedCurrencies == null) {
if (primaryBlockchain != null && primaryWalletManager != null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = listOf(primaryBlockchain), tokens = primaryTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(primaryTokens.toList()))
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(blockchains = listOf(primaryBlockchain), tokens = primaryTokens)
),
WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager),
WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)),
WalletAction.MultiWallet.AddTokens(primaryTokens.toList())
)
} else {
val blockchains = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(blockchains = blockchains, tokens = emptyList())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
val walletManagers = walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(CardCurrencies(
blockchains = blockchains,
tokens = emptyList()
)),
WalletAction.MultiWallet.AddWalletManagers(walletManagers),
WalletAction.MultiWallet.AddBlockchains(blockchains.toList()),
)
}
store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse)
store.dispatch(WalletAction.MultiWallet.FindTokensInUse)
dispatchOnMain(
WalletAction.MultiWallet.FindBlockchainsInUse,
WalletAction.MultiWallet.FindTokensInUse,
)
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
@ -187,10 +220,11 @@ class TapWalletManager {
} else {
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
}
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains))
store.dispatch(WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens.toList()))
dispatchOnMain(
WalletAction.MultiWallet.AddWalletManagers(walletManagers),
WalletAction.MultiWallet.AddBlockchains(blockchains),
WalletAction.MultiWallet.AddTokens(savedCurrencies.tokens.toList()),
)
}
}
@ -229,50 +263,45 @@ class TapWalletManager {
}
}
private fun checkForRentWarning(walletManager: WalletManager) {
private suspend fun checkForRentWarning(walletManager: WalletManager) {
val rentProvider = walletManager as? RentProvider ?: return
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance < rentExempt
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance < rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return@launch
val currency = walletManager.wallet.blockchain.currency
store.dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
if (!show) return
val currency = walletManager.wallet.blockchain.currency
dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<Currency, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
store.dispatch(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> store.dispatch(WalletAction.LoadFiatRate.Failure)
null -> {
}
results.map {
when (it.second) {
is Result.Success -> {
val rate = it.first to (it.second as Result.Success<BigDecimal>).data
dispatchOnMain(WalletAction.LoadFiatRate.Success(rate))
}
is Result.Failure -> dispatchOnMain(WalletAction.LoadFiatRate.Failure)
null -> {}
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
@ -8,5 +7,4 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
[REDACTED_AUTHOR]
*/
fun ScanResponse.isDemoCard(): Boolean = DemoHelper.isDemoCardId(card.cardId)
fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId)
fun Wallet.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(cardId)
fun WalletManager.isDemoWallet(): Boolean = DemoHelper.isDemoCardId(wallet.cardId)

View file

@ -29,17 +29,25 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
store.dispatch(BackupAction.CheckForUnfinishedBackup)
getView()?.findViewById<ComposeView>(R.id.cv_stories)?.setContent {
AppCompatTheme {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop) }
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(getRegionProvider())) }
)
}
}
}
private fun getRegionProvider(): RegionProvider = RegionService(
listOf(
// TelephonyManagerRegionProvider(requireContext()),
LocaleRegionProvider()
)
)
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->

View file

@ -0,0 +1,48 @@
package com.tangem.tap.features.home
import android.content.Context
import android.telephony.TelephonyManager
import android.telephony.TelephonyManager.PHONE_TYPE_CDMA
import androidx.compose.ui.text.intl.Locale
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
interface RegionProvider {
fun getRegion(): String?
}
class RegionService(
private val providers: List<RegionProvider>
) : RegionProvider {
override fun getRegion(): String? {
for (provider in providers) {
val region = provider.getRegion()
if (region != null) return region
}
return null
}
}
class TelephonyManagerRegionProvider(context: Context) : RegionProvider {
private val wContext: WeakReference<Context> = WeakReference(context)
override fun getRegion(): String? {
val tm = wContext.get()?.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager ?: return null
val region = when (tm.phoneType) {
PHONE_TYPE_CDMA -> {
// Result may be unreliable
tm.networkCountryIso
}
else -> tm.networkCountryIso
}
return region.ifEmpty { return null }
}
}
class LocaleRegionProvider : RegionProvider {
override fun getRegion(): String? = Locale.current.region
}

View file

@ -1,12 +1,13 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.home.RegionProvider
import org.rekotlin.Action
sealed class HomeAction : Action {
// from ui
object ReadCard : HomeAction()
object GoToShop : HomeAction()
data class GoToShop(val regionProvider: RegionProvider) : HomeAction()
// internal
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()

View file

@ -4,6 +4,7 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
@ -14,6 +15,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
@ -29,6 +31,7 @@ class HomeMiddleware {
val handler = homeMiddleware
const val CARD_SHOP_URI = "http://cards.tangem.com/"
const val BUY_WALLET_URL = "https://mv.tangem.com/"
}
}
@ -49,7 +52,10 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.GoToShop -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
when (action.regionProvider.getRegion()?.toLowerCase()) {
"ru" -> store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)
@ -66,7 +72,7 @@ private fun handleReadCard() {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard( onSuccess = { scanResponse ->
store.dispatch(GlobalAction.ScanCard(onSuccess = { scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))

View file

@ -21,7 +21,6 @@ sealed class OnboardingWalletAction : Action {
sealed class BackupAction : Action {
object DetermineBackupStep : BackupAction()
data class IntroduceBackup(val buyCardsUrl: String? = null) : BackupAction()
object StartBackup : BackupAction()
object DismissBackup : BackupAction()
@ -42,7 +41,6 @@ sealed class BackupAction : Action {
data class Success(val cardId: CardId, val artwork: Bitmap)
}
object GoToShop : BackupAction()
object FinishAddingBackupCards : BackupAction()
object ShowAccessCodeInfoScreen : BackupAction()
@ -58,7 +56,7 @@ sealed class BackupAction : Action {
data class WriteBackupCard(val cardNumber: Int) : BackupAction()
object PrepareToWritePrimaryCard : BackupAction()
object WritePrimaryCard: BackupAction()
object WritePrimaryCard : BackupAction()
object FinishBackup : BackupAction()
object DiscardBackup : BackupAction()

View file

@ -4,9 +4,6 @@ import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.operations.backup.BackupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppState
@ -17,7 +14,6 @@ import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.wallet.redux.Artwork
import kotlinx.coroutines.launch
import org.rekotlin.Action
@ -26,8 +22,6 @@ import org.rekotlin.Middleware
class OnboardingWalletMiddleware {
companion object {
val handler = onboardingWalletMiddleware
const val BUY_WALLET_URL = "https://wallet.tangem.com/"
}
}
@ -116,21 +110,8 @@ private fun handleWalletAction(action: Action) {
OnboardingWalletAction.ProceedBackup -> {
val newAction = when (val backupState = backupService.currentState) {
BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard
is BackupService.State.FinalizingBackupCard ->
BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> {
val url = if (card?.issuer?.name?.lowercase()?.contains("tangem") == true) {
BUY_WALLET_URL
} else {
null
}
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
walletState.backupState.backupStep == BackupStep.Finished) {
BackupAction.IntroduceBackup(url)
} else {
null
}
}
is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index)
else -> null
}
newAction?.let { store.dispatch(it) }
}
@ -221,13 +202,6 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
}
}
}
is BackupAction.GoToShop -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.ONBOARDING.param)
)
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards))

View file

@ -39,14 +39,7 @@ class BackupReducer {
): BackupState {
return when (action) {
is BackupAction.IntroduceBackup -> BackupState(
backupStep = BackupStep.InitBackup,
canSkipBackup = state.canSkipBackup,
buyAdditionalCardsUrl = action.buyCardsUrl
)
BackupAction.StartAddingPrimaryCard -> state.copy(backupStep = BackupStep.ScanOriginCard)
BackupAction.StartAddingBackupCards -> {
state.copy(backupStep = BackupStep.AddBackupCards)
}
@ -122,7 +115,6 @@ class BackupReducer {
BackupAction.DismissBackup -> state
is BackupAction.LoadBackupCardArtwork -> state
is BackupAction.CheckAccessCode -> state
BackupAction.GoToShop -> state
BackupAction.DetermineBackupStep -> state
BackupAction.CheckForUnfinishedBackup -> state
BackupAction.DiscardBackup -> state

View file

@ -52,7 +52,6 @@ data class BackupState(
val backupStep: BackupStep = BackupStep.InitBackup,
val maxBackupCards: Int = 2,
val canSkipBackup: Boolean = true,
val buyAdditionalCardsUrl: String? = null
)
enum class AccessCodeError {

View file

@ -2,7 +2,9 @@ package com.tangem.tap.features.onboarding.products.wallet.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
@ -387,28 +389,6 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.shop_menu -> {
store.dispatch(BackupAction.GoToShop)
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.shop, menu)
val backupState = store.state.onboardingWalletState.backupState
val backupStep = backupState.backupStep
val shopMenuShouldBeVisible =
(backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards) &&
backupState.buyAdditionalCardsUrl != null
menu.getItem(0).isVisible = shopMenuShouldBeVisible
}
override fun handleOnBackPressed() {
store.dispatch(OnboardingWalletAction.OnBackPressed)
}

View file

@ -204,7 +204,7 @@ private fun sendTransaction(
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain))
}
delay(10000)
delay(11000) // more than 10000 to avoid throttling
withContext(Dispatchers.Main) {
dispatch(WalletAction.LoadWallet(walletManager.wallet.blockchain))
}

View file

@ -9,6 +9,7 @@ import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -106,11 +107,11 @@ class TokensMiddleware {
val updatedScanResponse = scanResponse.copy(
derivedKeys = updatedDerivedKeys
)
store.dispatch(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
store.dispatchOnMain(GlobalAction.SaveScanNoteResponse(updatedScanResponse))
submitAdd(blockchains, tokens)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(NavigationAction.PopBackTo())
store.dispatchOnMain(NavigationAction.PopBackTo())
}
is CompletionResult.Failure -> {
store.dispatchErrorNotification(TapError.CustomError("Error adding tokens"))
@ -155,6 +156,6 @@ class TokensMiddleware {
WalletAction.MultiWallet.AddBlockchain(it)
} + tokens.map {
WalletAction.MultiWallet.AddToken(it)
}).forEach { store.dispatch(it) }
}).forEach { store.dispatchOnMain(it) }
}
}

View file

@ -55,8 +55,8 @@ data class WalletState(
val primaryWallet = if (walletsData.isNotEmpty()) walletsData[0] else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = walletManagers.map { it.wallet.blockchain }
@ -102,10 +102,10 @@ data class WalletState(
if (!isPrimaryCurrency(walletData)) {
val walletManager = getWalletManager(walletData.currency)
?: return true
?: return true
if (walletData.currency is Currency.Blockchain &&
walletManager.cardTokens.isNotEmpty()
walletManager.cardTokens.isNotEmpty()
) {
return false
}
@ -114,22 +114,22 @@ data class WalletState(
if (walletData.currency is Currency.Blockchain) {
return wallet.recentTransactions.toPendingTransactions(wallet.address).isEmpty() &&
wallet.amounts.toSendableAmounts().isEmpty()
wallet.amounts.toSendableAmounts().isEmpty()
} else if (walletData.currency is Currency.Token) (
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
)
}
return false
}
private fun isPrimaryCurrency(walletData: WalletData): Boolean {
return (walletData.currency is Currency.Blockchain &&
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
walletData.currency.blockchain == store.state.walletState.primaryBlockchain)
|| (walletData.currency is Currency.Token &&
walletData.currency.token == store.state.walletState.primaryToken)
}
fun replaceWalletInWallets(walletData: WalletData?): List<WalletData> {
@ -150,7 +150,7 @@ data class WalletState(
val remainingWallets: MutableList<WalletData> = newWallets.toMutableList()
val updatedWallets = walletsData.map { wallet ->
val newWallet = newWallets
.firstOrNull { wallet.currency == it.currency }
.firstOrNull { wallet.currency == it.currency }
if (newWallet == null) {
wallet
} else {
@ -177,7 +177,7 @@ data class WalletState(
fun addWalletManagers(newWalletManagers: List<WalletManager>): WalletState {
val updatedWalletManagers = this.walletManagers +
newWalletManagers.filterNot { this.blockchains.contains(it.wallet.blockchain) }
newWalletManagers.filterNot { this.blockchains.contains(it.wallet.blockchain) }
return copy(walletManagers = updatedWalletManagers)
}
}

View file

@ -92,14 +92,23 @@ class WalletMiddleware {
scope.launch {
when {
action.wallet != null -> {
tapWalletManager.loadFiatRate(fiatAppCurrency, action.wallet)
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
wallet = action.wallet,
)
}
action.currencyList != null -> {
tapWalletManager.loadFiatRate(fiatAppCurrency, action.currencyList)
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
currencies = action.currencyList,
)
}
else -> {
val currencyList = walletState.walletsData.map { it.currency }
tapWalletManager.loadFiatRate(fiatAppCurrency, currencyList)
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = fiatAppCurrency,
currencies = currencyList,
)
}
}
}
@ -146,6 +155,7 @@ class WalletMiddleware {
)
withMainContext { actionList.forEach { store.dispatch(it) } }
}
is Result.Failure -> {}
}
store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded)
}

View file

@ -95,7 +95,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
is WalletAction.LoadWallet -> {
if (action.blockchain == null) {
val wallets = newState.walletsData.map { wallet ->
wallet.copy(
currencyData = wallet.currencyData.copy(
status = BalanceStatus.Loading,

View file

@ -63,6 +63,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}.select { it.walletState }
}
walletView.setFragment(this, binding)
// store.dispatch(WalletAction.UpdateWallet(force = false))
store.dispatch(WalletAction.LoadWallet())
}

View file

@ -23,7 +23,6 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/shop"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/onboarding_getting_started" />

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/shop_menu"
android:title="@string/home_button_shop"
app:showAsAction="always" />
</menu>