Updated on 2026-08-14

This commit is contained in:
Tangem 2023-01-31 16:30:18 +03:00
commit ae43a82d23
92 changed files with 964 additions and 564 deletions

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">DebugTangem</string>
<string name="tangem_app_name">DebugTangem</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">DebugTangem</string>
<string name="tangem_app_name" translatable="false">DebugTangem</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BetaTangem</string>
<string name="tangem_app_name">BetaTangem</string>
</resources>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">BetaTangem</string>
<string name="tangem_app_name">BetaTangem</string>
</resources>

View file

@ -29,7 +29,7 @@
<application
android:theme="@style/AppTheme"
android:label="@string/app_name"
android:label="@string/tangem_app_name"
android:name="com.tangem.tap.TapApplication"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"

View file

@ -21,7 +21,6 @@ import com.tangem.tap.common.analytics.AnalyticsFactory
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.filters.BasicSignInFilter
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.common.analytics.filters.ShopPurchasedEventFilter
import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
@ -182,7 +181,6 @@ class TapApplication : Application(), ImageLoaderFactory {
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
factory.addFilter(ShopPurchasedEventFilter())
factory.addFilter(BasicSignInFilter())
factory.addFilter(BasicTopUpFilter(preferencesStorage.toppedUpWalletStorage))

View file

@ -20,6 +20,7 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.InterruptOnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.PutVisaCardDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
@ -132,6 +133,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.PutVisaCard -> PutVisaCardDialog.create(context)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)
is WalletDialog.CurrencySelectionDialog -> CurrencySelectionDialog.create(state.dialog, context)
is WalletDialog.ChooseTradeActionDialog -> ChooseTradeActionBottomSheetDialog(context, state.dialog)

View file

@ -42,7 +42,7 @@ class IntentHandler {
intent.action = null
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(HomeAction.ReadCard)
store.dispatch(HomeAction.ReadCard())
}
}
}

View file

@ -2,79 +2,157 @@ package com.tangem.tap.common.analytics.converters
import com.tangem.common.Converter
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.CardTypesResolver
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.filters.BasicTopUpFilter
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalCryptoAmount
import timber.log.Timber
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
class BasicSignInEventConverter(
private val scanResponse: ScanResponse,
) : Converter<WalletState, Basic.SignedIn?> {
class BasicEventsPreChecker {
override fun convert(value: WalletState): Basic.SignedIn? {
if (!statesIsReadyToCreateEvent(scanResponse.cardTypesResolver, value)) return null
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver) ?: return null
fun tryToSend(converterData: BasicEventsSourceData) {
if (!isReadyToSend(converterData)) return
BasicSignInEventConverter().convert(converterData)?.let { Analytics.send(it) }
BasicTopUpEventConverter().convert(converterData)?.let { Analytics.send(it) }
}
private fun isReadyToSend(data: BasicEventsSourceData): Boolean {
val (scanResponse, walletState, biometricsWalletDataModels) = data
if (walletState.derivationsCheckIsScheduled) {
Timber.d("FAILED: derivationsCheckIsScheduled")
return false
}
if (scanResponse.cardTypesResolver.isMultiwalletAllowed() && walletState.missingDerivations.isNotEmpty()) {
Timber.d("FAILED: isMultiwalletAllowed || missingDerivations.isNotEmpty")
return false
}
if (biometricsWalletDataModels == null) {
Timber.d("SWITCH: OLD")
val walletsDataFromStores = data.walletState.walletsDataFromStores
if (walletsDataFromStores.isEmpty()) {
Timber.d("FAILED: walletsDataFromStores.isEmpty")
return false
}
val totalBalanceState = data.walletState.totalBalance?.state
if (totalBalanceState == null || totalBalanceState == ProgressState.Loading ||
totalBalanceState == ProgressState.Refreshing
) {
Timber.d("FAILED: totalBalanceState: ${totalBalanceState?.name}")
return false
}
val balancesCount = walletsDataFromStores
.map { if (it.currencyData.amount == null) 0 else 1 }
.reduce { acc, i -> acc + i }
if (balancesCount != walletsDataFromStores.size) {
Timber.d("FAILED: balancesCount != walletsDataFromStores.size")
return false
}
} else {
Timber.d("SWITCH: BIOMETRICS")
if (biometricsWalletDataModels.isEmpty()) {
Timber.d("FAILED: biometricsWalletDataModels.isEmpty")
return false
}
if (biometricsWalletDataModels.any {
it.status is WalletDataModel.Loading ||
it.status is WalletDataModel.NoAccount ||
it.status is WalletDataModel.Unreachable ||
it.status is WalletDataModel.MissedDerivation ||
it.status.isErrorStatus
}) {
Timber.d("FAILED: by status")
return false
}
}
Timber.d("SUCCESS")
return true
}
}
/**
* With biometrics enabled, we should check its storage instead of "WalletState.walletsDataFromStores"
* because the latter is updated after some time.
* @property biometricsWalletDataModels - wallet data models from the 'WalletStoresManager'. If null, then
* the "WalletState.walletsDataFromStores" will be used to determine appropriate state
*/
data class BasicEventsSourceData(
val scanResponse: ScanResponse,
val walletState: WalletState,
val biometricsWalletDataModels: List<WalletDataModel>?,
) {
val batchId: String by lazy { scanResponse.card.batchId }
val userWalletIdStringValue: String? by lazy {
UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.stringValue
}
val paramCardCurrency: AnalyticsParam.CardCurrency? by lazy {
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
}
val paramCardBalanceState: AnalyticsParam.CardBalanceState by lazy { calculateAmount().toCardBalanceState() }
private fun calculateAmount(): BigDecimal {
return biometricsWalletDataModels?.calculateTotalCryptoAmount()
?: walletState.walletsDataFromStores.calculateTotalCryptoAmount()
}
private fun BigDecimal.toCardBalanceState(): AnalyticsParam.CardBalanceState = when {
isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
private fun List<WalletDataModel>.calculateTotalCryptoAmount(): BigDecimal {
return this
.map { it.status.amount }
.reduce(BigDecimal::plus)
}
}
class BasicSignInEventConverter : Converter<BasicEventsSourceData, Basic.SignedIn?> {
override fun convert(value: BasicEventsSourceData): Basic.SignedIn? {
if (value.paramCardCurrency == null || value.userWalletIdStringValue == null) return null
return Basic.SignedIn(
state = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
currency = cardCurrency,
batch = scanResponse.card.batchId,
state = value.paramCardBalanceState,
currency = value.paramCardCurrency!!,
batch = value.batchId,
).apply {
filterData = UserWalletIdBuilder.scanResponse(scanResponse)
.build()
?.stringValue
filterData = value.userWalletIdStringValue
}
}
}
class BasicTopUpEventConverter(
private val scanResponse: ScanResponse,
) : Converter<WalletState, Basic.ToppedUp?> {
class BasicTopUpEventConverter : Converter<BasicEventsSourceData, Basic.ToppedUp?> {
override fun convert(value: WalletState): Basic.ToppedUp? {
if (!statesIsReadyToCreateEvent(scanResponse.cardTypesResolver, value)) return null
val cardCurrency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver) ?: return null
override fun convert(value: BasicEventsSourceData): Basic.ToppedUp? {
if (value.paramCardCurrency == null || value.userWalletIdStringValue == null) return null
val data = BasicTopUpFilter.Data(
walletId = UserWalletIdBuilder.scanResponse(scanResponse).build()?.stringValue ?: "",
cardBalanceState = AnalyticsParam.CardBalanceState.from(value.walletsDataFromStores),
walletId = value.userWalletIdStringValue!!,
cardBalanceState = value.paramCardBalanceState,
)
return Basic.ToppedUp(cardCurrency).apply { filterData = data }
return Basic.ToppedUp(value.paramCardCurrency!!).apply { filterData = data }
}
}
private fun AnalyticsParam.CardBalanceState.Companion.from(
walletsData: List<WalletData>,
): AnalyticsParam.CardBalanceState {
val totalCryptoAmount = walletsData.calculateTotalCryptoAmount()
return when {
totalCryptoAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty
else -> AnalyticsParam.CardBalanceState.Full
}
}
private fun statesIsReadyToCreateEvent(cardTypesResolver: CardTypesResolver, state: WalletState): Boolean {
if (cardTypesResolver.isMultiwalletAllowed() && state.missingDerivations.isNotEmpty()) return false
if (state.walletsDataFromStores.isEmpty()) return false
val totalBalanceState = state.totalBalance?.state ?: return false
if (totalBalanceState == ProgressState.Loading || totalBalanceState == ProgressState.Refreshing) return false
val balancesCount = state.walletsDataFromStores.map {
if (it.currencyData.amount == null) 0 else 1
}.reduce { acc, i -> acc + i }
if (balancesCount != state.walletsStores.size) return false
return true
}

View file

@ -18,6 +18,7 @@ class ParamCardCurrencyConverter : Converter<CardTypesResolver, AnalyticsParam.C
value.isTangemNote() -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.isTangemTwins() -> AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
value.isSaltPay() -> AnalyticsParam.CurrencyType.Token(SaltPayWorkaround.tokenFrom(Blockchain.SaltPay))
value.getBlockchain() != Blockchain.Unknown -> AnalyticsParam.CurrencyType.Blockchain(value.getBlockchain())
value.getPrimaryToken() != null -> AnalyticsParam.CurrencyType.Token(value.getPrimaryToken()!!)
else -> null
} ?: return null

View file

@ -8,19 +8,19 @@ import com.tangem.tap.common.shop.data.ProductType
/**
[REDACTED_AUTHOR]
*/
class ShopOrderToEventConverter : Converter<Pair<Storefront.Order, ProductType>, Shop.Purchased> {
class ShopOrderToEventConverter : Converter<Pair<Storefront.Checkout, ProductType>, Shop.Purchased> {
override fun convert(value: Pair<Storefront.Order, ProductType>): Shop.Purchased {
val order = value.first
override fun convert(value: Pair<Storefront.Checkout, ProductType>): Shop.Purchased {
val checkout = value.first
val productType = value.second
val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: productType.sku
val sku = checkout.lineItems?.edges?.firstOrNull()?.node?.variant?.sku ?: productType.sku
val count = when (productType) {
ProductType.WALLET_2_CARDS -> "2"
ProductType.WALLET_3_CARDS -> "3"
}
val amount = "${order.totalPriceV2.amount} ${order.totalPriceV2.currencyCode.name}"
val code = (order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
val amount = "${checkout.totalPriceV2.amount} ${checkout.totalPriceV2.currencyCode.name}"
val code = (checkout.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
return Shop.Purchased(sku, count, amount, code)
}

View file

@ -7,14 +7,14 @@ sealed class MyWallets(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("My Wallets", event, params) {
object MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
object CardWasScanned : MyWallets(event = "Card Was Scanned")
object WalletUnlockTapped : MyWallets(event = "Wallet Unlock Tapped")
class MyWalletsScreenOpened : MyWallets(event = "My Wallets Screen Opened")
class CardWasScanned : MyWallets(event = "Card Was Scanned")
object Button {
object ScanNewCard : MyWallets(event = "Button - Scan New Card")
object UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
object EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
object DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
sealed class Button {
class ScanNewCard : MyWallets(event = "Button - Scan New Card")
class UnlockWithBiometrics : MyWallets(event = "Button - Unlock all with Face ID")
class EditWalletTapped : MyWallets(event = "Button - Edit Wallet Tapped")
class DeleteWalletTapped : MyWallets(event = "Button - Delete Wallet Tapped")
class WalletUnlockTapped : MyWallets(event = "Button - Wallet Unlock Tapped")
}
}

View file

@ -75,6 +75,8 @@ sealed class Onboarding(
class ClaimScreenOpened : Onboarding("Onboarding", "Claim screen opened")
class ButtonClaim : Onboarding("Onboarding", "Button - Claim")
class ClaimWasSuccessfully : Onboarding("Onboarding", "Claim was successfully")
class ButtonChat: Onboarding("Onboarding", "Button - Chat")
class EnableBiometrics(state: AnalyticsParam.OnOffState) : Onboarding(
category = "Onboarding / Biometric",
event = "Enable Biometric",

View file

@ -21,6 +21,7 @@ sealed class Settings(
class ButtonCardSettings : Settings(event = "Button - Card Settings")
class ButtonAppSettings : Settings(event = "Button - App Settings")
class ButtonCreateBackup : Settings(event = "Button - Create Backup")
class ButtonWalletConnect : Settings(event = "Button - Wallet Connect")
class ButtonSocialNetwork(network: SocialNetwork) : Settings(
event = "Button - Social Network",

View file

@ -11,7 +11,7 @@ sealed class Shop(
params: Map<String, String> = mapOf(),
) : AnalyticsEvent("Shop", event, params) {
class ScreenOpened : IntroductionProcess("Shop Screen Opened")
class ScreenOpened : Shop("Shop Screen Opened")
class Purchased(sku: String, count: String, amount: String, couponCode: String?) : Shop(
event = "Purchased",
@ -23,8 +23,8 @@ sealed class Shop(
).filterNotNull(),
)
class Redirected(partnerName: String) : Shop(
class Redirected(partnerName: String?) : Shop(
event = "Redirected",
params = mapOf("Partner" to partnerName),
params = partnerName?.let { mapOf("Partner" to partnerName) } ?: mapOf(),
)
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.analytics.events
import com.tangem.core.analytics.AnalyticsEvent
/**
[REDACTED_AUTHOR]
*/
sealed class SignIn(
event: String,
params: Map<String, String> = mapOf(),
error: Throwable? = null,
) : AnalyticsEvent("Sign In", event, params, error) {
class ScreenOpened : SignIn(event = "Sing In Screen Opened")
class CardWasScanned : SignIn(event = "Card Was Scanned")
class ButtonBiometricSignIn : SignIn(event = "Button - Biometric Sign In")
class ButtonCardSignIn : SignIn(event = "Button - Card Sign In")
}

View file

@ -46,14 +46,14 @@ sealed class Token(
params = mapOf("Token" to type.value),
)
sealed class Recieve(
sealed class Receive(
event: String,
params: Map<String, String> = mapOf(),
) : Token("Token / Recieve", event, params) {
) : Token("Token / Receive", event, params) {
class ScreenOpened : Recieve("Recieve Screen Opened")
class ButtonCopyAddress : Recieve("Button - Copy Address")
class ButtonShareAddress : Recieve("Button - Share Address")
class ScreenOpened : Receive("Receive Screen Opened")
class ButtonCopyAddress : Receive("Button - Copy Address")
class ButtonShareAddress : Receive("Button - Share Address")
}
sealed class Send(

View file

@ -11,6 +11,7 @@ sealed class WalletConnect(
error: Throwable? = null,
) : AnalyticsEvent("Wallet Connect", event, params, error) {
class ScreenOpened : WalletConnect(event = "WC Screen Opened")
class NewSessionEstablished : WalletConnect("New Session Established")
class SessionDisconnected : WalletConnect("Session Disconnected")
class RequestSigned : WalletConnect("Request Signed")

View file

@ -1,25 +0,0 @@
package com.tangem.tap.common.analytics.filters
import com.tangem.core.analytics.api.AnalyticsEventFilter
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.analytics.handlers.appsFlyer.AppsFlyerAnalyticsHandler
import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler
/**
[REDACTED_AUTHOR]
*/
class ShopPurchasedEventFilter : AnalyticsEventFilter {
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is Shop.Purchased
override fun canBeSent(event: AnalyticsEvent): Boolean = true
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
return when (handler) {
is AppsFlyerAnalyticsHandler, is FirebaseAnalyticsHandler -> true
else -> false
}
}
}

View file

@ -1,7 +1,10 @@
package com.tangem.tap.common.analytics.handlers.appsFlyer
import com.appsflyer.AFInAppEventType
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.events.Shop
class AppsFlyerAnalyticsHandler(
private val client: AppsFlyerAnalyticsClient,
@ -13,6 +16,14 @@ class AppsFlyerAnalyticsHandler(
client.logEvent(event, params)
}
override fun send(event: AnalyticsEvent) {
if (event is Shop.Purchased) {
send(AFInAppEventType.PURCHASE, event.params)
} else {
super.send(event)
}
}
companion object {
const val ID = "AppsFlyer"
}

View file

@ -1,10 +1,12 @@
package com.tangem.tap.common.analytics.handlers.firebase
import com.google.firebase.analytics.FirebaseAnalytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.AnalyticsHandler
import com.tangem.core.analytics.api.ErrorEventHandler
import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder
import com.tangem.tap.common.analytics.converters.AnalyticsErrorConverter
import com.tangem.tap.common.analytics.events.Shop
class FirebaseAnalyticsHandler(
private val client: FirebaseAnalyticsClient,
@ -18,17 +20,23 @@ class FirebaseAnalyticsHandler(
override fun send(event: AnalyticsEvent) {
val error = event.error
if (error == null) {
super.send(event)
} else {
val errorConverter = AnalyticsErrorConverter()
if (!errorConverter.canBeHandled(error)) return
when {
error != null -> {
val errorConverter = AnalyticsErrorConverter()
if (!errorConverter.canBeHandled(error)) return
val errorParams = errorConverter.convert(error).toMutableMap()
errorParams["Category"] = event.category
errorParams["Event"] = event.event
errorParams.putAll(event.params)
send(error, errorParams)
val errorParams = errorConverter.convert(error).toMutableMap()
errorParams["Category"] = event.category
errorParams["Event"] = event.event
errorParams.putAll(event.params)
send(error, errorParams)
}
event is Shop.Purchased -> {
send(FirebaseAnalytics.Event.PURCHASE, event.params)
}
else -> {
super.send(event)
}
}
}

View file

@ -11,7 +11,7 @@ import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
// todo move extensions to utils
// TODO: move extensions to utils
fun BigDecimal.toFormattedString(
decimals: Int,
roundingMode: RoundingMode = RoundingMode.DOWN,
@ -75,7 +75,7 @@ fun BigDecimal.toFormattedFiatValue(
): String {
val fiatValue = this.setScale(2, RoundingMode.HALF_UP)
.let { if (formatWithSpaces) it.formatWithSpaces() else it }
return "$fiatValue $fiatCurrencyName"
return "$fiatValue$fiatCurrencyName"
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.feedback
import android.content.Context
import android.os.Build
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
@ -108,6 +109,7 @@ class FeedbackManager(
}
private fun showMessagingActivity(context: Context) {
Analytics.send(com.tangem.tap.common.analytics.events.Chat.ScreenOpened())
MessagingActivity.builder()
.withMultilineResponseOptionsEnabled(false)
.withBotLabelStringRes(R.string.chat_bot_name)

View file

@ -202,8 +202,8 @@ class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
suspend fun waitForCheckout(productType: ProductType) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess { checkout ->
checkout.order?.let {
val event = ShopOrderToEventConverter().convert(it to productType)
if (checkout.order != null && checkout.lineItems != null) {
val event = ShopOrderToEventConverter().convert(checkout to productType)
Analytics.send(event)
}
}

View file

@ -29,6 +29,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.toBlockchainNetworks
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.middlewares.handleBasicAnalyticsEvent
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
@ -123,6 +124,7 @@ class TapWalletManager {
.doOnSuccess {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
handleBasicAnalyticsEvent()
}
.doOnFailure { error ->
val errorAction = when (error) {
@ -200,6 +202,7 @@ class TapWalletManager {
}
if (data.cardTypesResolver.isMultiwalletAllowed()) {
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
loadMultiWalletData(data)
} else {
loadSingleWalletData(data)
@ -222,9 +225,7 @@ class TapWalletManager {
.filter {
it.derivationPath != null && !scanResponse.hasDerivation(it.blockchain, it.derivationPath)
}
if (missingDerivations.isNotEmpty()) {
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
}
store.dispatch(WalletAction.MultiWallet.AddMissingDerivations(missingDerivations))
}
private suspend fun loadSingleWalletData(data: ScanResponse) {
@ -276,7 +277,9 @@ class TapWalletManager {
}
suspend fun reloadData(data: ScanResponse) {
loadUserCurrencies(data, walletManagerFactory)
if (data.cardTypesResolver.isMultiwalletAllowed()) {
loadUserCurrencies(data, walletManagerFactory)
}
withContext(Dispatchers.Main) {
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)

View file

@ -14,6 +14,7 @@ import java.math.BigDecimal
* @param existentialDeposit Amount that must be held on currency's balance, if balance is below that amount all
* founds will be destroyed. Null if currency don't have existential deposit
* @param fiatRate Wallet's fiat rate, used to calculate fiat balance. Null if not provided
* @param isCardSingleToken shows that [Currency] is a card token
* */
data class WalletDataModel(
val currency: Currency,
@ -21,6 +22,7 @@ data class WalletDataModel(
val walletAddresses: List<AddressData>,
val existentialDeposit: BigDecimal?,
val fiatRate: BigDecimal?,
val isCardSingleToken: Boolean,
) {
/**

View file

@ -6,10 +6,12 @@ import com.tangem.common.hdWallet.DerivationPath
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.domain.model.WalletStoreModel.WalletRent
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import java.math.BigDecimal
/**
* Contains info about the blockchain and its currencies
*
* @param userWalletId ID of the associated [UserWallet]
* @param blockchain [Blockchain] of this WalletStore
* @param derivationPath [DerivationPath] of this store, null if the card does not support the
@ -21,6 +23,9 @@ import java.math.BigDecimal
* TODO: Remove after WalletMiddleware refactoring
* @param walletManager [WalletManager], may be null if it fails to create this manager.
* TODO: Remove after WalletMiddleware refactoring
*
* @property blockchainWalletData Returns the [WalletDataModel] of the blockchain of this wallet store
* or throw [NoSuchElementException] if this wallet store not contains [WalletDataModel] of the blockchain
* */
data class WalletStoreModel(
val userWalletId: UserWalletId,
@ -34,6 +39,9 @@ data class WalletStoreModel(
val walletManager: WalletManager?,
) {
val blockchainWalletData: WalletDataModel
get() = walletsData.first { it.currency is Currency.Blockchain }
/**
* Represents wallet blockchain rent
* @param rent Amount that will be charged in overtime if the blockchain does not have an amount greater than

View file

@ -97,6 +97,7 @@ private fun BlockchainNetwork.getBlockchainWalletData(walletManager: WalletManag
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
)
}
@ -113,6 +114,7 @@ private fun BlockchainNetwork.getTokensWalletsData(walletManager: WalletManager?
walletAddresses = walletManager?.wallet?.createAddressesData().orEmpty(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager?.cardTokens?.contains(token) ?: false,
)
}
}
@ -128,6 +130,7 @@ private fun Blockchain.toBlockchainWalletData(walletManager: WalletManager): Wal
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = false,
)
}
@ -143,6 +146,7 @@ private fun Token.toTokenWalletData(walletManager: WalletManager): WalletDataMod
walletAddresses = wallet.createAddressesData(),
existentialDeposit = getExistentialDeposit(walletManager),
fiatRate = null,
isCardSingleToken = walletManager.cardTokens.contains(this),
)
}

View file

@ -7,17 +7,15 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -30,6 +28,7 @@ import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayState
import com.tangem.tap.preferencesStorage
@ -37,7 +36,6 @@ import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -45,6 +43,7 @@ import kotlinx.coroutines.launch
// TODO: Create repository for that
object ScanCardProcessor {
suspend fun scan(
analyticsEvent: AnalyticsEvent? = null,
additionalBlockchainsToDerive: Collection<Blockchain>? = null,
cardId: String? = null,
onProgressStateChange: suspend (showProgress: Boolean) -> Unit = {},
@ -69,7 +68,6 @@ object ScanCardProcessor {
result
.doOnFailure { error ->
onProgressStateChange(false)
onScanStateChange(false)
onFailure(error)
}
@ -77,14 +75,15 @@ object ScanCardProcessor {
tangemSdkManager.changeDisplayedCardIdNumbersCount(scanResponse)
onScanStateChange(false)
sendAnalytics(analyticsEvent, scanResponse.card.batchId)
checkForUnfinishedBackupForSaltPay(
backupService = backupService,
scanResponse = scanResponse,
onProgressStateChange = { onProgressStateChange(it) },
onFailure = onFailure,
nextHandler = { scanResponse1 ->
showDisclaimerIfNeed(
scanResponse = scanResponse1,
onProgressStateChange = onProgressStateChange,
disclaimerWillShow = disclaimerWillShow,
onFailure = onFailure,
nextHandler = { scanResponse2 ->
@ -102,16 +101,24 @@ object ScanCardProcessor {
}
}
private fun sendAnalytics(
analyticsEvent: AnalyticsEvent?,
batchId: String,
) {
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId))
analyticsEvent?.let { Analytics.send(it) }
}
/**
* It checks only the SaltPay cards. To check for unfinished backups for the standard Wallet cards
* see BackupAction.CheckForUnfinishedBackup
* If user touches card other than Visa SaltPay - show dialog and block next processing
*/
private inline fun checkForUnfinishedBackupForSaltPay(
private suspend inline fun checkForUnfinishedBackupForSaltPay(
backupService: BackupService,
scanResponse: ScanResponse,
onProgressStateChange: (showProgress: Boolean) -> Unit,
nextHandler: (ScanResponse) -> Unit,
onFailure: suspend (error: TangemError) -> Unit,
) {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
nextHandler(scanResponse)
@ -123,8 +130,9 @@ object ScanCardProcessor {
?: false
if (scanResponse.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
onProgressStateChange(false)
showSaltPayTapVisaLogoCardDialog()
val error = SaltPayActivationError.PutVisaCard
SaltPayExceptionHandler.handle(error)
onFailure(TangemSdkError.ExceptionError(error))
} else {
nextHandler(scanResponse)
}
@ -133,7 +141,6 @@ object ScanCardProcessor {
private suspend inline fun showDisclaimerIfNeed(
scanResponse: ScanResponse,
crossinline disclaimerWillShow: () -> Unit = {},
crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit,
crossinline nextHandler: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
@ -141,30 +148,27 @@ object ScanCardProcessor {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
if (disclaimer.isAccepted()) {
nextHandler(scanResponse)
} else {
scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
disclaimerWillShow()
dispatchOnMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onProgressStateChange(false)
onFailure(TangemSdkError.UserCancelled())
}
},
),
nextHandler((scanResponse))
} else scope.launch {
delay(DELAY_SDK_DIALOG_CLOSE)
disclaimerWillShow()
dispatchOnMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
scope.launch(Dispatchers.Main) {
nextHandler(scanResponse)
}
},
onDismiss = {
scope.launch(Dispatchers.Main) {
onFailure(TangemSdkError.UserCancelled())
}
},
),
)
}
),
)
}
}
@ -176,14 +180,10 @@ object ScanCardProcessor {
crossinline onSuccess: suspend (ScanResponse) -> Unit,
crossinline onFailure: suspend (error: TangemError) -> Unit,
) {
Analytics.send(IntroductionProcess.CardWasScanned())
val globalState = store.state.globalState
val tapWalletManager = globalState.tapWalletManager
tapWalletManager.updateConfigManager(scanResponse)
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(scanResponse.card.batchId))
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (scanResponse.cardTypesResolver.isSaltPay()) {
@ -204,25 +204,24 @@ object ScanCardProcessor {
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
is Result.Failure -> {
SaltPayExceptionHandler.handle(result.error)
delay(DELAY_SDK_DIALOG_CLOSE)
SaltPayExceptionHandler.handle(result.error)
onFailure(TangemSdkError.ExceptionError(result.error))
onProgressStateChange(false)
}
}
}
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
if (scanResponse.card.backupStatus?.isActive == false) {
showSaltPayTapVisaLogoCardDialog()
val error = SaltPayActivationError.PutVisaCard
SaltPayExceptionHandler.handle(error)
onFailure(TangemSdkError.ExceptionError(error))
} else {
onSuccess(scanResponse)
}
onProgressStateChange(false)
}
} else {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
@ -238,21 +237,11 @@ object ScanCardProcessor {
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(scanResponse)
onProgressStateChange(false)
}
}
}
}
private fun showSaltPayTapVisaLogoCardDialog() {
store.dispatchDialogShow(
AppDialog.SimpleOkDialogRes(
headerId = R.string.saltpay_error_empty_backup_title,
messageId = R.string.saltpay_error_empty_backup_message,
),
)
}
private suspend inline fun navigateTo(
screen: AppScreen,
onProgressStateChange: (showProgress: Boolean) -> Unit,

View file

@ -48,7 +48,10 @@ internal class DefaultTotalFiatBalanceCalculator : TotalFiatBalanceCalculator {
is WalletDataModel.SameCurrencyTransactionInProgress,
is WalletDataModel.TransactionInProgress,
is WalletDataModel.NoAccount,
-> TotalFiatBalanceStatus.Loaded
-> {
if (walletData.fiatRate == null) TotalFiatBalanceStatus.Error
else TotalFiatBalanceStatus.Loaded
}
is WalletDataModel.Unreachable,
is WalletDataModel.MissedDerivation,
-> TotalFiatBalanceStatus.Error

View file

@ -22,6 +22,7 @@ interface WalletStoresManager {
* @return [Flow] with [WalletStoreModel] list
* */
fun get(userWalletId: UserWalletId): Flow<List<WalletStoreModel>>
suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel>
/**
* Delete [WalletStoreModel]s associated with provided [UserWalletId]s

View file

@ -44,6 +44,10 @@ internal class DefaultWalletStoresManager(
.distinctUntilChanged()
}
override suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
return walletStoresRepository.getSync(userWalletId)
}
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
return walletStoresRepository.delete(userWalletsIds)
.flatMap { walletManagersRepository.delete(userWalletsIds) }

View file

@ -17,6 +17,10 @@ internal class DummyWalletStoresManager : WalletStoresManager {
return emptyFlow()
}
override suspend fun getSync(userWalletId: UserWalletId): List<WalletStoreModel> {
return emptyList()
}
override suspend fun delete(userWalletsIds: List<UserWalletId>): CompletionResult<Unit> {
return CompletionResult.Success(Unit)
}

View file

@ -26,6 +26,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletAmountsRepository
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStore
import com.tangem.tap.domain.walletStores.repository.implementation.utils.replaceWalletStores
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithDemoAmounts
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithError
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithFiatRates
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithMissedDerivation
@ -33,6 +34,7 @@ import com.tangem.tap.domain.walletStores.repository.implementation.utils.update
import com.tangem.tap.domain.walletStores.repository.implementation.utils.updateWithUnreachable
import com.tangem.tap.domain.walletStores.storage.WalletManagerStorage
import com.tangem.tap.domain.walletStores.storage.WalletStoresStorage
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.filterByCoin
@ -208,6 +210,8 @@ internal class DefaultWalletAmountsRepository(
updateWalletStoreWithAmounts(
walletStore = walletStore,
updatedWallet = walletManager.wallet,
// FIXME: move DemoHelper to Demo core module maybe
isDemo = DemoHelper.isDemoCardId(scanResponse.card.cardId),
)
}
.flatMap { fetchWalletStoreRentIfNeeded(walletStore, walletManager) }
@ -297,6 +301,7 @@ internal class DefaultWalletAmountsRepository(
private suspend fun updateWalletStoreWithAmounts(
walletStore: WalletStoreModel,
updatedWallet: Wallet,
isDemo: Boolean,
) = withContext(Dispatchers.Default) {
Timber.d(
"""
@ -310,7 +315,11 @@ internal class DefaultWalletAmountsRepository(
prevState.replaceWalletStore(
walletStoreToUpdate = walletStore,
update = {
it.updateWithAmounts(wallet = updatedWallet)
if (isDemo) {
it.updateWithDemoAmounts(wallet = updatedWallet)
} else {
it.updateWithAmounts(wallet = updatedWallet)
}
},
)
}

View file

@ -7,6 +7,7 @@ import com.tangem.common.core.TangemError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.getPendingTransactions
import java.math.BigDecimal
@ -74,12 +75,25 @@ internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
)
}
internal fun WalletDataModel.updateWithDemoAmount(wallet: Wallet): WalletDataModel {
val amount = DemoHelper.config.getBalance(wallet.blockchain)
return this.copy(
status = WalletDataModel.VerifiedOnline(amount = amount.value ?: BigDecimal.ZERO),
)
}
internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithAmount(wallet)
}
}
internal fun List<WalletDataModel>.updateWithDemoAmounts(wallet: Wallet): List<WalletDataModel> {
return this.map { walletData ->
walletData.updateWithDemoAmount(wallet)
}
}
internal fun WalletDataModel.updateWithError(
wallet: Wallet,
error: TangemError,
@ -164,7 +178,7 @@ internal fun List<WalletDataModel>.updateWithSelf(
val updatedWalletsData = arrayListOf<WalletDataModel>()
newWalletsData.forEach { newWalletData ->
val walletDataToUpdate = oldWalletsData.find(newWalletData::isSameWalletData)
val walletDataToUpdate = oldWalletsData.firstOrNull(newWalletData::isSameWalletData)
if (walletDataToUpdate != null) {
updatedWalletsData.add(walletDataToUpdate.updateWithSelf(newWalletData))
} else {
@ -176,5 +190,5 @@ internal fun List<WalletDataModel>.updateWithSelf(
}
internal fun WalletDataModel.isSameWalletData(other: WalletDataModel): Boolean {
return currency == other.currency
return this.currency == other.currency
}

View file

@ -49,6 +49,14 @@ internal fun WalletStoreModel.updateWithAmounts(
)
}
internal fun WalletStoreModel.updateWithDemoAmounts(
wallet: Wallet,
): WalletStoreModel {
return this.copy(
walletsData = walletsData.updateWithDemoAmounts(wallet = wallet),
)
}
internal fun WalletStoreModel.updateWithFiatRates(
rates: Map<String, Double>,
): WalletStoreModel {
@ -96,26 +104,30 @@ private inline fun List<WalletStoreModel>.replaceWalletStores(
walletStoresToUpdate.forEach { walletStoreToUpdate ->
val index = mutableStores.indexOfFirst(walletStoreToUpdate::isSameWalletStore)
// Can be possible if user hides wallet store when it's tokens is loading
if (index == -1) return@forEach
val currentWalletStore = mutableStores[index]
val updatedWalletStore = update(currentWalletStore)
if (currentWalletStore != updatedWalletStore) {
Timber.d(
"""
Update wallet store in storage
|- User wallet ID: ${updatedWalletStore.userWalletId}
|- Blockchain: ${updatedWalletStore.blockchain}
""".trimIndent(),
)
if (currentWalletStore != updatedWalletStore) {
Timber.d(
"""
Update wallet store in storage
|- User wallet ID: ${updatedWalletStore.userWalletId}
|- Blockchain: ${updatedWalletStore.blockchain}
""".trimIndent(),
)
mutableStores[index] = updatedWalletStore
mutableStores[index] = updatedWalletStore
}
}
}
return mutableStores
}
internal fun WalletStoreModel.isSameWalletStore(other: WalletStoreModel): Boolean {
return this.blockchain == other.blockchain &&
return this.userWalletId == other.userWalletId &&
this.blockchain == other.blockchain &&
this.derivationPath == other.derivationPath
}

View file

@ -140,8 +140,8 @@ class DetailsMiddleware {
}
}
.doOnFailure { error ->
(error as? TangemSdkError)?.let { sdkError ->
Analytics.send(Settings.CardSettings.FactoryResetFinished(sdkError))
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.FactoryResetFinished(error))
}
}
}
@ -179,7 +179,8 @@ class DetailsMiddleware {
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Success)
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
val error = result.error
if (error is TangemSdkError && error !is TangemSdkError.UserCancelled) {
Analytics.send(Settings.CardSettings.SecurityModeChanged(paramValue, error))
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)

View file

@ -12,7 +12,7 @@ data class DetailsScreenState(
val onItemsClick: (SettingsElement) -> Unit,
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
) {
val appNameRes: Int = R.string.app_name
val appNameRes: Int = R.string.tangem_app_name
}
@Immutable
@ -44,7 +44,7 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook)
object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram)
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github)
object Youtube : SocialNetwork("Youtube", R.drawable.ic_youtube)
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube)
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin)
}
@ -55,7 +55,7 @@ object TangemSocialAccounts {
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
)
val accountsRu: List<SocialNetworkLink> = listOf(
@ -64,7 +64,7 @@ object TangemSocialAccounts {
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.Youtube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
)
}

View file

@ -62,6 +62,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
private fun handleClickingSettingsItem(item: SettingsElement) {
when (item) {
SettingsElement.WalletConnect -> {
Analytics.send(Settings.ButtonWalletConnect())
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
SettingsElement.Chat -> {

View file

@ -9,7 +9,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
@ -23,6 +25,7 @@ class WalletConnectFragment : Fragment(), StoreSubscriber<WalletConnectState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(WalletConnect.ScreenOpened())
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(android.R.transition.fade)
exitTransition = inflater.inflateTransition(android.R.transition.fade)

View file

@ -83,7 +83,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
homeState,
onScanButtonClick = {
Analytics.send(IntroductionProcess.ButtonScanCard())
store.dispatch(HomeAction.ReadCard)
store.dispatch(HomeAction.ReadCard())
},
onShopButtonClick = {
Analytics.send(IntroductionProcess.ButtonBuyCards())

View file

@ -97,6 +97,7 @@ fun RowScope.ProgressButton(
.weight(1f)
.height(48.dp),
onClick = onClick,
enabled = !inProgress,
colors = ButtonDefaults.textButtonColors(
backgroundColor = backgroundColor,
contentColor = contentColor,

View file

@ -1,15 +1,18 @@
package com.tangem.tap.features.home.redux
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.entities.IndeterminateProgressButton
import org.rekotlin.Action
sealed class HomeAction : Action {
object Init : HomeAction()
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
// from ui
object ReadCard : HomeAction()
data class ReadCard(
val analyticsEvent: AnalyticsEvent? = IntroductionProcess.CardWasScanned(),
) : HomeAction()
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
data class GoToShop(val userCountryCode: String?) : HomeAction()

View file

@ -3,12 +3,14 @@ package com.tangem.tap.features.home.redux
import com.tangem.common.doOnFailure
import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.postUiDelayBg
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -52,14 +54,11 @@ private fun handleHomeAction(action: Action) {
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUiDelayBg(ms = 700) { store.dispatch(HomeAction.ReadCard) }
}
is HomeAction.ReadCard -> {
readCard(action.analyticsEvent)
}
is HomeAction.ReadCard -> readCard()
is HomeAction.GoToShop -> {
Analytics.send(Shop.ScreenOpened())
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE -> store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
@ -68,19 +67,19 @@ private fun handleHomeAction(action: Action) {
}
}
private fun readCard() = scope.launch {
private fun readCard(analyticsEvent: AnalyticsEvent?) = scope.launch {
delay(timeMillis = 200)
tangemSdkManager.setAccessCodeRequestPolicy(
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
analyticsEvent = analyticsEvent,
onProgressStateChange = { showProgress ->
if (showProgress) {
changeButtonState(ButtonState.PROGRESS)
} else {
changeButtonState(ButtonState.ENABLED)
}
// else { //todo hide this because
// changeButtonState(ButtonState.ENABLED)
// }
},
onScanStateChange = { scanInProgress ->
store.dispatch(HomeAction.ScanInProgress(scanInProgress))
@ -92,25 +91,32 @@ private fun readCard() = scope.launch {
scope.launch {
if (preferencesStorage.shouldSaveUserWallets) {
val userWallet = UserWalletBuilder(scanResponse).build() ?: return@launch
userWalletsListManager.save(userWallet).doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.onCardScanned(scanResponse)
}.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
}.doOnResult {
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
}
userWalletsListManager.save(userWallet)
.doOnFailure { error ->
Timber.e(error, "Unable to save user wallet")
store.onCardScanned(scanResponse)
}
.doOnSuccess {
scope.launch { store.onUserWalletSelected(userWallet) }
}
.doOnResult {
navigateTo(AppScreen.Wallet)
}
} else {
store.onCardScanned(scanResponse)
changeButtonState(ButtonState.ENABLED)
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet))
navigateTo(AppScreen.Wallet)
}
}
},
)
}
private suspend fun navigateTo(appScreen: AppScreen) {
store.dispatchOnMain(NavigationAction.NavigateTo(appScreen))
delay(200)
changeButtonState(ButtonState.ENABLED)
}
private fun changeButtonState(state: ButtonState) {
store.dispatchOnMain(HomeAction.ChangeScanCardButtonState(IndeterminateProgressButton(state)))
}

View file

@ -15,9 +15,6 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
is HomeAction.ScanInProgress -> {
state = state.copy(scanInProgress = action.scanInProgress)
}
is HomeAction.ShouldScanCardOnResume -> {
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
}
is HomeAction.ChangeScanCardButtonState -> {
state = state.copy(btnScanState = action.state)
}

View file

@ -6,7 +6,6 @@ import com.tangem.tap.features.wallet.redux.ProgressState
import org.rekotlin.StateType
data class HomeState(
val shouldScanCardOnResume: Boolean = false,
val scanInProgress: Boolean = false,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
) : StateType {

View file

@ -42,7 +42,7 @@ class AddressInfoBottomSheetDialog(
override fun show() {
super.show()
Analytics.send(Token.Recieve.ScreenOpened())
Analytics.send(Token.Receive.ScreenOpened())
showData(data = stateDialog.addressData)
}
@ -54,12 +54,12 @@ class AddressInfoBottomSheetDialog(
imvQrCode.setImageBitmap(data.qrCode)
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
Analytics.send(Token.Recieve.ButtonCopyAddress())
Analytics.send(Token.Receive.ButtonCopyAddress())
context.copyToClipboard(data.address)
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btnFlShare.setOnClickListener {
Analytics.send(Token.Recieve.ButtonShareAddress())
Analytics.send(Token.Receive.ButtonShareAddress())
store.dispatchShare(data.shareUrl)
}
val blockchain = stateDialog.currency.blockchain

View file

@ -4,6 +4,8 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.core.view.MenuProvider
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
@ -19,6 +21,7 @@ class OnboardingMenuProvider : MenuProvider {
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
R.id.menu_item_chat_support -> {
Analytics.send(Onboarding.ButtonChat())
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
true
}

View file

@ -175,16 +175,16 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
val topUpUrl = walletManager.getTopUpUrl() ?: return
val blockchain = walletManager.wallet.blockchain
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl, blockchain)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
return
}
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
}
is OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
@ -10,6 +9,7 @@ import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -27,8 +27,11 @@ import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -103,10 +106,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!scanResponse.twinsIsTwinned() -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
TwinCardsStep.CreateFirstWallet
}
!scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
@ -147,18 +147,23 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
Analytics.send(Onboarding.Twins.ScreenOpened())
preferencesStorage.saveTwinsOnboardingShown()
}
TwinCardsStep.TopUpWallet -> store.dispatch(TwinCardsAction.Balance.Update)
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
TwinCardsStep.CreateFirstWallet -> {
Analytics.send(Onboarding.CreateWallet.ScreenOpened())
Analytics.send(Onboarding.Twins.SetupStarted())
scope.launch {
userWalletsListManager.delete(
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
)
}
}
TwinCardsStep.TopUpWallet -> {
Analytics.send(Onboarding.Topup.ScreenOpened())
store.dispatch(TwinCardsAction.Balance.Update)
}
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
TwinCardsStep.None,
TwinCardsStep.Warning,
TwinCardsStep.CreateSecondWallet,
@ -167,7 +172,6 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
Analytics.send(Onboarding.Twins.SetupStarted())
val manager = TwinCardsManager(
card = getScanResponse().card,
assetReader = action.reader,
@ -288,12 +292,23 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val topUpUrl = twinCardsState.walletManager?.getTopUpUrl() ?: return
val walletManager = twinCardsState.walletManager.guard {
store.dispatchDebugErrorNotification("NPE: WalletManager")
return
}
val currencyType = AnalyticsParam.CurrencyType.Blockchain(Blockchain.Bitcoin)
val topUpUrl = walletManager.getTopUpUrl() ?: return
val blockchain = walletManager.wallet.blockchain
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(topUpUrl)
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
val dialogData = WalletDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog(dialogData))
} else {
store.dispatchOpenUrl(topUpUrl)
}
}
TwinCardsAction.Done -> {
val scanResponse = getScanResponse()

View file

@ -154,7 +154,7 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
if (scanResponse == null) {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(HomeAction.ReadCard)
store.dispatch(HomeAction.ReadCard())
} else {
val backupState = store.state.onboardingWalletState.backupState
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)

View file

@ -38,7 +38,7 @@ class GnosisRegistrator(
}
private val otpProcessorContractAddress: String = when (walletManager.wallet.blockchain) {
Blockchain.SaltPay -> "0x3B4397C817A26521Df8bD01a949AFDE2251d91C2"
Blockchain.SaltPay -> "0xc659f4FEd7A84a188F54cBA4A7a49D77c1a20522"
else -> throw IllegalArgumentException("GnosisRegistrator supports only the SaltPay blockchain")
}

View file

@ -11,33 +11,34 @@ import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
object SaltPayExceptionHandler {
fun handle(throwable: Throwable) {
when (throwable) {
is SaltPayActivationError -> {
val dialog = when (throwable) {
is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas
else -> SaltPayDialog.Activation.OnError(throwable)
}
store.dispatchDialogShow(dialog)
}
is TangemSdkError -> {
when (throwable) {
is TangemSdkError.NetworkError -> {
val message = throwable.customMessage
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
class SaltPayExceptionHandler {
companion object {
fun handle(throwable: Throwable) {
when (throwable) {
is SaltPayActivationError -> {
val dialog = when (throwable) {
is SaltPayActivationError.NoGas -> SaltPayDialog.Activation.NoGas
is SaltPayActivationError.PutVisaCard -> SaltPayDialog.Activation.PutVisaCard
else -> SaltPayDialog.Activation.OnError(throwable)
}
else -> {
// do nothing
store.dispatchDialogShow(dialog)
}
is TangemSdkError -> {
when (throwable) {
is TangemSdkError.NetworkError -> {
val message = (throwable).customMessage
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
else -> Unit
}
}
}
is BlockchainSdkError -> {
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(throwable.customMessage))
}
else -> {
val message = throwable.localizedMessage ?: "SaltPay unknown error"
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
is BlockchainSdkError -> {
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(throwable.customMessage))
}
else -> {
val message = throwable.localizedMessage ?: "SaltPay unknown error"
store.dispatchDialogShow(AppDialog.SimpleOkErrorDialog(message))
}
}
}
}

View file

@ -11,15 +11,17 @@ import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
object InterruptOnboardingDialog {
fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.onboarding_exit_alert_title))
setMessage(context.getString(R.string.onboarding_exit_alert_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
class InterruptOnboardingDialog {
companion object {
fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(context.getString(R.string.onboarding_exit_alert_title))
setMessage(context.getString(R.string.onboarding_exit_alert_message))
setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() }
setNegativeButton(R.string.common_cancel) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
setCancelable(false)
}.create()
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog
import android.app.Dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class PutVisaCardDialog {
companion object {
fun create(context: Context): Dialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.saltpay_error_empty_backup_title)
setMessage(R.string.saltpay_error_empty_backup_message)
setPositiveButton(R.string.common_ok) { _, _ -> }
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPa
sealed class SaltPayDialog : StateDialog {
sealed class Activation : SaltPayDialog() {
object NoGas : SaltPayDialog()
object PutVisaCard : SaltPayDialog()
data class OnError(val error: SaltPayActivationError) : SaltPayDialog()
}
}

View file

@ -49,6 +49,8 @@ sealed class SaltPayActivationError(
object NoFundsToClaim : SaltPayActivationError(subCode = 15)
object ClaimTransactionFailed : SaltPayActivationError(subCode = 16)
object PutVisaCard : SaltPayActivationError(18)
companion object {
const val EMPTY_MESSAGE = ""
}

View file

@ -285,7 +285,7 @@ suspend fun SaltPayActivationManager.update(
return Result.Failure(ex)
}
// checkGasIfNeeded(this, newStep).successOr { return it }
checkGasIfNeeded(this, newStep).successOr { return it }
Timber.d("update: success: %s", newStep)
return Result.Success(newStep)

View file

@ -28,9 +28,9 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWa
private val viewModel by viewModels<SaveWalletViewModel>()
override fun onDismiss(dialog: DialogInterface) {
viewModel.dismiss()
super.onDismiss(dialog)
override fun onCancel(dialog: DialogInterface) {
viewModel.cancelOrClose()
super.onCancel(dialog)
}
@Composable
@ -51,7 +51,10 @@ internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment<SaveWa
SaveWalletScreenContent(
showProgress = state.showProgress,
onSaveWalletClick = viewModel::saveWallet,
onCloseClick = this@SaveWalletBottomSheetFragment::dismissAllowingStateLoss,
onCloseClick = {
viewModel.cancelOrClose()
dismissAllowingStateLoss()
},
)
SnackbarHost(

View file

@ -25,7 +25,7 @@ internal class SaveWalletViewModel : ViewModel(), StoreSubscriber<SaveWalletStat
store.dispatch(SaveWalletAction.Save)
}
fun dismiss() {
fun cancelOrClose(){
store.dispatch(SaveWalletAction.Dismiss)
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.features.shop.redux
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Shop
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.redux.AppState
@ -111,6 +113,7 @@ private fun handle(action: Action) {
}
}
ShopAction.StartWebCheckout -> {
Analytics.send(Shop.Redirected(null))
store.dispatchOpenUrl(shopService.getCheckoutUrl(shopState.selectedProduct))
store.dispatch(ShopAction.FinishSuccessfulOrder)
}

View file

@ -44,7 +44,6 @@ class ShopFragment : BaseStoreFragment(R.layout.fragment_shop), StoreSubscriber<
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(Shop.ScreenOpened())
activity?.onBackPressedDispatcher?.addCallback(
this,

View file

@ -49,7 +49,6 @@ class AddTokensFragment : BaseFragment(R.layout.fragment_add_tokens), StoreSubsc
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
Analytics.send(ManageTokens.ScreenOpened())
addBackPressHandler(this)
}

View file

@ -7,6 +7,7 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.common.CardDTO
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.NotificationAction
@ -102,6 +103,7 @@ sealed class WalletAction : Action {
data class SetPrimaryToken(val token: Token) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
object ScheduleCheckForMissingDerivation : MultiWallet()
data class AddMissingDerivations(val blockchains: List<BlockchainNetwork>) : MultiWallet()
object ScanToGetDerivations : MultiWallet()
}
@ -144,7 +146,7 @@ sealed class WalletAction : Action {
object Failure : WalletAction()
}
object Scan : WalletAction()
data class Scan(val onScanSuccessEvent: AnalyticsEvent?) : WalletAction()
data class Send(val amount: Amount? = null) : WalletAction()

View file

@ -29,6 +29,9 @@ import org.rekotlin.StateType
import java.math.BigDecimal
import kotlin.properties.ReadOnlyProperty
/**
* @property derivationsCheckIsScheduled - used only for analytics
*/
data class WalletState(
val cardId: String = "",
val state: ProgressState = ProgressState.Done,
@ -46,6 +49,7 @@ data class WalletState(
val totalBalance: TotalBalance? = null,
val showBackupWarning: Boolean = false,
val missingDerivations: List<BlockchainNetwork> = emptyList(),
val derivationsCheckIsScheduled: Boolean = false,
val loadingUserTokens: Boolean = false,
val walletCardsCount: Int? = null,
) : StateType {

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.toFiatRateString
@ -10,7 +10,6 @@ import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.ProgressState
@ -24,11 +23,9 @@ import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.BigDecimal
internal fun List<WalletStoreModel>.mapToReduxModels(
isMultiWalletAllowed: Boolean,
): List<WalletStore> {
internal fun List<WalletStoreModel>.mapToReduxModels(): List<WalletStore> {
return this.map { walletStoreModel ->
walletStoreModel.mapToReduxModel(isMultiWalletAllowed)
walletStoreModel.mapToReduxModel()
}
}
@ -44,103 +41,151 @@ internal fun TotalFiatBalance.mapToReduxModel(): TotalBalance {
)
}
internal fun WalletStoreModel.mapToReduxModel(
isMultiWalletAllowed: Boolean,
): WalletStore {
internal fun WalletStoreModel.mapToReduxModel(): WalletStore {
val appCurrencySymbol = store.state.globalState.appCurrency.symbol
return WalletStore(
walletManager = walletManager,
blockchainNetwork = blockchainNetwork,
walletsData = walletsData.mapToReduxModel(isMultiWalletAllowed, walletRent),
walletsData = walletsData.mapToReduxModels(walletRent, appCurrencySymbol),
)
.updateTokenModels(blockchainWalletData.status.amount)
.setupIfHadCardSingleToken(
blockchain = blockchain,
walletsDataModel = walletsData,
appCurrencySymbol = appCurrencySymbol,
blockchainWalletData = blockchainWalletData.mapToReduxModel(
walletRent = walletRent,
appCurrencySymbol = appCurrencySymbol,
),
)
}
@Suppress("LongMethod", "ComplexMethod")
private fun List<WalletDataModel>.mapToReduxModel(
isMultiWalletAllowed: Boolean,
private fun List<WalletDataModel>.mapToReduxModels(
walletRent: WalletStoreModel.WalletRent?,
appCurrencySymbol: String,
): List<WalletData> {
return this.map { walletDataModel ->
with(walletDataModel) {
val amount = status.amount
val amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
val appCurrency = store.state.globalState.appCurrency
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrency.symbol)
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrency.symbol)
val blockchainAmountValue = getBlockchainAmount()
WalletData(
currency = currency,
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
WalletAddresses(
selectedAddress = selectedAddress,
list = walletAddresses,
)
},
existentialDepositString = existentialDeposit?.toPlainString(),
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
pendingTransactions = status.pendingTransactions,
mainButton = WalletMainButton.SendButton(
enabled = !blockchainAmountValue.isZero() &&
!status.amount.isZero() &&
status.pendingTransactions.isEmpty(),
),
walletRent = walletRent?.let {
WalletRent(
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} " +
currency.blockchain.currency,
)
},
currencyData = BalanceWidgetData(
status = when (status) {
is WalletDataModel.Loading -> BalanceStatus.Loading
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
is WalletDataModel.SameCurrencyTransactionInProgress ->
BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable -> BalanceStatus.Unreachable
is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
},
currency = currency.currencyName,
currencySymbol = currency.currencySymbol,
blockchainAmount = blockchainAmountValue,
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
token = when {
!isMultiWalletAllowed && currency is Currency.Token -> {
TokenData(
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
tokenSymbol = currency.currencySymbol,
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
)
}
else -> null
},
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
?.amountToCreateAccount
?.toString(),
errorMessage = status.errorMessage,
),
)
}
walletDataModel.mapToReduxModel(walletRent, appCurrencySymbol)
}
}
private fun WalletDataModel.getBlockchainAmount(): BigDecimal {
val walletStore = store.state.walletState.getWalletStore(currency) ?: return BigDecimal.ZERO
return walletStore.walletManager?.wallet?.amounts?.get(AmountType.Coin)?.value ?: BigDecimal.ZERO
private fun WalletDataModel.mapToReduxModel(
walletRent: WalletStoreModel.WalletRent?,
appCurrencySymbol: String,
): WalletData {
val amount = status.amount
val amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
)
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrencySymbol)
val fiatRateFormatted = fiatRate?.toFiatRateString(appCurrencySymbol)
return WalletData(
currency = currency,
walletAddresses = walletAddresses.getOrNull(0)?.let { selectedAddress ->
WalletAddresses(
selectedAddress = selectedAddress,
list = walletAddresses,
)
},
existentialDepositString = existentialDeposit?.toPlainString(),
fiatRate = fiatRate,
fiatRateString = fiatRateFormatted,
pendingTransactions = status.pendingTransactions,
mainButton = WalletMainButton.SendButton(
enabled = !status.amount.isZero() && status.pendingTransactions.isEmpty(),
),
walletRent = walletRent?.let {
WalletRent(
minRentValue = "${it.rent.stripZeroPlainString()} ${currency.blockchain.currency}",
rentExemptValue = "${it.exemptionAmount.stripZeroPlainString()} ${currency.blockchain.currency}",
)
},
currencyData = BalanceWidgetData(
status = when (status) {
is WalletDataModel.Loading -> BalanceStatus.Loading
is WalletDataModel.NoAccount -> BalanceStatus.NoAccount
is WalletDataModel.SameCurrencyTransactionInProgress -> BalanceStatus.SameCurrencyTransactionInProgress
is WalletDataModel.TransactionInProgress -> BalanceStatus.TransactionInProgress
is WalletDataModel.Unreachable -> BalanceStatus.Unreachable
is WalletDataModel.MissedDerivation -> BalanceStatus.MissedDerivation
is WalletDataModel.VerifiedOnline -> BalanceStatus.VerifiedOnline
},
currency = currency.currencyName,
currencySymbol = currency.currencySymbol,
blockchainAmount = BigDecimal.ZERO,
amount = amount,
amountFormatted = amountFormatted,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted,
token = null,
amountToCreateAccount = (status as? WalletDataModel.NoAccount)
?.amountToCreateAccount
?.toString(),
errorMessage = status.errorMessage,
),
)
}
private fun WalletStore.updateTokenModels(blockchainAmount: BigDecimal): WalletStore {
val updatedTokensWalletData = walletsData.filter { it.currency.isToken() }.map {
it.copy(
mainButton = when (it.mainButton) {
is WalletMainButton.SendButton -> {
WalletMainButton.SendButton(it.mainButton.enabled && !blockchainAmount.isZero())
}
is WalletMainButton.CreateWalletButton -> it.mainButton
},
currencyData = it.currencyData.copy(
blockchainAmount = blockchainAmount,
),
)
}
return updateWallets(updatedTokensWalletData)
}
private fun WalletStore.setupIfHadCardSingleToken(
blockchain: Blockchain,
walletsDataModel: List<WalletDataModel>,
appCurrencySymbol: String,
blockchainWalletData: WalletData,
): WalletStore {
// Card with single token contains only 2 model - blockchain and token
if (walletsData.size != 2) return this
val cardSingleTokenWalletData = walletsDataModel.firstOrNull {
it.currency.isToken() && it.currency.blockchain == blockchain && it.isCardSingleToken
} ?: return this
val blockchainWalletDataWithSingleToken = blockchainWalletData.copy(
currencyData = blockchainWalletData.currencyData.copy(
token = cardSingleTokenWalletData.toTokenData(appCurrencySymbol),
),
)
return updateWallets(listOf(blockchainWalletDataWithSingleToken))
}
private fun WalletDataModel.toTokenData(appCurrencySymbol: String): TokenData {
val amount = status.amount
val fiatAmount = fiatRate?.let { status.amount.toFiatValue(it) }
return TokenData(
amount = amount,
amountFormatted = amount.toFormattedCurrencyString(
decimals = currency.decimals,
currency = currency.currencySymbol,
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount
?.takeIf { !status.isErrorStatus }
?.toFormattedFiatValue(appCurrencySymbol),
tokenSymbol = currency.currencySymbol,
fiatRate = fiatRate,
fiatRateString = fiatRate?.toFiatRateString(appCurrencySymbol),
)
}

View file

@ -8,7 +8,6 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token.ButtonRemoveToken
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
@ -174,12 +173,15 @@ class MultiWalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
}
}
is WalletAction.MultiWallet.AddMissingDerivations -> {
scope.launch { handleBasicAnalyticsEvent() }
}
is WalletAction.MultiWallet.ScanToGetDerivations -> {
val selectedWallet = userWalletsListManager.selectedUserWalletSync
if (selectedWallet != null) {
scanAndUpdateCard(selectedWallet, walletState)
} else {
store.dispatch(WalletAction.Scan)
store.dispatch(WalletAction.Scan(onScanSuccessEvent = null))
}
}
else -> {}
@ -190,8 +192,9 @@ class MultiWalletMiddleware {
selectedUserWallet: UserWallet,
state: WalletState?,
) = scope.launch(Dispatchers.Default) {
Analytics.send(MainScreen.CardWasScanned())
dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
ScanCardProcessor.scan(
analyticsEvent = null,
cardId = selectedUserWallet.cardId,
additionalBlockchainsToDerive = state?.missingDerivations?.map { it.blockchain },
) { scanResponse ->

View file

@ -48,21 +48,22 @@ class TradeCryptoMiddleware {
state: () -> AppState?,
action: WalletAction.TradeCryptoAction.Buy,
) {
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog())
return
}
val selectedWalletData = store.state.walletState.selectedWalletData ?: return
val card = store.state.globalState.scanResponse?.card ?: return
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val exchangeManager = store.state.globalState.exchangeManager
val appCurrency = store.state.globalState.appCurrency
val currency = selectedWalletData.currency
Analytics.send(Token.ButtonBuy(AnalyticsParam.CurrencyType.Currency(currency)))
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
val walletManager = store.state.walletState.getWalletManager(currency)

View file

@ -12,6 +12,9 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.operations.attestation.Attestation
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.analytics.converters.BasicEventsPreChecker
import com.tangem.tap.common.analytics.converters.BasicEventsSourceData
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Token
import com.tangem.tap.common.extensions.copyToClipboard
@ -33,6 +36,7 @@ import com.tangem.tap.domain.failedRates
import com.tangem.tap.domain.loadedRates
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
@ -54,10 +58,12 @@ import com.tangem.tap.tangemSdkManager
import com.tangem.tap.totalFiatBalanceCalculator
import com.tangem.tap.userWalletsListManager
import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.tap.walletStoresManager
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -109,6 +115,7 @@ class WalletMiddleware {
walletState.walletManagers.map { walletManager ->
async { globalState.tapWalletManager.loadWalletData(walletManager) }
}.awaitAll()
handleBasicAnalyticsEvent()
} else {
val walletManager = walletState.getWalletManager(action.blockchain)
?: action.walletManager
@ -194,8 +201,11 @@ class WalletMiddleware {
}
}
is WalletAction.Scan -> {
store.dispatch(HomeAction.ShouldScanCardOnResume(true))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
scope.launch {
delay(700)
store.dispatchOnMain(HomeAction.ReadCard(action.onScanSuccessEvent))
}
}
is WalletAction.LoadCardInfo -> {
val attestationFailed = action.card.attestation.status == Attestation.Status.Failed
@ -249,10 +259,12 @@ class WalletMiddleware {
}
}
is WalletAction.CopyAddress -> {
Analytics.send(Token.Receive.ButtonCopyAddress())
action.context.copyToClipboard(action.address)
store.dispatch(WalletAction.CopyAddress.Success)
}
is WalletAction.ShareAddress -> {
Analytics.send(Token.Receive.ButtonShareAddress())
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
@ -274,6 +286,9 @@ class WalletMiddleware {
} else {
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.state.walletState.selectedWalletData?.currency?.let { currency ->
Analytics.send(Token.ButtonSend(AnalyticsParam.CurrencyType.Currency(currency)))
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
@ -286,6 +301,7 @@ class WalletMiddleware {
}
is WalletAction.UserWalletChanged -> Unit
is WalletAction.WalletStoresChanged -> {
store.dispatchOnMain(WalletAction.MultiWallet.ScheduleCheckForMissingDerivation)
updateWalletStores(action.walletStores, walletState)
fetchTotalFiatBalance(action.walletStores)
findMissedDerivations(action.walletStores)
@ -303,7 +319,7 @@ class WalletMiddleware {
}
}
val reduxWalletStores = wallStores.mapToReduxModels(state.isMultiwalletAllowed)
val reduxWalletStores = wallStores.mapToReduxModels()
store.dispatchOnMain(
WalletAction.WalletStoresChanged.UpdateWalletStores(
reduxWalletStores = reduxWalletStores,
@ -361,7 +377,7 @@ class WalletMiddleware {
}
else -> {
Analytics.send(MainScreen.ButtonScanCard())
store.dispatch(WalletAction.Scan)
store.dispatch(WalletAction.Scan(MainScreen.CardWasScanned()))
}
}
}
@ -481,4 +497,19 @@ class WalletMiddleware {
}
}
}
}
suspend fun handleBasicAnalyticsEvent() {
val scanResponse = store.state.globalState.scanResponse ?: return
val biometricsWalletDataModels = if (preferencesStorage.shouldSaveUserWallets) {
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let { userWalletId ->
walletStoresManager.getSync(userWalletId).map { it.walletsData }.flatten()
}
} else {
null
}
val converterData = BasicEventsSourceData(scanResponse, store.state.walletState, biometricsWalletDataModels)
BasicEventsPreChecker().tryToSend(converterData)
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.wallet.redux.models
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.wallet.R
@ -38,6 +37,6 @@ sealed interface WalletDialog : StateDialog {
}
data class RussianCardholdersWarningDialog(val data: Data?) : WalletDialog {
data class Data(val topUpUrl: String, val blockchain: Blockchain)
data class Data(val topUpUrl: String)
}
}

View file

@ -187,7 +187,13 @@ class MultiWalletReducer {
is WalletAction.MultiWallet.SetPrimaryToken -> state.copy(primaryToken = action.token)
is WalletAction.MultiWallet.SaveCurrencies -> state
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(showBackupWarning = action.show)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(missingDerivations = action.blockchains)
is WalletAction.MultiWallet.ScheduleCheckForMissingDerivation -> state.copy(
derivationsCheckIsScheduled = true,
)
is WalletAction.MultiWallet.AddMissingDerivations -> state.copy(
missingDerivations = action.blockchains,
derivationsCheckIsScheduled = false
)
is WalletAction.MultiWallet.BackupWallet -> state
is WalletAction.MultiWallet.ScanToGetDerivations -> state.copy(state = ProgressState.Loading)
}

View file

@ -16,6 +16,8 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.badoo.mvicore.DiffStrategy
import com.badoo.mvicore.ModelWatcher
import com.badoo.mvicore.modelWatcher
import com.tangem.common.doOnResult
import com.tangem.core.analytics.Analytics
@ -39,6 +41,8 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
@ -68,7 +72,13 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private val binding: FragmentWalletDetailsBinding by viewBinding(FragmentWalletDetailsBinding::bind)
private val walletDataWatcher = modelWatcher<WalletData> {
private val walletDataWatcher: ModelWatcher<WalletData> = modelWatcher {
val addressCardStrategy: DiffStrategy<WalletData> = { old, new ->
old.currency != new.currency ||
old.walletAddresses?.selectedAddress != new.walletAddresses?.selectedAddress ||
old.shouldShowMultipleAddress() != new.shouldShowMultipleAddress()
}
WalletData::pendingTransactions {
showPendingTransactionsIfPresent(it)
}
@ -78,25 +88,35 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
WalletData::currencyData {
setupBalanceData(it)
}
WalletData::walletAddresses { walletAddresses ->
setupCopyAndShareButtons(walletAddresses?.selectedAddress?.address)
}
WalletData::assembleWarnings { warnings ->
handleWarnings(warnings)
}
(WalletData::currencyData or WalletData::currency) { walletData ->
setupCurrency(walletData.currencyData, walletData.currency)
setupSwipeRefresh(walletData.currencyData, walletData.currency)
}
watch({ it }, addressCardStrategy) { walletData ->
setupAddressCard(
shouldShowMultipleAddress = walletData.shouldShowMultipleAddress(),
selectedAddress = walletData.walletAddresses?.selectedAddress,
currency = walletData.currency,
)
}
}
private val walletStateWatcher = modelWatcher<WalletState> {
(WalletState::selectedCurrency or WalletState::selectedWalletData) { state ->
val selectedWalletData = state.selectedWalletData
if (selectedWalletData != null) {
walletDataWatcher.invoke(selectedWalletData)
setupButtons(selectedWalletData, state.isExchangeServiceFeatureOn)
setupAddressCard(selectedWalletData)
handleWarnings(selectedWalletData)
private val walletStateWatcher: ModelWatcher<WalletState> = modelWatcher {
WalletState::selectedWalletData { selectedWallet ->
if (selectedWallet != null) {
walletDataWatcher.invoke(selectedWallet)
}
}
(WalletState::selectedCurrency or WalletState::isExchangeServiceFeatureOn) { state ->
if (state.selectedWalletData != null) {
setupButtons(state.selectedWalletData!!, state.isExchangeServiceFeatureOn)
(WalletState::selectedWalletData or WalletState::isExchangeServiceFeatureOn) { state ->
val selectedWallet = state.selectedWalletData
if (selectedWallet != null) {
setupButtonsRow(selectedWallet, state.isExchangeServiceFeatureOn)
}
}
(WalletState::state or WalletState::error) { state ->
@ -133,8 +153,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
override fun onStop() {
super.onStop()
store.unsubscribe(this)
walletDataWatcher.clear()
walletStateWatcher.clear()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -148,6 +166,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupTestActionButton()
}
override fun onDestroyView() {
super.onDestroyView()
clearWatchers()
}
private fun setupTransactionsRecyclerView() = with(binding) {
pendingTransactionAdapter = PendingTransactionsAdapter()
rvPendingTransaction.layoutManager = LinearLayoutManager(requireContext())
@ -243,23 +266,25 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
currencyData.status == BalanceStatus.Refreshing
}
private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) {
lWalletDetails.btnCopy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, requireContext()))
private fun setupCopyAndShareButtons(walletAddress: String?) {
binding.lWalletDetails.btnCopy.setOnClickListener {
if (walletAddress != null) {
store.dispatch(WalletAction.CopyAddress(walletAddress, requireContext()))
}
}
lWalletDetails.btnShare.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.ShareAddress(addressString, requireContext()))
binding.lWalletDetails.btnShare.setOnClickListener {
if (walletAddress != null) {
store.dispatch(WalletAction.ShareAddress(walletAddress, requireContext()))
}
}
}
rowButtons.updateButtonsVisibility(
private fun setupButtonsRow(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) {
binding.rowButtons.updateButtonsVisibility(
exchangeServiceFeatureOn = isExchangeServiceFeatureOn,
sendAllowed = selectedWallet.mainButton.enabled,
)
rowButtons.onTradeClick = {
binding.rowButtons.onTradeClick = {
store.dispatch(
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy,
@ -270,9 +295,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
}
private fun handleWarnings(selectedWallet: WalletData) = with(binding) {
private fun handleWarnings(warnings: List<WalletWarning>) = with(binding) {
val converter = WalletWarningConverter(requireContext())
val warningDetails = selectedWallet.assembleWarnings().map { converter.convert(it) }
val warningDetails = warnings.map { converter.convert(it) }
warningMessagesAdapter.submitList(warningDetails)
rvWarningMessages.show(warningDetails.isNotEmpty())
@ -293,51 +318,59 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
binding.rvPendingTransaction.show(pendingTransactions.isNotEmpty())
}
private fun setupAddressCard(state: WalletData) = with(binding.lWalletDetails) {
if (state.walletAddresses != null) {
if (state.shouldShowMultipleAddress() && state.currency is Currency.Blockchain) {
(cardBalance as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
private fun setupAddressCard(
shouldShowMultipleAddress: Boolean,
selectedAddress: AddressData?,
currency: Currency,
) = with(binding.lWalletDetails) {
if (selectedAddress == null) return@with
val checkedId =
MultipleAddressUiHelper.typeToId(state.walletAddresses.selectedAddress.type)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
setupAddressTypeChips(shouldShowMultipleAddress, selectedAddress, currency)
chipGroupAddressType.setOnCheckedChangeListener { group, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type =
MultipleAddressUiHelper.idToType(checkedId, state.currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chipGroupAddressType.hide()
}
tvAddress.text = state.walletAddresses.selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(
WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
requireContext(),
),
)
}
ivQrCode.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
tvAddress.text = selectedAddress.address
tvExplore.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(selectedAddress.exploreUrl, requireContext()))
}
ivQrCode.setImageBitmap(selectedAddress.shareUrl.toQrCode())
tvReceiveMessage.text = when (val currency = state.currency) {
is Currency.Blockchain -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol,
currency.blockchain.fullName,
)
is Currency.Token -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName,
)
tvReceiveMessage.text = when (currency) {
is Currency.Blockchain -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol,
currency.blockchain.fullName,
)
is Currency.Token -> tvReceiveMessage.getString(
id = R.string.address_qr_code_message_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName,
)
}
}
private fun setupAddressTypeChips(
shouldShowMultipleAddress: Boolean,
selectedAddress: AddressData,
currency: Currency,
) = with(binding.lWalletDetails) {
if (shouldShowMultipleAddress && currency is Currency.Blockchain) {
(cardBalance as? ViewGroup)?.beginDelayedTransition()
chipGroupAddressType.show()
chipGroupAddressType.fitChipsByGroupWidth()
val checkedId =
MultipleAddressUiHelper.typeToId(selectedAddress.type)
if (checkedId != View.NO_ID) chipGroupAddressType.check(checkedId)
chipGroupAddressType.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
val type =
MultipleAddressUiHelper.idToType(checkedId, currency.blockchain)
type?.let { store.dispatch(WalletAction.ChangeSelectedAddress(type)) }
}
} else {
chipGroupAddressType.hide()
}
}
@ -426,6 +459,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
inflater.inflate(R.menu.menu_wallet_details, menu)
}
private fun clearWatchers() {
walletDataWatcher.clear()
walletStateWatcher.clear()
}
private fun TextView.setWarningStatus(mainMessage: Int, error: String? = null) {
val text = getString(mainMessage).appendIfNotNull(error, "\nError: ")
setStatus(text, R.color.warning, R.drawable.ic_warning_small)

View file

@ -22,8 +22,6 @@ import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.tap.MainActivity
import com.tangem.tap.common.analytics.converters.BasicSignInEventConverter
import com.tangem.tap.common.analytics.converters.BasicTopUpEventConverter
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.show
@ -150,7 +148,6 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null || view == null) return
handleBasicAnalyticsEvent(state)
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
when {
@ -260,11 +257,4 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.shouldShowDetails) inflater.inflate(R.menu.menu_wallet, menu)
}
private fun handleBasicAnalyticsEvent(state: WalletState) {
val scanResponse = store.state.globalState.scanResponse ?: return
BasicSignInEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
BasicTopUpEventConverter(scanResponse).convert(state)?.let { Analytics.send(it) }
}
}

View file

@ -35,7 +35,7 @@ class WalletWarningConverter(
is WalletWarning.TransactionInProgress -> {
context.getString(
R.string.token_details_send_blocked_tx_format,
message.currencyName
message.currencyName,
)
}
is WalletWarning.Rent -> {

View file

@ -41,8 +41,6 @@ class RussianCardholdersWarningBottomSheetDialog(
binding?.btnYes?.setOnClickListener {
if (dialogData != null) {
val currencyType = AnalyticsParam.CurrencyType.Blockchain(dialogData.blockchain)
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
store.dispatchOpenUrl(dialogData.topUpUrl)
} else {
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.tap.common.analytics.events.MainScreen
import com.tangem.tap.common.analytics.events.ManageTokens
import com.tangem.tap.common.analytics.events.Portfolio
import com.tangem.tap.common.extensions.animateVisibility
import com.tangem.tap.common.extensions.formatAmountAsSpannedString
@ -136,6 +137,8 @@ class MultiWalletView : WalletView() {
derivationStyle = card.derivationStyle,
),
)
Analytics.send(ManageTokens.ScreenOpened())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
@ -72,8 +73,11 @@ class SingleWalletView : WalletView() {
}
private fun showPendingTransactionsIfPresent(pendingTransactions: List<PendingTransaction>) {
pendingTransactionAdapter.submitList(pendingTransactions)
binding?.rvPendingTransaction?.show(pendingTransactions.isNotEmpty())
val knownTransactions = pendingTransactions.filterNot {
it.type == PendingTransactionType.Unknown
}
pendingTransactionAdapter.submitList(knownTransactions)
binding?.rvPendingTransaction?.show(knownTransactions.isNotEmpty())
}
private fun setupBalance(state: WalletState, primaryWallet: WalletData) {

View file

@ -24,8 +24,8 @@ internal sealed interface WalletSelectorAction : Action {
val walletsStores: Map<UserWalletId, List<WalletStoreModel>>,
) : WalletSelectorAction
data class BalanceLoaded(
val userWalletModel: UserWalletModel,
data class BalancesLoaded(
val userWalletModels: List<UserWalletModel>,
) : WalletSelectorAction
object UnlockWithBiometry : WalletSelectorAction {

View file

@ -10,6 +10,7 @@ import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.analytics.paramsInterceptor.BatchIdParamsInterceptor
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -21,6 +22,7 @@ import com.tangem.tap.domain.model.WalletStoreModel
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
@ -35,7 +37,6 @@ import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
import java.math.BigDecimal
// Refactoring is coming
@Suppress("LargeClass")
@ -83,7 +84,7 @@ internal class WalletSelectorMiddleware {
is WalletSelectorAction.SelectedWalletChanged,
is WalletSelectorAction.UnlockWithBiometry.Error,
is WalletSelectorAction.UnlockWithBiometry.Success,
is WalletSelectorAction.BalanceLoaded,
is WalletSelectorAction.BalancesLoaded,
is WalletSelectorAction.IsLockedChanged,
is WalletSelectorAction.CloseError,
-> Unit
@ -103,28 +104,16 @@ internal class WalletSelectorMiddleware {
updatedWalletStores: Map<UserWalletId, List<WalletStoreModel>>,
state: WalletSelectorState,
) {
if (updatedWalletStores.isNotEmpty()) {
scope.launch(Dispatchers.Default) {
state.wallets
.associateWith { updatedWalletStores[it.id] }
.forEach { (wallet, walletStores) ->
val isWalletTokensEmpty = (wallet.type as? UserWalletModel.Type.MultiCurrency)?.tokensCount == 0
val updatedWallet = if (walletStores == null && isWalletTokensEmpty) {
wallet.copy(fiatBalance = TotalFiatBalance.Loaded(BigDecimal.ZERO))
} else {
wallet.updateWalletStoresAndCalculateFiatBalance(walletStores.orEmpty())
}
if (wallet != updatedWallet) {
store.dispatchOnMain(WalletSelectorAction.BalanceLoaded(updatedWallet))
}
}
if (updatedWalletStores.isNotEmpty()) scope.launch(Dispatchers.Default) {
val updatedWallets = state.wallets.updateWalletStoresAndCalculateFiatBalance(updatedWalletStores)
if (updatedWallets != state.wallets) {
store.dispatchOnMain(WalletSelectorAction.BalancesLoaded(updatedWallets))
}
}
}
private fun unlockWalletsWithBiometry() {
Analytics.send(MyWallets.Button.UnlockWithBiometrics)
Analytics.send(MyWallets.Button.UnlockWithBiometrics())
scope.launch {
userWalletsListManager.unlockWithBiometry()
@ -139,7 +128,7 @@ internal class WalletSelectorMiddleware {
}
private fun addWallet() = scope.launch {
Analytics.send(MyWallets.Button.ScanNewCard)
Analytics.send(MyWallets.Button.ScanNewCard())
val prevUseBiometricsForAccessCode = tangemSdkManager.useBiometricsForAccessCode()
@ -149,6 +138,7 @@ internal class WalletSelectorMiddleware {
)
ScanCardProcessor.scan(
analyticsEvent = MyWallets.CardWasScanned(),
onWalletNotCreated = {
// No need to rollback policy, continue with the policy set before the card scan
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
@ -169,8 +159,16 @@ internal class WalletSelectorMiddleware {
onFailure = { error ->
// Rollback policy if card scanning was failed
tangemSdkManager.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
Timber.e(error, "Unable to scan card")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
when {
error is TangemSdkError.ExceptionError && error.cause is SaltPayActivationError -> {
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo())
}
else -> {
Timber.e(error, "Unable to scan card")
store.dispatchOnMain(WalletSelectorAction.AddWallet.Error(error))
}
}
},
)
}
@ -181,8 +179,6 @@ internal class WalletSelectorMiddleware {
return userWalletsListManager.save(userWallet)
.doOnSuccess {
Analytics.send(MyWallets.CardWasScanned)
store.dispatchOnMain(WalletSelectorAction.AddWallet.Success)
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(userWallet)
@ -211,6 +207,9 @@ internal class WalletSelectorMiddleware {
.doOnSuccess {
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync
if (selectedUserWallet != null) {
val batchId = selectedUserWallet.scanResponse.card.batchId
Analytics.addParamsInterceptor(BatchIdParamsInterceptor(batchId))
store.dispatchOnMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.onUserWalletSelected(selectedUserWallet)
}
@ -219,6 +218,7 @@ internal class WalletSelectorMiddleware {
}
private suspend fun unlockUserWalletWithScannedCard(userWallet: UserWallet): CompletionResult<Unit> {
Analytics.send(MyWallets.Button.WalletUnlockTapped())
tangemSdkManager.changeDisplayedCardIdNumbersCount(userWallet.scanResponse)
return tangemSdkManager.scanProduct(userTokensRepository)
.map { scanResponse ->
@ -258,7 +258,7 @@ internal class WalletSelectorMiddleware {
}
private fun deleteWallets(userWalletsIds: List<UserWalletId>, state: WalletSelectorState) {
Analytics.send(MyWallets.Button.DeleteWalletTapped)
Analytics.send(MyWallets.Button.DeleteWalletTapped())
scope.launch {
when (userWalletsIds.size) {
@ -282,8 +282,6 @@ internal class WalletSelectorMiddleware {
}
private fun renameWallet(userWalletId: UserWalletId, newName: String) {
Analytics.send(MyWallets.Button.EditWalletTapped)
scope.launch {
userWalletsListManager.update(userWalletId) { it.copy(name = newName) }
.doOnFailure { error ->
@ -357,18 +355,28 @@ internal class WalletSelectorMiddleware {
}
}
private suspend fun List<UserWalletModel>.updateWalletStoresAndCalculateFiatBalance(
walletStores: Map<UserWalletId, List<WalletStoreModel>>,
): List<UserWalletModel> {
return this
.associateWith { walletStores[it.id] }
.map { (wallet, walletStores) ->
wallet.updateWalletStoresAndCalculateFiatBalance(walletStores)
}
}
private suspend fun UserWalletModel.updateWalletStoresAndCalculateFiatBalance(
walletStores: List<WalletStoreModel>,
walletStores: List<WalletStoreModel>?,
): UserWalletModel {
return this.copy(
type = when (type) {
is UserWalletModel.Type.MultiCurrency -> type.copy(
tokensCount = walletStores.flatMap { it.walletsData }.size,
tokensCount = walletStores?.flatMap { it.walletsData }?.size ?: 0,
)
is UserWalletModel.Type.SingleCurrency -> type
},
fiatBalance = totalFiatBalanceCalculator.calculate(
walletStores = walletStores,
walletStores = walletStores.orEmpty(),
initial = TotalFiatBalance.Loading,
),
)

View file

@ -25,8 +25,8 @@ internal object WalletSelectorReducer {
is WalletSelectorAction.IsLockedChanged -> state.copy(
isLocked = action.isLocked,
)
is WalletSelectorAction.BalanceLoaded -> state.copy(
wallets = state.wallets.updateWithBalance(action.userWalletModel),
is WalletSelectorAction.BalancesLoaded -> state.copy(
wallets = action.userWalletModels,
)
is WalletSelectorAction.CloseError -> state.copy(error = null)
is WalletSelectorAction.UnlockWithBiometry -> state.copy(
@ -85,19 +85,6 @@ internal object WalletSelectorReducer {
}
}
private fun List<UserWalletModel>.updateWithBalance(
userWalletModel: UserWalletModel,
): List<UserWalletModel> {
return ArrayList(this).apply {
val index = indexOfFirst { it.id == userWalletModel.id }
if (index == -1) {
add(userWalletModel)
} else {
this[index] = userWalletModel
}
}
}
private fun UserWallet.getType(prevType: UserWalletModel.Type? = null): UserWalletModel.Type {
return if (isMultiCurrency) {
UserWalletModel.Type.MultiCurrency(

View file

@ -9,6 +9,7 @@ import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
import com.tangem.tap.features.walletSelector.ui.model.MultiCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.SingleCurrencyUserWalletItem
import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
import java.math.BigDecimal
internal fun WalletSelectorScreenState.updateWithNewState(
newState: WalletSelectorState,
@ -50,10 +51,14 @@ private fun List<UserWalletModel>.toUiModels(
): Sequence<UserWalletItem> {
return this.asSequence().map { userWalletModel ->
with(userWalletModel) {
val balance = UserWalletItem.Balance(
amount = fiatBalance.amount.toFormattedFiatValue(appCurrency.symbol),
isLoading = fiatBalance is TotalFiatBalance.Loading,
)
val formatAmount = { amount: BigDecimal ->
amount.toFormattedFiatValue(appCurrency.symbol)
}
val balance = when (fiatBalance) {
is TotalFiatBalance.Error -> UserWalletItem.Balance.Error(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Loaded -> UserWalletItem.Balance.Loaded(formatAmount(fiatBalance.amount))
is TotalFiatBalance.Loading -> UserWalletItem.Balance.Loading
}
when (type) {
is UserWalletModel.Type.MultiCurrency -> MultiCurrencyUserWalletItem(
id = id,

View file

@ -33,7 +33,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
private val viewModel by viewModels<WalletSelectorViewModel>()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
Analytics.send(MyWallets.MyWalletsScreenOpened)
Analytics.send(MyWallets.MyWalletsScreenOpened())
return super.onCreateDialog(savedInstanceState)
}

View file

@ -2,7 +2,9 @@ package com.tangem.tap.features.walletSelector.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.features.walletSelector.redux.WalletSelectorAction
import com.tangem.tap.features.walletSelector.redux.WalletSelectorState
@ -71,6 +73,7 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
.find { it.id == editedUserWalletId }
if (editedUserWallet != null) {
Analytics.send(MyWallets.Button.EditWalletTapped())
val dialog = RenameWalletDialog(
currentName = editedUserWallet.name,
onApply = { newName ->

View file

@ -9,9 +9,8 @@ import com.tangem.tap.features.walletSelector.ui.model.UserWalletItem
internal object MockData {
private val multiCurrencyUserWallet = MultiCurrencyUserWalletItem(
id = UserWalletId("wallet_1"),
balance = UserWalletItem.Balance(
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
isLoading = false,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",
@ -22,9 +21,8 @@ internal object MockData {
private val singleCurrencyUserWallet = SingleCurrencyUserWalletItem(
id = UserWalletId("wallet_4"),
balance = UserWalletItem.Balance(
balance = UserWalletItem.Balance.Loaded(
amount = "6781.05 $",
isLoading = false,
),
name = "Wallet",
imageUrl = "https://app.tangem.com/cards/card_default.png",

View file

@ -171,13 +171,24 @@ private fun RowScope.TokensInfo(
if (isLocked) {
LockedPlaceholder()
} else {
if (balance.isLoading) {
LoadingTokensInfo(isMultiCurrencyWallet = tokensCount != null)
} else {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
)
when (balance) {
is UserWalletItem.Balance.Error -> {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
showWarning = true,
)
}
is UserWalletItem.Balance.Loaded -> {
LoadedTokensInfo(
balanceAmount = balance.amount,
tokensCount = tokensCount,
showWarning = false,
)
}
is UserWalletItem.Balance.Loading -> {
LoadingTokensInfo(isMultiCurrencyWallet = tokensCount != null)
}
}
}
}
@ -216,14 +227,35 @@ private fun LoadingTokensInfo(isMultiCurrencyWallet: Boolean) {
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun LoadedTokensInfo(balanceAmount: String, tokensCount: Int?) {
Column(horizontalAlignment = Alignment.End) {
Text(
text = balanceAmount,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
private fun LoadedTokensInfo(
modifier: Modifier = Modifier,
balanceAmount: String,
tokensCount: Int?,
showWarning: Boolean,
) {
Column(
modifier = modifier,
horizontalAlignment = Alignment.End,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text(
text = balanceAmount,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.End,
)
if (showWarning) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_alert_24),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
)
}
}
if (tokensCount != null) {
SpacerH2()
Text(

View file

@ -9,10 +9,13 @@ internal sealed interface UserWalletItem {
val balance: Balance
val isLocked: Boolean
data class Balance(
val amount: String,
val isLoading: Boolean,
)
sealed interface Balance {
object Loading : Balance
data class Error(val amount: String) : Balance
data class Loaded(val amount: String) : Balance
}
}
internal data class MultiCurrencyUserWalletItem(

View file

@ -1,9 +1,11 @@
package com.tangem.tap.features.welcome.redux
import android.content.Intent
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.redux.AppState
@ -11,6 +13,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.model.builders.UserWalletBuilder
import com.tangem.tap.domain.scanCard.ScanCardProcessor
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.intentHandler
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
@ -98,11 +101,19 @@ internal class WelcomeMiddleware {
useBiometricsForAccessCode = preferencesStorage.shouldSaveAccessCodes,
)
ScanCardProcessor.scan(
analyticsEvent = SignIn.CardWasScanned(),
onSuccess = { scanResponse ->
scope.launch { onCardScanned(scanResponse) }
},
onFailure = {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(it))
when {
it is TangemSdkError.ExceptionError && it.cause is SaltPayActivationError -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)
}
else -> {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Error(it))
}
}
},
onWalletNotCreated = {
store.dispatchOnMain(WelcomeAction.ProceedWithCard.Success)

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.welcome.ui
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
@ -18,9 +19,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.fragment.app.viewModels
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.fragments.ComposeFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.welcome.ui.components.WelcomeScreenContent
import com.tangem.wallet.R
@ -28,6 +31,11 @@ import com.tangem.wallet.R
internal class WelcomeFragment : ComposeFragment<WelcomeScreenState>() {
private val viewModel by viewModels<WelcomeViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Analytics.send(SignIn.ScreenOpened())
}
@Composable
override fun provideState(): State<WelcomeScreenState> {
return viewModel.state.collectAsState()

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.welcome.ui
import androidx.lifecycle.ViewModel
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.SignIn
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.ui.cardsettings.TextReference
import com.tangem.tap.features.welcome.redux.WelcomeAction
@ -21,10 +23,12 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
}
fun unlockWallets() {
Analytics.send(SignIn.ButtonBiometricSignIn())
store.dispatch(WelcomeAction.ProceedWithBiometrics)
}
fun scanCard() {
Analytics.send(SignIn.ButtonCardSignIn())
store.dispatch(WelcomeAction.ProceedWithCard)
}

View file

@ -151,13 +151,12 @@
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="12dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance"
app:layout_goneMarginTop="12dp" />
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<include
android:id="@+id/l_card_balance"
@ -173,6 +172,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
@ -194,7 +194,7 @@
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
app:layout_constraintTop_toBottomOf="@id/l_card_total_balance" />
<ProgressBar
android:id="@+id/pb_loading_user_tokens"
@ -206,7 +206,7 @@
android:nestedScrollingEnabled="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_wallets"

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Tangem</string>
<string name="tangem_app_name" translatable="false">Tangem</string>
</resources>