Updated on 2026-08-14

This commit is contained in:
Tangem 2022-07-14 16:15:52 +04:00
commit 9ba18ee14f
157 changed files with 3478 additions and 2220 deletions

View file

@ -0,0 +1,46 @@
package com.tangem.tap
import android.app.Activity
import android.app.Application.ActivityLifecycleCallbacks
import android.os.Bundle
import java.util.WeakHashMap
import kotlin.reflect.KClass
class ForegroundActivityObserver {
private val activities = WeakHashMap<KClass<out Activity>, Activity>()
val foregroundActivity: Activity?
get() = activities.entries
.filterNot { it.value.isDestroyed }
.firstOrNull()
?.value
val callbacks get() = Callbacks()
inner class Callbacks : ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
activities[activity::class] = activity
}
override fun onActivityDestroyed(activity: Activity) {
activities.remove(activity::class)
}
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityStarted(activity: Activity) {
}
override fun onActivityPaused(activity: Activity) {
}
override fun onActivityStopped(activity: Activity) {
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}
}
}
fun ForegroundActivityObserver.withForegroundActivity(
block: (Activity) -> Unit
) {
foregroundActivity?.let { block(it) }
}

View file

@ -27,11 +27,11 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ActivityMainBinding
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import java.lang.ref.WeakReference
import kotlin.coroutines.CoroutineContext
lateinit var tangemSdk: TangemSdk
lateinit var tangemSdkManager: TangemSdkManager
@ -57,7 +57,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.state.globalState.feedbackManager?.updateActivity(this)
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)

View file

@ -1,6 +1,8 @@
package com.tangem.tap
import android.app.Application
import coil.ImageLoader
import coil.ImageLoaderFactory
import com.appsflyer.AppsFlyerLib
import com.google.firebase.ktx.Firebase
import com.google.firebase.remoteconfig.ktx.remoteConfig
@ -10,7 +12,10 @@ import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.domain.DomainLayer
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.feedback.AdditionalFeedbackInfo
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.images.createCoilImageLoader
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
@ -22,9 +27,6 @@ import com.tangem.tap.domain.configurable.warningMessage.RemoteWarningLoader
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tokens.CurrenciesRepository
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
import com.tangem.tap.features.feedback.AdditionalEmailInfo
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.feedback.TangemLogCollector
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
@ -38,12 +40,14 @@ val store = Store(
)
val logConfig = LogConfig()
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
class TapApplication : Application() {
class TapApplication : Application(), ImageLoaderFactory {
override fun onCreate() {
super.onCreate()
@ -61,12 +65,13 @@ class TapApplication : Application() {
NetworkConnectivity.createInstance(store, this)
preferencesStorage = PreferencesStorage(this)
PicassoHelper.initPicassoWithCaching(this)
currenciesRepository = CurrenciesRepository(
this, store.state.domainNetworks.tangemTechService
)
walletConnectRepository = WalletConnectRepository(this)
foregroundActivityObserver = ForegroundActivityObserver()
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
initFeedbackManager()
loadConfigs()
@ -75,21 +80,32 @@ class TapApplication : Application() {
initAppsFlyer()
}
override fun newImageLoader(): ImageLoader {
return createCoilImageLoader(context = this)
}
private fun loadConfigs() {
val moshi = MoshiConverter.defaultMoshi()
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load {
configManager.load { config ->
store.dispatch(GlobalAction.SetConfigManager(configManager))
shopService = TangemShopService(this, configManager.config.shopify!!)
shopService = TangemShopService(
application = this,
shopifyShop = config.shopify!!
)
store.state.globalState.feedbackManager?.initChat(
context = this,
zendeskConfig = config.zendesk!!
)
}
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}
private fun initFeedbackManager() {
val infoHolder = AdditionalEmailInfo()
val infoHolder = AdditionalFeedbackInfo()
infoHolder.setAppVersion(this)
val logWriter = TangemLogCollector()
@ -108,11 +124,6 @@ class TapApplication : Application() {
}
data class LogConfig(
// disables both [internal, http]
val picasso: Boolean = BuildConfig.DEBUG,
val picassoInternal: Boolean = true,
val picassoHttp: Boolean = true,
val coil: Boolean = BuildConfig.DEBUG,
val storeAction: Boolean = BuildConfig.DEBUG,
)

View file

@ -135,8 +135,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
messageRes = state.dialog.messageRes,
context = context,
primaryButtonRes = state.dialog.primaryButtonRes,
primaryButtonAction = state.dialog.action
primaryButtonAction = state.dialog.onOk
)
is WalletDialog.RussianCardholdersWarningDialog ->
RussianCardholdersWarningBottomSheetDialog(context)
else -> null
}
dialog?.show()

View file

@ -1,6 +1,5 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.common.compose
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -12,7 +11,6 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun TextAutoSize(
modifier: Modifier = Modifier,

View file

@ -38,6 +38,11 @@ fun SpacerH24(modifier: Modifier = Modifier) {
SpacerH(24.dp, modifier)
}
@Composable
fun SpacerH32(modifier: Modifier = Modifier) {
SpacerH(32.dp, modifier)
}
// ***************************** Vertical
@Composable
fun SpacerW(width: Dp, modifier: Modifier = Modifier) {
@ -64,6 +69,11 @@ fun SpacerW24(modifier: Modifier = Modifier) {
SpacerW(24.dp, modifier)
}
@Composable
fun SpacerW32(modifier: Modifier = Modifier) {
SpacerW(32.dp, modifier)
}
// ***************************** Size
@Composable
fun SpacerS(size: Dp, modifier: Modifier = Modifier) {
@ -88,4 +98,9 @@ fun SpacerS16(modifier: Modifier = Modifier) {
@Composable
fun SpacerS24(modifier: Modifier = Modifier) {
SpacerS(24.dp, modifier)
}
@Composable
fun SpacerS32(modifier: Modifier = Modifier) {
SpacerS(32.dp, modifier)
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.animation.core.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
/**
[REDACTED_AUTHOR]
*/
typealias AnimatedValue = Pair<Float, Float>
@Composable
fun AnimatedValue.toAnimatable(
isPaused: Boolean,
duration: Int,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
return animatable(
values = this,
isPaused = isPaused,
duration = duration,
easing = easing,
)
}
@Composable
fun animatable(
values: AnimatedValue,
duration: Int,
isPaused: Boolean = false,
easing: Easing = LinearEasing,
): Animatable<Float, AnimationVector1D> {
val animatable = remember { Animatable(values.first) }
LaunchedEffect(isPaused) {
if (isPaused) {
animatable.stop()
} else {
animatable.animateTo(
targetValue = values.second,
animationSpec = tween(
durationMillis = duration,
easing = easing
)
)
}
}
return animatable
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Dp.toPx(): Float {
val currentDp = this
return with(LocalDensity.current) { currentDp.toPx() }
}
fun DpSize.halfWidth(): Dp = this.width / 2
fun DpSize.halfHeight(): Dp = this.height / 2

View file

@ -0,0 +1,19 @@
package com.tangem.tap.common.compose.extensions
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalContext
import androidx.core.graphics.drawable.toBitmap
/**
[REDACTED_AUTHOR]
*/
@Composable
fun asImageBitmap(@DrawableRes drawableId: Int): ImageBitmap {
val drawable = AppCompatResources.getDrawable(LocalContext.current, drawableId)
?: throw NullPointerException()
return drawable.toBitmap().asImageBitmap()
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.MutableState
/**
[REDACTED_AUTHOR]
*/
fun <T> MutableState<List<T>>.addAndNotify(value: T) {
this.value = this.value.toMutableList().apply { add(value) }
}
fun <T> MutableState<List<T>>.removeAndNotify(value: T) {
this.value = this.value.toMutableList().apply { remove(value) }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.compose.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tangem_sdk_new.extensions.pxToDp
/**
[REDACTED_AUTHOR]
*/
@Composable
fun Painter.dpSize(): DpSize = DpSize(
intrinsicSize.width.pxToDp().dp,
intrinsicSize.height.pxToDp().dp,
)
@Composable
private fun Float.dpToPx(): Float = LocalContext.current.dpToPx(this)
@Composable
private fun Float.pxToDp(): Float = LocalContext.current.pxToDp(this)

View file

@ -62,6 +62,7 @@ fun Blockchain.getNetworkName(): String {
Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"
Blockchain.BSC, Blockchain.BSCTestnet -> "BEP20"
Blockchain.Binance, Blockchain.BinanceTestnet -> "BEP2"
Blockchain.Tron, Blockchain.TronTestnet -> "TRC20"
else -> ""
}
}

View file

@ -1,135 +0,0 @@
package com.tangem.tap.common.extensions
import android.graphics.*
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import com.squareup.picasso.Transformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(
imageView: ImageFilterView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain,
) {
val url: String? = if (token != null) {
token.id?.let { getIconUrl(it) } ?: token.getCustomIconUrl()
?: IconsUtil.getTokenIconUri(blockchain, token)?.toString()
} else {
getIconUrl(blockchain.toNetworkId())
}
imageView.setImageDrawable(null)
imageView.colorFilter = null
textView.text = null
when {
token?.symbol == QCX -> {
this.load(R.drawable.ic_qcx)?.into(imageView)
}
token?.symbol == VOYR -> {
this.load(R.drawable.ic_voyr)?.into(imageView)
}
url != null -> {
if (token != null) {
setTokenImage(imageView, textView, token, blockchain)
}
this.load(url)
.transform(RoundedCornersTransform())
.noPlaceholder()
?.into(imageView,
object : Callback {
override fun onError(e: Exception?) {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
override fun onSuccess() {
if (token != null) {
imageView.colorFilter = null
textView.text = null
}
if (blockchain.isTestnet()) imageView.saturation = 0f
}
})
}
else -> {
setOfflineCurrencyImage(imageView, textView, token, blockchain)
}
}
}
private const val QCX = "QCX"
private const val VOYR = "VOYRME"
private fun setOfflineCurrencyImage(
imageView: ImageFilterView,
textView: TextView,
token: Token?,
blockchain: Blockchain,
) {
when (token) {
null -> setBlockchainImage(imageView, textView, blockchain)
else -> setTokenImage(imageView, textView, token, blockchain)
}
}
private fun setBlockchainImage(
imageView: ImageFilterView,
textView: TextView,
blockchain: Blockchain,
) {
imageView.setImageResource(blockchain.getRoundIconRes())
imageView.colorFilter = null
if (blockchain.isTestnet()) imageView.saturation = 0f
textView.text = null
}
private fun setTokenImage(
imageView: ImageFilterView,
textView: TextView,
token: Token,
tokenBlockchain: Blockchain
) {
imageView.setImageResource(R.drawable.shape_circle)
if (tokenBlockchain.isTestnet()) {
imageView.saturation = 0f
} else {
imageView.setColorFilter(token.getColor())
}
textView.text = token.symbol.take(1)
}
private class RoundedCornersTransform : Transformation {
override fun transform(source: Bitmap): Bitmap {
val size = source.width.coerceAtMost(source.height)
val x = (source.width - size) / 2
val y = (source.height - size) / 2
val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
if (squaredBitmap != source) source.recycle()
val paint = Paint().apply {
shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
isAntiAlias = true
}
val rectF = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat())
val radius = size / 8f
val bitmap = Bitmap.createBitmap(size, size, source.config)
Canvas(bitmap).drawRoundRect(rectF, radius, radius, paint)
squaredBitmap.recycle()
return bitmap
}
override fun key(): String = "rounded_corners"
}

View file

@ -53,8 +53,7 @@ fun BigDecimal.toFiatString(
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.HALF_UP)
val fiatValue = rateValue.multiply(this)
return fiatValue.toFormattedFiatValue(fiatCurrencyName, formatWithSpaces)
}
@ -67,7 +66,8 @@ fun BigDecimal.toFormattedFiatValue(
fiatCurrencyName: String,
formatWithSpaces: Boolean = false
): String {
val fiatValue = if (formatWithSpaces) this.formatWithSpaces() else this
val fiatValue = this.setScale(2, RoundingMode.HALF_UP)
.let { if (formatWithSpaces) it.formatWithSpaces() else it }
return "${fiatValue} $fiatCurrencyName"
}

View file

@ -1,6 +1,8 @@
package com.tangem.tap.common.extensions
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.core.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
@ -9,8 +11,13 @@ import com.tangem.wallet.R
fun Token.getColor(): Int {
return try {
("#" + this.contractAddress.subSequence(2..7).toString())
.toColorInt()
.toColorInt()
} catch (exception: Exception) {
R.color.lightGray4
}
}
@ColorInt
fun Token.getTextColor(): Int {
return if (this.getColor().luminance > 0.5) Color.BLACK else Color.WHITE
}

View file

@ -93,32 +93,39 @@ fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> U
}
fun Context.dpToPixels(dp: Int): Int =
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
fun Context.pixelsToDp(pixels: Int): Int {
return (pixels.toFloat() /
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
(resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
.toInt()
}
fun Context.dpToPixels(dp: Float): Float =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.resources.displayMetrics)
fun Context.pixelsToDp(pixels: Float): Float =
(pixels / (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT))
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
marginLeftDp: Int = 16,
marginTopDp: Int = 8,
marginRightDp: Int = 16,
marginBottomDp: Int = 8
) {
val params = this.layoutParams
(params as ViewGroup.MarginLayoutParams).setMargins(
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
context.dpToPixels(marginLeftDp),
context.dpToPixels(marginTopDp),
context.dpToPixels(marginRightDp),
context.dpToPixels(marginBottomDp)
)
this.layoutParams = params
}
@ -128,11 +135,11 @@ fun Activity.setSystemBarTextColor(setTextDark: Boolean) {
val flags = this.window.decorView.systemUiVisibility
// Update the SystemUiVisibility dependening on whether we want a Light or Dark theme.
this.window.decorView.systemUiVisibility =
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
if (setTextDark) {
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
} else {
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
@ -150,7 +157,7 @@ fun Context.copyToClipboard(value: Any, label: String = "") {
fun Context.getFromClipboard(default: CharSequence? = null): CharSequence? {
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager
?: return default
?: return default
val clipData = clipboard.primaryClip ?: return default
if (clipData.itemCount == 0) return default
@ -172,10 +179,10 @@ fun Fragment.shareText(text: String) {
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)

View file

@ -49,11 +49,11 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
fun WalletManager?.getToUpUrl(): String? {
val globalState = store.state.globalState
val currencyExchangeManager = globalState.currencyExchangeManager ?: return null
val exchangeManager = globalState.exchangeManager ?: return null
val wallet = this?.wallet ?: return null
val defaultAddress = wallet.address
return currencyExchangeManager.getUrl(
return exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,

View file

@ -0,0 +1,127 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
class AdditionalFeedbackInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
var derivationPath: String = "",
)
var appVersion: String = ""
// card
var cardId: String = ""
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
// device
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
// send error
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
var token: String = ""
fun setAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
fun setCardInfo(data: ScanResponse) {
cardId = data.card.cardId
cardBlockchain = data.walletData?.blockchain ?: ""
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = data.card.wallets
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(
EmailWalletInfo(
blockchain = manager.wallet.blockchain,
address = getAddress(manager.wallet),
explorerLink = getExploreUri(manager.wallet),
host = manager.currentHost,
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
)
)
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
}
}
fun updateOnSendError(
wallet: Wallet,
host: String,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
) {
onSendErrorWalletInfo = EmailWalletInfo(
blockchain = wallet.blockchain,
address = getAddress(wallet),
explorerLink = getExploreUri(wallet),
host = host,
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
}
}
}

View file

@ -0,0 +1,108 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.domain.common.TapWorkarounds
import com.tangem.wallet.R
interface FeedbackData {
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalFeedbackInfo) {}
fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String
fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(3)
append(context.getString(getDataCollectionMessageResId()))
appendLine()
append(createOptionalMessage(infoHolder))
}.toString()
}
}
class RateCanBeBetterEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_rate_negative
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendLine()
.appendPhoneInfo()
.build()
}
class ScanFailsEmail : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_scan_failed
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
override fun joinTogether(context: Context, infoHolder: AdditionalFeedbackInfo): String = StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(4)
append(createOptionalMessage(infoHolder))
}.toString()
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendPhoneInfo()
.build()
}
class SendTransactionFailedEmail(
val error: String
) : FeedbackData {
override val subjectResId: Int = R.string.feedback_subject_tx_failed
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendTxFailedBlockchainInfo(error)
.appendLine()
.appendPhoneInfo()
.build()
}
class FeedbackEmail : FeedbackData {
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
private var isS2CCard = false
override fun prepare(infoHolder: AdditionalFeedbackInfo) {
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
}
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String = FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendWalletsInfo()
.appendLine()
.appendPhoneInfo()
.build()
}
class SupportInfo : FeedbackData {
override val subjectResId: Int = R.string.details_ask_a_question
override val mainMessageResId: Int = R.string.details_ask_a_question
override fun createOptionalMessage(infoHolder: AdditionalFeedbackInfo): String {
return FeedbackDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendPhoneInfo()
.build()
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.tap.common.feedback
class FeedbackDataBuilder(
private val infoHolder: AdditionalFeedbackInfo
) {
val builder = StringBuilder()
fun appendDelimiter(): FeedbackDataBuilder {
builder.appendDelimiter()
return this
}
fun appendLine(count: Int = 1): FeedbackDataBuilder {
builder.appendLine(count)
return this
}
fun appendCardInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
return this
}
fun appendWalletsInfo(): FeedbackDataBuilder {
infoHolder.walletsInfo.forEach {
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Explorer link", it.explorerLink)
infoHolder.tokens[it.blockchain]?.let { tokens ->
builder.append("Tokens:")
appendLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
}
return this
}
fun appendTxFailedBlockchainInfo(error: String): FeedbackDataBuilder {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalFeedbackInfo.EmailWalletInfo()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)
builder.appendDelimiter()
builder.appendKeyValue("Source address", walletInfo.address)
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
builder.appendKeyValue("Amount", infoHolder.amount)
builder.appendKeyValue("Fee", infoHolder.fee)
return this
}
fun appendPhoneInfo(): FeedbackDataBuilder {
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return this
}
fun build(): String = builder.toString()
}
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
return if (value.isNotBlank()) this.append("$key: $value\n") else this
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
return append(List(count) { "\n" }.joinToString(separator = ""))
}

View file

@ -0,0 +1,113 @@
package com.tangem.tap.common.feedback
import android.content.Context
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.withForegroundActivity
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
import timber.log.Timber
import zendesk.configurations.Configuration
import zendesk.core.Zendesk
import zendesk.support.Support
import zendesk.support.request.RequestConfiguration
import zendesk.support.requestlist.RequestListActivity
import zendesk.support.requestlist.RequestListConfiguration
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
) {
fun initChat(
context: Context,
zendeskConfig: ZendeskConfig,
) {
Zendesk.INSTANCE.init(
/* context = */ context,
/* zendeskUrl = */ zendeskConfig.url,
/* applicationId = */ zendeskConfig.appId,
/* oauthClientId = */ zendeskConfig.clientId,
)
Support.INSTANCE.init(Zendesk.INSTANCE)
}
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
message = feedbackData.joinTogether(activity, infoHolder),
file = fileLog,
onFail = onFail
)
}
}
fun openChat(feedbackData: FeedbackData) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
RequestListActivity.builder()
.show(
/* context = */ activity,
/* configurations = */ buildConfigs(activity, feedbackData)
)
}
}
private fun createLogFile(context: Context): File? {
return try {
val file = File(context.filesDir, "logs.txt")
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logCollector.getLogs().forEach { stringWriter.append(it) }
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
file
} catch (ex: Exception) {
Timber.e(ex, "Can't create the logs file")
null
}
}
private fun buildConfigs(
context: Context,
feedbackData: FeedbackData,
): List<Configuration> {
return listOf(
// Request configuration
RequestConfiguration.Builder()
.withRequestSubject(context.getString(feedbackData.subjectResId))
.config(),
// Request list configuration
RequestListConfiguration.Builder()
.withContactUsButtonVisible(true)
.config(),
)
}
private fun getSupportEmail(): String {
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
S2C_SUPPORT_EMAIL
} else {
DEFAULT_SUPPORT_EMAIL
}
}
companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
}
}

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.images
import android.content.Context
import android.util.Log
import coil.ImageLoader
import coil.util.Logger
import com.tangem.tap.logConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
private const val COIL_LOG_TAG = "COIL"
fun createCoilImageLoader(
context: Context
): ImageLoader {
return ImageLoader.Builder(context)
.apply {
if (!logConfig.coil) return@apply
logger(CoilTimberLogger())
okHttpClient {
OkHttpClient.Builder()
.addNetworkInterceptor(
HttpLoggingInterceptor { message ->
Timber.tag(COIL_LOG_TAG).d(message)
}
.apply {
level = HttpLoggingInterceptor.Level.BODY
}
)
.build()
}
}
.build()
}
private class CoilTimberLogger : Logger {
override var level: Int = Log.DEBUG
override fun log(tag: String, priority: Int, message: String?, throwable: Throwable?) {
with(Timber.tag(COIL_LOG_TAG)) {
throwable?.let { e -> e(e, message) }
message?.let { msg -> d(msg) }
}
}
}

View file

@ -1,68 +0,0 @@
package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import com.tangem.tap.logConfig
import com.tangem.wallet.BuildConfig
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
class PicassoHelper {
companion object {
private const val KEEP_CACHE_MAX_DAYS = 7
fun initPicassoWithCaching(application: Application) {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = logIsEnabled()
picasso.setIndicatorsEnabled(BuildConfig.DEBUG)
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
return OkHttpClient.Builder().apply {
cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
callTimeout(15000, TimeUnit.MILLISECONDS)
if (logIsEnabled()) {
addDebugInterceptors(this)
}
addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
.build()
val origRequest = chain.request()
val neverExpireRequest = origRequest.newBuilder()
.cacheControl(cacheControl)
.build()
chain.proceed(neverExpireRequest)
}
}.build()
}
private fun addDebugInterceptors(okHttpBuilder: OkHttpClient.Builder) {
val picassoInterceptor = HttpLoggingInterceptor(PicassoOkHttpLogger()).apply {
level = HttpLoggingInterceptor.Level.BODY
}
okHttpBuilder.addInterceptor(picassoInterceptor)
}
private fun logIsEnabled(): Boolean = BuildConfig.DEBUG && logConfig.picasso
}
}
private class PicassoOkHttpLogger : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.common.log
import com.tangem.Log
import com.tangem.TangemSdkLogger
import java.text.SimpleDateFormat
import java.util.Date
class TangemLogCollector : TangemSdkLogger {
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
private val logs = mutableListOf<String>()
private val mutex = Object()
override fun log(message: () -> String, level: Log.Level) {
val time = dateFormatter.format(Date())
synchronized(mutex) {
logs.add("$time: ${message()}\n")
}
}
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
fun clearLogs() {
synchronized(mutex) { logs.clear() }
}
}

View file

@ -7,15 +7,18 @@ import com.tangem.common.core.TangemError
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.GlobalAnalyticsHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.*
import com.tangem.tap.common.feedback.FeedbackData
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.DebugErrorAction
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -74,10 +77,21 @@ sealed class GlobalAction : Action {
data class SetFeedbackManager(val feedbackManager: FeedbackManager) : GlobalAction()
data class SetAnanlyticHandlers(val analyticsHandlers: GlobalAnalyticsHandler) : GlobalAction()
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class SendEmail(val feedbackData: FeedbackData) : GlobalAction()
data class OpenChat(val feedbackData: FeedbackData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object InitCurrencyExchangeManager : GlobalAction() {
data class Success(val exchangeManager: CurrencyExchangeManager) : GlobalAction()
object ExchangeManager : GlobalAction() {
object Init : GlobalAction() {
data class Success(
val exchangeManager: com.tangem.tap.network.exchangeServices.CurrencyExchangeManager,
) : GlobalAction()
}
object Update : GlobalAction()
}
object FetchUserCountry : GlobalAction() {
data class Success(val countryCode: String) : GlobalAction()
}
}

View file

@ -2,20 +2,31 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.ifNotNull
import com.tangem.common.services.Result
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.onramper.OnramperService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import java.util.Locale
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class GlobalMiddleware {
@ -24,94 +35,134 @@ class GlobalMiddleware {
}
}
private val globalMiddlewareHandler: Middleware<AppState> = { _, appState ->
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
handleAction(action, appState, dispatch)
nextDispatch(action)
}
}
}
private fun handleAction(action: Action, appState: () -> AppState?, dispatch: DispatchFunction) {
when (action) {
is GlobalAction.ScanFailsCounter.ChooseBehavior -> {
when (action.result) {
is CompletionResult.Success -> store.dispatch(GlobalAction.ScanFailsCounter.Reset)
is CompletionResult.Failure -> {
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
))
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
if (it.hideWarning(action.warning)) {
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
//TODO: No appropriate warningMessage identification. Make it better later
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
}
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
}
}
}
is GlobalAction.SendEmail -> {
store.state.globalState.feedbackManager?.sendEmail(action.feedbackData)
}
is GlobalAction.OpenChat -> {
store.state.globalState.feedbackManager?.openChat(action.feedbackData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.ExchangeManager.Init -> {
val config = appState()?.globalState?.configManager?.config
ifNotNull(
config?.mercuryoWidgetId,
config?.mercuryoSecret,
config?.moonPayApiKey,
config?.moonPayApiSecretKey,
) { mercuryoWidgetId, mercuryoSecret, moonPayKey, moonPaySecretKey ->
scope.launch {
val buyService = MercuryoService(
apiVersion = MercuryoApi.API_VERSION,
mercuryoWidgetId = mercuryoWidgetId,
secret = mercuryoSecret,
)
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
val exchangeManager = CurrencyExchangeManager(buyService, sellService)
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}
}
}
is GlobalAction.ExchangeManager.Init.Success -> {}
is GlobalAction.ExchangeManager.Update -> {
val exchangeManager = appState()?.globalState?.exchangeManager.guard {
store.dispatchDebugErrorNotification("exchangeManager is not initialized")
return
}
scope.launch { exchangeManager.update() }
}
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandlers,
currenciesRepository,
action.additionalBlockchainsToDerive,
action.messageResId
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> {
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
}
}
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
))
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
if (it.hideWarning(action.warning)) {
if (WarningMessagesManager.isAlreadySignedHashesWarning(action.warning)) {
//TODO: No appropriate warningMessage identification. Make it better later
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
}
store.dispatch(WalletAction.Warnings.Update)
store.dispatch(SendAction.Warnings.Update)
}
}
}
is GlobalAction.SendFeedback -> {
store.state.globalState.feedbackManager?.send(action.emailData)
}
is GlobalAction.UpdateWalletSignedHashes -> {
store.dispatch(WalletAction.Warnings.CheckRemainingSignatures(action.remainingSignatures))
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.InitCurrencyExchangeManager -> {
val config = appState()?.globalState?.configManager?.config
ifNotNull(
config?.onramperApiKey,
config?.moonPayApiKey,
config?.moonPayApiSecretKey,
) { onramperKey, moonPayKey, moonPaySecretKey ->
scope.launch {
val onramper = OnramperService(onramperKey)
val moonPay = MoonPayService(moonPayKey, moonPaySecretKey)
val exchangeManager = CurrencyExchangeManager(onramper, moonPay)
exchangeManager.getStatus()
store.dispatchOnMain(GlobalAction.InitCurrencyExchangeManager.Success(exchangeManager))
}
}
}
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(
store.state.globalState.analyticsHandlers,
currenciesRepository,
action.additionalBlockchainsToDerive,
action.messageResId
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> {
action.onFailure?.invoke(result.error)
}
}
action.onFailure?.invoke(result.error)
}
}
}
}
nextDispatch(action)
}
is GlobalAction.FetchUserCountry -> {
scope.launch {
val techService = store.state.domainNetworks.tangemTechService
when (val result = techService.userCountry()) {
is Result.Success -> {
store.dispatch(
GlobalAction.FetchUserCountry.Success(
countryCode = result.data.code.lowercase()
)
)
}
is Result.Failure -> {
store.dispatch(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase()
)
)
}
}
}
}
}
}

View file

@ -70,12 +70,14 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.InitCurrencyExchangeManager.Success -> {
globalState.copy(currencyExchangeManager = action.exchangeManager)
is GlobalAction.ExchangeManager.Init.Success -> {
globalState.copy(exchangeManager = action.exchangeManager)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)
is GlobalAction.FetchUserCountry.Success -> globalState.copy(
userCountryCode = action.countryCode
)
else -> globalState
}
}

View file

@ -3,12 +3,12 @@ package com.tangem.tap.common.redux.global
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
@ -25,9 +25,10 @@ data class GlobalState(
val appCurrency: FiatCurrency = FiatCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val currencyExchangeManager: CurrencyExchangeManager? = null,
val exchangeManager: CurrencyExchangeManager? = null,
val resources: AndroidResources = AndroidResources(),
val analyticsHandlers: AnalyticsHandler? = null,
val userCountryCode: String? = null,
) : StateType

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.zendesk
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class ZendeskConfig(
@Json(name = "zendeskApiKey")
val apiKey: String,
@Json(name = "zendeskAppId")
val appId: String,
@Json(name = "zendeskClientId")
val clientId: String,
@Json(name = "zendeskUrl")
val url: String,
)

View file

@ -39,21 +39,26 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
data class UnsupportedState(
val stateError: String,
val customMessage: String = "Unsupported state:"
) : TapError(R.string.common_custom_string, listOf("$customMessage $stateError"))
sealed class XmlError {
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
sealed class WalletManager {
object CreationError: CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
class InternalError(message: String): CustomError(message)
object BlockchainIsUnreachable: TapError(R.string.wallet_balance_blockchain_unreachable)
object BlockchainIsUnreachableTryLater: TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
object CreationError : CustomError("Can't create wallet manager")
class NoAccountError(amountToCreateAccount: String) : CustomError(amountToCreateAccount)
class InternalError(message: String) : CustomError(message)
object BlockchainIsUnreachable : TapError(R.string.wallet_balance_blockchain_unreachable)
object BlockchainIsUnreachableTryLater : TapError(R.string.wallet_balance_blockchain_unreachable_try_later)
}
data class ValidateTransactionErrors(
override val errorList: List<TapError>,
override val builder: (List<String>) -> String
override val errorList: List<TapError>,
override val builder: (List<String>) -> String
) : TapError(-1), MultiMessageError
}

View file

@ -1,6 +1,12 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.card.Card
import com.tangem.common.services.Result
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
@ -85,6 +91,12 @@ class TapWalletManager {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
store.dispatch(
WalletAction.MultiWallet.ShowWalletBackupWarning(
show = data.card.settings.isBackupAllowed
&& data.card.backupStatus == Card.BackupStatus.NoBackup
)
)
loadData(data)
}
}

View file

@ -1,49 +0,0 @@
package com.tangem.tap.domain
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import com.squareup.picasso.Picasso
import com.squareup.picasso.Target
import com.tangem.common.services.Result
/**
[REDACTED_AUTHOR]
*/
class UrlBitmapLoader {
private val mainHandler = Handler(Looper.getMainLooper())
fun loadBitmap(url: String, callback: (Result<Bitmap>) -> Unit) {
val target = DownloadTarget(callback)
protectedFromGarbageCollectorTargets.add(target)
mainHandler.post { Picasso.get().load(url).into(target) }
}
fun loadBitmap(url: String, target: DownloadTarget) {
protectedFromGarbageCollectorTargets.add(target)
Picasso.get().load(url).into(target)
}
}
private val protectedFromGarbageCollectorTargets = mutableListOf<Target>()
// It adds the ability to trigger multiple downloads with a unique callback.
open class DownloadTarget(
val callback: (Result<Bitmap>) -> Unit,
) : Target {
override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) {
callback(Result.Success(bitmap))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
callback.invoke(Result.Failure(e ?: Exception("Unknown exception")))
protectedFromGarbageCollectorTargets.remove(this)
}
override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
}
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.domain.configurable.Loader
/**
@ -11,15 +11,17 @@ import com.tangem.tap.domain.configurable.Loader
data class Config(
val coinMarketCapKey: String = "f6622117-c043-47a0-8975-9d673ce484de",
val moonPayApiKey: String = "pk_test_kc90oYTANy7UQdBavDKGfL4K9l6VEPE",
val onramperApiKey: String = "pk_test_Ix2aCF3ej_5tcDKkBR7MChIvf5Nb0oPORPQ3Oal5G8I0",
val moonPayApiSecretKey: String = "sk_test_V8w4M19LbDjjYOt170s0tGuvXAgyEb1C",
val mercuryoWidgetId: String = "",
val mercuryoSecret: String = "",
val appsFlyerDevKey: String = "",
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null
val shopify: ShopifyShop? = null,
val zendesk: ZendeskConfig? = null,
)
class ConfigManager(
@ -32,11 +34,11 @@ class ConfigManager(
private var defaultConfig = Config()
fun load(onComplete: VoidCallback? = null) {
localLoader.load { config ->
setupFeature(config.features)
setupKey(config.configValues)
onComplete?.invoke()
fun load(onComplete: ((config: Config) -> Unit)? = null) {
localLoader.load { configModel ->
setupFeature(configModel.features)
setupKey(configModel.configValues)
onComplete?.invoke(config)
}
// Uncomment to enable remote config
// remoteLoader.load { config ->
@ -82,8 +84,9 @@ class ConfigManager(
config = config.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
onramperApiKey = values.onramperApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
mercuryoWidgetId = values.mercuryoWidgetId,
mercuryoSecret = values.mercuryoSecret,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
@ -92,12 +95,14 @@ class ConfigManager(
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
onramperApiKey = values.onramperApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
mercuryoWidgetId = values.mercuryoWidgetId,
mercuryoSecret = values.mercuryoSecret,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
@ -106,6 +111,7 @@ class ConfigManager(
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
zendesk = values.zendesk,
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.domain.configurable.config
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.zendesk.ZendeskConfig
/**
[REDACTED_AUTHOR]
@ -15,15 +16,17 @@ class FeatureModel(
class ConfigValueModel(
val coinMarketCapKey: String,
val mercuryoWidgetId: String,
val mercuryoSecret: String,
val moonPayApiKey: String,
val onramperApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockchairAuthorizationToken: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,
val appsFlyerDevKey: String,
val shopifyShop: ShopifyShop?
val shopifyShop: ShopifyShop?,
val zendesk: ZendeskConfig?,
)
class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) {

View file

@ -6,7 +6,6 @@ import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import java.util.*
/**
[REDACTED_AUTHOR]
@ -188,31 +187,6 @@ class WarningMessagesManager(
WarningMessage.Origin.Local
)
fun restoreFundsWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Warning,
listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.alert_title,
messageResId = R.string.alert_funds_restoration_message,
origin = WarningMessage.Origin.Local,
buttonTextId = R.string.warning_button_learn_more
)
const val REMAINING_SIGNATURES_WARNING = 10
private const val RESTORE_FUNDS_GUIDE_URL_RU =
"https://tangem.com/ru/kak-vosstanovit-tokeny-otpravlennye-ne-na-tot-adres-v-tangem-wallet"
private const val RESTORE_FUNDS_GUIDE_URL_EN =
"https://tangem.com/en/how-to-recover-crypto-sent-to-the-wrong-address-in-tangem-wallet"
fun getRestoreFundsGuideUrl(locale: String): String {
return if (locale == Locale("ru").language) {
RESTORE_FUNDS_GUIDE_URL_RU
} else {
RESTORE_FUNDS_GUIDE_URL_EN
}
}
}
}

View file

@ -1,57 +0,0 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Blockchain.Arbitrum
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
fun CurrencyExchangeManager.buyIsAllowed(currency: Currency): Boolean {
return this.status?.buyIsAllowed(currency) ?: false
}
fun CurrencyExchangeManager.sellIsAllowed(currency: Currency): Boolean {
return this.status?.sellIsAllowed(currency) ?: false
}
fun CurrencyExchangeStatus.buyIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (currency.blockchain == Arbitrum) return false
if (!isBuyAllowed) return false
//TODO: temporary, for the 3.32 release, unlock all buy button
return true
return when (currency) {
is Currency.Blockchain -> {
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null
blockchain == Blockchain.Unknown -> false
else -> availableToBuy.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}
fun CurrencyExchangeStatus.sellIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isSellAllowed) return false
return when (currency) {
is Currency.Blockchain -> {
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> false
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
else -> availableToSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}

View file

@ -112,31 +112,44 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
this.card = card
val curves = card.getCurvesForNonCreatedWallets()
if (curves.isEmpty()) {
val createWalletResponses = card.wallets.map { CreateWalletResponse(card.cardId, it) }
proceedWithCreatedWallets(createWalletResponses, session, callback)
return
}
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val createWalletResponses = result.data.createWalletResponses
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(card = session.environment.card!!)
)
)
}
}
proceedWithCreatedWallets(result.data.createWalletResponses, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun proceedWithCreatedWallets(
createWalletResponses: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(card = session.environment.card!!)
)
)
}
}
}
private fun linkPrimaryCard(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,

View file

@ -185,8 +185,9 @@ class CurrenciesRepository(
tokens.map {
async {
tangemNetworkService.getTokens(
it.contractAddress,
it.blockchainDao.toBlockchain().toNetworkId()
contractAddress = it.contractAddress,
networkId = it.blockchainDao.toBlockchain().toNetworkId(),
active = true,
)
}
}.map { it.await() }

View file

@ -53,6 +53,7 @@ class LoadAvailableCoinsService(
val networkIds = supportedBlockchains.toSet().map { it.toNetworkId() }
return networkService.getListOfCoins(
networkIds = networkIds,
active = true,
offset = offset,
limit = LOAD_PER_PAGE,
searchText = searchInput

View file

@ -5,11 +5,15 @@ import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Types
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.network.common.MoshiConverter
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletForSession
import com.trustwallet.walletconnect.models.WCPeerMeta
import com.trustwallet.walletconnect.models.session.WCSession
import timber.log.Timber
import java.nio.charset.Charset
class WalletConnectRepository(val context: Application) {
private val moshi = MoshiConverter.defaultMoshi()
@ -35,17 +39,29 @@ class WalletConnectRepository(val context: Application) {
fun loadSavedSessions(): List<WalletConnectSession> {
return try {
val json = context.readFileText(FILE_NAME_PREFIX_SESSIONS)
.hexToUtf8()
walletConnectAdapter.fromJson(json)!!.map { it.toSession() }
} catch (exception: Exception) {
Timber.e(exception)
emptyList()
}
}
private fun saveSessions(sessions: List<WalletConnectSession>) {
val json = walletConnectAdapter.toJson(sessions.map { SessionDao.fromSession(it) })
.utf8ToHex() // convert to hex to solve problems with saving text with emojis
Timber.e("WC sessions, saving following json: $json")
context.rewriteFile(json, FILE_NAME_PREFIX_SESSIONS)
}
private fun String.utf8ToHex(): String {
return this.toByteArray().toHexString()
}
private fun String.hexToUtf8(): String {
return this.hexToBytes().toString(Charset.defaultCharset())
}
private fun Context.readFileText(fileName: String): String =
this.openFileInput(fileName).bufferedReader().readText()

View file

@ -8,8 +8,9 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.store
import java.util.EnumSet
import org.rekotlin.Action
import java.util.*
class DetailsReducer {
companion object {
@ -49,6 +50,7 @@ private fun handlePrepareScreen(
cardInfo = action.scanResponse.card.toCardInfo(),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card),
createBackupAllowed = action.scanResponse.card.backupStatus == Card.BackupStatus.NoBackup,
appCurrency = store.state.globalState.appCurrency
)
}

View file

@ -9,6 +9,8 @@ import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -16,7 +18,6 @@ import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -124,7 +125,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
}
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
store.dispatch(GlobalAction.SendEmail(FeedbackEmail()))
}
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
@ -132,6 +133,10 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tvSupport.setOnClickListener {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
}
llManageSecurity.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
}

View file

@ -1,380 +0,0 @@
package com.tangem.tap.features.feedback
import android.app.Activity
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.*
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.wallet.R
import timber.log.Timber
import java.io.File
import java.io.FileWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.*
/**
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val logCollector: TangemLogCollector,
) {
private lateinit var activity: Activity
fun updateActivity(activity: Activity) {
this.activity = activity
}
fun send(emailData: EmailData, onFail: ((Exception) -> Unit)? = null) {
if (!this::activity.isInitialized) return
emailData.prepare(infoHolder)
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(emailData.subjectResId),
message = emailData.joinTogether(activity, infoHolder),
file = fileLog,
onFail = onFail
)
}
private fun getSupportEmail(): String {
return if (TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)) {
S2C_SUPPORT_EMAIL
} else {
DEFAULT_SUPPORT_EMAIL
}
}
private fun createLogFile(): File? {
return try {
val file = File(activity.filesDir, "logs.txt")
file.delete()
file.createNewFile()
val stringWriter = StringWriter()
logCollector.getLogs().forEach { stringWriter.append(it) }
val fileWriter = FileWriter(file)
fileWriter.write(stringWriter.toString())
fileWriter.close()
logCollector.clearLogs()
file
} catch (ex: Exception) {
Timber.e(ex, "Can't create a file for email attachment")
null
}
}
companion object {
const val DEFAULT_SUPPORT_EMAIL = "support@tangem.com"
const val S2C_SUPPORT_EMAIL = "cardsupport@start2coin.com"
}
}
class TangemLogCollector : TangemSdkLogger {
private val dateFormatter = SimpleDateFormat("HH:mm:ss.SSS")
private val logs = mutableListOf<String>()
private val mutex = Object()
override fun log(message: () -> String, level: Log.Level) {
val time = dateFormatter.format(Date())
synchronized(mutex) {
logs.add("$time: ${message()}\n")
}
}
fun getLogs(): List<String> = synchronized(mutex) { logs.toList() }
fun clearLogs() {
synchronized(mutex) { logs.clear() }
}
}
class AdditionalEmailInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
var derivationPath: String = "",
)
var appVersion: String = ""
// card
var cardId: String = ""
var cardFirmwareVersion: String = ""
var cardIssuer: String = ""
var cardBlockchain: String = ""
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
// device
var phoneModel: String = Build.MODEL
var osVersion: String = Build.VERSION.SDK_INT.toString()
// send error
var destinationAddress: String = ""
var amount: String = ""
var fee: String = ""
var token: String = ""
fun setAppVersion(context: Context) {
try {
val pInfo = context.packageManager.getPackageInfo(context.packageName, 0)
appVersion = pInfo.versionName
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
fun setCardInfo(data: ScanResponse) {
cardId = data.card.cardId
cardBlockchain = data.walletData?.blockchain ?: ""
cardFirmwareVersion = data.card.firmwareVersion.stringValue
cardIssuer = data.card.issuer.name
signedHashesCount = data.card.wallets
.joinToString("; ") { "${it.curve.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(
EmailWalletInfo(
blockchain = manager.wallet.blockchain,
address = getAddress(manager.wallet),
explorerLink = getExploreUri(manager.wallet),
host = manager.currentHost,
derivationPath = manager.wallet.publicKey.derivationPath?.rawPath ?: ""
)
)
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
}
}
fun updateOnSendError(wallet: Wallet, host: String, amountToSend: Amount, feeAmount: Amount, destinationAddress: String) {
onSendErrorWalletInfo = EmailWalletInfo(
blockchain = wallet.blockchain,
address = getAddress(wallet),
explorerLink = getExploreUri(wallet),
host = host,
derivationPath = wallet.publicKey.derivationPath?.rawPath ?: ""
)
this.destinationAddress = destinationAddress
amount = amountToSend.value?.stripZeroPlainString() ?: "0"
fee = feeAmount.value?.stripZeroPlainString() ?: "0"
token = if (amountToSend.type is AmountType.Token) amountToSend.currencySymbol else ""
}
private fun getAddress(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.address
} else {
val addresses = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${it.value}"
}
"Multiple address: $addresses"
}
}
private fun getExploreUri(wallet: Wallet): String {
return if (wallet.addresses.size == 1) {
wallet.getExploreUrl(wallet.address)
} else {
val links = wallet.addresses.joinToString(", ") {
"${it.type.javaClass.simpleName} - ${wallet.getExploreUrl(it.value)}"
}
"Multiple explorers links: $links"
}
}
}
interface EmailData {
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalEmailInfo) {}
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(3)
append(context.getString(getDataCollectionMessageResId()))
appendLine()
append(createOptionalMessage(infoHolder))
}.toString()
}
}
class RateCanBeBetterEmail : EmailData {
override val subjectResId: Int = R.string.feedback_subject_rate_negative
override val mainMessageResId: Int = R.string.feedback_preface_rate_negative
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
.appendCardInfo()
.appendLine()
.appendPhoneInfo()
.build()
}
class ScanFailsEmail : EmailData {
override val subjectResId: Int = R.string.feedback_subject_scan_failed
override val mainMessageResId: Int = R.string.feedback_preface_scan_failed
override fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String = StringBuilder().apply {
append(context.getString(mainMessageResId))
appendLine(4)
append(createOptionalMessage(infoHolder))
}.toString()
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
.appendPhoneInfo()
.build()
}
class SendTransactionFailedEmail(
val error: String
) : EmailData {
override val subjectResId: Int = R.string.feedback_subject_tx_failed
override val mainMessageResId: Int = R.string.feedback_preface_tx_failed
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
.appendCardInfo()
.appendDelimiter()
.appendTxFailedBlockchainInfo(error)
.appendLine()
.appendPhoneInfo()
.build()
}
class FeedbackEmail : EmailData {
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
private var isS2CCard = false
override fun prepare(infoHolder: AdditionalEmailInfo) {
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
}
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String = EmailDataBuilder(infoHolder)
.appendCardInfo()
.appendWalletsInfo()
.appendLine()
.appendPhoneInfo()
.build()
}
class EmailDataBuilder(
private val infoHolder: AdditionalEmailInfo
) {
val builder = StringBuilder()
fun appendDelimiter(): EmailDataBuilder {
builder.appendDelimiter()
return this
}
fun appendLine(count: Int = 1): EmailDataBuilder {
builder.appendLine(count)
return this
}
fun appendCardInfo(): EmailDataBuilder {
builder.appendKeyValue("Card ID", infoHolder.cardId)
builder.appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
builder.appendKeyValue("Card Blockchain", infoHolder.cardBlockchain)
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
return this
}
fun appendWalletsInfo(): EmailDataBuilder {
infoHolder.walletsInfo.forEach {
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Derivation path", it.derivationPath)
builder.appendKeyValue("Explorer link", it.explorerLink)
infoHolder.tokens[it.blockchain]?.let { tokens ->
builder.append("Tokens:")
appendLine()
tokens.forEach { token ->
builder.appendKeyValue("Name", token.name)
builder.appendKeyValue("ID", token.id ?: "[custom token]")
builder.appendKeyValue("Contract address", token.contractAddress)
}
}
}
return this
}
fun appendTxFailedBlockchainInfo(error: String): EmailDataBuilder {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
builder.appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
builder.appendKeyValue("Derivation path", walletInfo.derivationPath)
builder.appendKeyValue("Host", walletInfo.host)
builder.appendKeyValue("Token", infoHolder.token)
builder.appendKeyValue("Error", error)
builder.appendDelimiter()
builder.appendKeyValue("Source address", walletInfo.address)
builder.appendKeyValue("Destination address", infoHolder.destinationAddress)
builder.appendKeyValue("Amount", infoHolder.amount)
builder.appendKeyValue("Fee", infoHolder.fee)
return this
}
fun appendPhoneInfo(): EmailDataBuilder {
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return this
}
fun build(): String = builder.toString()
}
private fun StringBuilder.appendKeyValue(key: String, value: String): StringBuilder {
return if (value.isNotBlank()) this.append("$key: $value\n") else this
}
private fun StringBuilder.appendDelimiter(): StringBuilder = append("----------\n")
private fun StringBuilder.appendLine(count: Int = 1): StringBuilder {
return append(List(count) { "\n" }.joinToString(separator = ""))
}

View file

@ -38,7 +38,9 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop(getRegionProvider())) },
onShopButtonClick = {
store.dispatch(HomeAction.GoToShop(store.state.globalState.userCountryCode))
},
onSearchTokensClick = {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))

View file

@ -44,5 +44,8 @@ class TelephonyManagerRegionProvider(context: Context) : RegionProvider {
}
class LocaleRegionProvider : RegionProvider {
override fun getRegion(): String? = Locale.current.region
}
override fun getRegion(): String = Locale.current.region
}
const val RUSSIA_COUNTRY_CODE = "ru"
const val BELARUS_COUNTRY_CODE = "by"

View file

@ -1,153 +0,0 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.scaleIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun FirstStoriesContent(
paused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(paused) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxSize(),
) {
val text = when (screenState.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
if (screenState.value != StartingScreenState.MEET_TANGEM) {
TextAutoSize(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.MEET_TANGEM,
enter = slideInVertically() { it / 2 }
) {
TextAutoSize(
modifier = Modifier
.fillMaxWidth()
.padding(start = 40.dp, end = 40.dp),
text = text?.let { stringResource(text) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
}
AnimatedVisibility(
visible = screenState.value == StartingScreenState.SHOW_CARD ||
screenState.value == StartingScreenState.MEET_TANGEM,
enter = scaleIn(initialScale = 3f)
) {
Image(
painter = painterResource(
id = R.drawable.meet_tangem
),
contentDescription = null,
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
}
}
enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@Preview
@Composable
fun Stories1Preview() {
FirstStoriesContent(false, 7_000) {}
}

View file

@ -24,8 +24,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerS24
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.features.home.compose.content.*
import com.tangem.tap.features.home.compose.views.HomeButtons
import com.tangem.tap.features.home.compose.views.StoriesProgressBar
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
@ -39,8 +42,6 @@ fun StoriesScreen(
onSearchTokensClick: () -> Unit,
) {
val steps = 6
val stepDuration = 8_000
val currentStep = remember { mutableStateOf(1) }
val isDarkBackground = currentStep.value !in 3..5
@ -55,7 +56,7 @@ fun StoriesScreen(
val isPressed = remember { mutableStateOf(false) }
val needsToBePaused =
remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) }
val pause = isPressed.value || needsToBePaused.value
val isPaused = isPressed.value || needsToBePaused.value
val hideContent = remember { mutableStateOf(true) }
@ -122,9 +123,8 @@ fun StoriesScreen(
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,
// paused = isPressed.value,
stepDuration = stepDuration,
paused = pause,
stepDuration = currentStep.duration(),
paused = isPaused,
onStepFinished = goToNextScreen,
)
Image(
@ -139,12 +139,12 @@ fun StoriesScreen(
colorFilter = if (isDarkBackground) null else ColorFilter.tint(Color.Black)
)
when (currentStep.value) {
1 -> FirstStoriesContent(pause, stepDuration) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet()
3 -> StoriesUltraSecureBackup()
4 -> StoriesThousandsOfCurrencies()
5 -> StoriesWeb3()
6 -> StoriesWalletForEveryone()
1 -> FirstStoriesContent(isPaused, currentStep.duration()) { hideContent.value = it }
2 -> StoriesRevolutionaryWallet(currentStep.duration())
3 -> StoriesUltraSecureBackup(isPaused, currentStep.duration())
4 -> StoriesCurrencies(isPaused, currentStep.duration())
5 -> StoriesWeb3(isPaused, currentStep.duration())
6 -> StoriesWalletForEveryone(currentStep.duration())
}
}
Column(
@ -157,8 +157,7 @@ fun StoriesScreen(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.height(48.dp)
,
.height(48.dp),
colors = ButtonDefaults.textButtonColors(
backgroundColor = Color.White,
contentColor = Color(0xFF080C10)
@ -188,9 +187,13 @@ fun StoriesScreen(
}
}
private fun MutableState<Int>.duration(): Int = when (this.value) {
1 -> 8000
else -> 6000
}
@Preview
@Composable
fun InstagramScreenPreview() {
fun StoriesScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {})
}

View file

@ -0,0 +1,152 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.core.*
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.toAnimatable
@Composable
fun HorizontalSlidingImage(
painter: Painter,
paused: Boolean,
duration: Int,
itemSize: DpSize,
startOffset: Float,
targetOffset: Float,
contentDescription: String,
) {
val translateX = AnimatedValue(startOffset * -1f, (startOffset + targetOffset) * -1f)
Image(
modifier = Modifier
.requiredWidth(itemSize.width)
.requiredHeight(itemSize.height)
.graphicsLayer(
translationX = translateX.toAnimatable(isPaused = paused, duration = duration).value
),
alignment = Alignment.TopStart,
contentScale = ContentScale.FillBounds,
painter = painter,
contentDescription = contentDescription,
)
}
@Composable
fun StoriesTextAnimation(
slideInDuration: Int = 500,
slideInDelay: Int = 200,
slideDistance: Dp = 60.dp,
label: String = "",
content: @Composable (Modifier) -> Unit
) {
val isLaunched = remember { mutableStateOf(false) }
val transition = updateTransition(targetState = isLaunched.value, label = label)
val offsetY = transition.animateDp(
transitionSpec = {
tween(
durationMillis = slideInDuration,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Slide in"
) { value -> if (value) 0.dp else slideDistance }
val alpha = transition.animateFloat(
transitionSpec = {
tween(
durationMillis = slideInDuration * 2,
delayMillis = slideInDelay,
easing = FastOutSlowInEasing
)
},
label = "Visibility"
) { value -> if (value) 1f else 0f }
content(
Modifier
.absoluteOffset(y = offsetY.value)
.alpha(alpha.value)
)
LaunchedEffect(Unit) { isLaunched.value = true }
}
@Composable
fun StoriesBottomImageAnimation(
initialScale: Float = 2.5f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit
) {
val scaleSwitchBarrier = 1.15f
val secondStepDuration = totalDuration - firstStepDuration
val isFirstStepLaunched = remember { mutableStateOf(false) }
val isSecondStepLaunched = remember { mutableStateOf(false) }
val firstTransition = updateTransition(
targetState = isFirstStepLaunched.value,
label = "Image appearing"
)
val firstScaleStep = firstTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = firstStepDuration,
easing = FastOutLinearInEasing,
)
},
label = "Appearing scale"
) { value -> if (value) scaleSwitchBarrier else initialScale }
val secondTransition = updateTransition(
targetState = isSecondStepLaunched.value,
label = "Image slow outgoing"
)
val secondScaleStep = secondTransition.animateFloat(
transitionSpec = {
tween(
durationMillis = secondStepDuration,
easing = LinearEasing,
)
},
label = "Outgoing scale"
) { value -> if (value) 1f else scaleSwitchBarrier }
val fadeIn = firstTransition.animateFloat(
transitionSpec = { tween(durationMillis = 400) },
label = "Fade in on start"
) { value -> if (value) 1f else 0f }
if (firstScaleStep.value == scaleSwitchBarrier) {
isSecondStepLaunched.value = true
}
val modifier = if (!isSecondStepLaunched.value) {
Modifier.scale(firstScaleStep.value)
} else {
Modifier.scale(secondScaleStep.value)
}.alpha(fadeIn.value)
content(modifier)
LaunchedEffect(Unit) { isFirstStepLaunched.value = true }
}

View file

@ -1,162 +0,0 @@
package com.tangem.tap.features.home.compose
import android.content.Context
import android.graphics.Typeface
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.compose.SpacerS16
import com.tangem.tap.common.compose.SpacerS24
import com.tangem.tap.common.compose.extensions.toAndroidGraphicsColor
import com.tangem.wallet.R
@Composable
fun StoriesGeneralContent(
titleText: String,
subtitleText: String,
imageSource: Int?,
isDarkBackground: Boolean,
subtitleTextId: Int? = null,
imageComposable: (() -> Unit)? = null,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier =
Modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
Text(
text = titleText,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
SpacerS16()
SubtitleText(subtitleText, subtitleTextId)
SpacerS24()
if (imageSource != null) {
Image(
painter = painterResource(id = imageSource),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth()
)
}
imageComposable?.invoke()
}
}
@Composable
fun SubtitleText(subtitleText: String, subtitleTextId: Int?) {
val color = Color(0xFFA6AAAD)
if (subtitleTextId == null) {
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier.padding(start = 40.dp, end = 40.dp),
color = color,
textAlign = TextAlign.Center
)
} else {
HtmlText(subtitleTextId) { context ->
val padding = context.dpToPx(40f).toInt()
TextView(context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
setPadding(padding, 0, padding, 0)
textAlignment = View.TEXT_ALIGNMENT_CENTER
typeface = Typeface.DEFAULT
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
setTextColor(color.toAndroidGraphicsColor())
}
}
}
}
@Composable
fun StoriesRevolutionaryWallet() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description),
imageSource = R.drawable.revolutionary_wallet,
isDarkBackground = true
)
}
@Composable
fun StoriesUltraSecureBackup() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = stringResource(id = R.string.story_backup_description),
imageSource = R.drawable.floating_cards,
subtitleTextId = R.string.story_backup_description,
isDarkBackground = false
)
}
@Composable
fun StoriesThousandsOfCurrencies() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description),
imageSource = R.drawable.thousands_of_currencies,
isDarkBackground = false
)
}
@Composable
fun StoriesWeb3() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description),
imageSource = R.drawable.web_3,
isDarkBackground = false
)
}
@Composable
fun StoriesWalletForEveryone() {
StoriesGeneralContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description),
imageSource = R.drawable.wallet_for_everyone,
isDarkBackground = true
)
}
@Composable
fun HtmlText(
stringResId: Int,
modifier: Modifier = Modifier,
factory: (Context) -> TextView
) {
AndroidView(factory, modifier) { it.text = it.context.getText(stringResId) }
}

View file

@ -0,0 +1,263 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.SpacerH16
import com.tangem.tap.common.compose.SpacerH32
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun StoriesRevolutionaryWallet(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_awe_title),
subtitleText = stringResource(id = R.string.story_awe_description).annotated(),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 300,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.revolutionary_wallet,
isDarkBackground = true,
)
}
}
)
}
@Composable
fun StoriesUltraSecureBackup(isPaused: Boolean, stepDuration: Int) {
val subtitleText = buildAnnotatedString {
append(stringResource(id = R.string.story_backup_description_1))
append(" ")
withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(id = R.string.story_backup_description_2_bold))
}
append(" ")
append(stringResource(id = R.string.story_backup_description_3))
}
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_backup_title),
subtitleText = subtitleText,
isDarkBackground = false,
)
},
bottomContent = {
FloatingCardsContent(isPaused, stepDuration)
}
)
}
@Composable
fun StoriesCurrencies(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_currencies_title),
subtitleText = stringResource(id = R.string.story_currencies_description).annotated(),
isDarkBackground = false,
)
},
bottomContent = {
StoriesCurrenciesContent(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWeb3(isPaused: Boolean, stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_web3_title),
subtitleText = stringResource(id = R.string.story_web3_description).annotated(),
isDarkBackground = false,
)
},
bottomContent = {
StoriesWeb3Content(paused = isPaused, duration = stepDuration)
}
)
}
@Composable
fun StoriesWalletForEveryone(stepDuration: Int) {
SplitContent(
topContent = {
TopContent(
titleText = stringResource(id = R.string.story_finish_title),
subtitleText = stringResource(id = R.string.story_finish_description).annotated(),
isDarkBackground = true,
)
},
bottomContent = {
StoriesBottomImageAnimation(
totalDuration = stepDuration,
firstStepDuration = 500,
) { modifier ->
StoriesImage(
modifier = modifier,
drawableResId = R.drawable.wallet_for_everyone,
isDarkBackground = true,
)
}
}
)
}
@Composable
private fun SplitContent(
topContent: @Composable () -> Unit,
bottomContent: @Composable () -> Unit,
) {
Column(
modifier = Modifier
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) {
topContent()
bottomContent()
}
}
@Composable
private fun TopContent(
titleText: String,
subtitleText: AnnotatedString,
isDarkBackground: Boolean,
) {
SpacerH32()
StoriesTitleText(
text = titleText,
isDarkBackground = isDarkBackground,
)
SpacerH16()
StoriesSubtitleText(
subtitleText = subtitleText,
)
SpacerH32()
}
@Composable
private fun StoriesTitleText(
text: String,
isDarkBackground: Boolean,
) {
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 150,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
text = text,
fontSize = 32.sp,
fontWeight = FontWeight.SemiBold,
color = if (isDarkBackground) Color.White else Color(0xFF090E13),
textAlign = TextAlign.Center
)
}
}
@Composable
private fun StoriesSubtitleText(
subtitleText: AnnotatedString,
) {
val color = Color(0xFFA6AAAD)
StoriesTextAnimation(
slideInDuration = 500,
slideInDelay = 400,
) { modifier ->
Text(
modifier = modifier
.padding(start = 40.dp, end = 40.dp),
fontWeight = FontWeight.Normal,
text = subtitleText,
fontSize = 20.sp,
color = color,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun StoriesImage(
modifier: Modifier = Modifier,
@DrawableRes drawableResId: Int,
isDarkBackground: Boolean,
) {
Image(
painter = painterResource(id = drawableResId),
contentDescription = null,
contentScale = if (isDarkBackground) ContentScale.Inside else ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
)
}
private fun String.annotated(): AnnotatedString {
val source = this
return buildAnnotatedString { append(source) }
}
@Preview
@Composable
private fun RevolutionaryWalletPreview() {
StoriesRevolutionaryWallet(6000)
}
@Preview
@Composable
private fun UltraSecureBackupPreview() {
StoriesUltraSecureBackup(false, 6000)
}
@Preview
@Composable
private fun CurrenciesPreview() {
StoriesCurrencies(false, 6000)
}
@Preview
@Composable
private fun Web3Preview() {
StoriesWeb3(false, 6000)
}
@Preview
@Composable
private fun WalletForEveryonePreview() {
StoriesWalletForEveryone(6000)
}

View file

@ -0,0 +1,144 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.SpacerH
import com.tangem.tap.common.compose.extensions.dpSize
import com.tangem.tap.common.compose.extensions.halfHeight
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.common.extensions.isEven
import com.tangem.tap.features.home.compose.HorizontalSlidingImage
import com.tangem.wallet.R
@Composable
fun StoriesCurrenciesContent(
paused: Boolean,
duration: Int,
) {
val currencyDrawableList = remember {
listOf(
R.drawable.currency0,
R.drawable.currency1,
R.drawable.currency2,
R.drawable.currency3,
R.drawable.currency4,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / currencyDrawableList.size }
val designItemHeight = remember { 82.dp }
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
currencyDrawableList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.halfHeight()
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 50.dp - (50.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Currency row",
)
SpacerH(12.dp)
}
}
}
}
@Composable
fun StoriesWeb3Content(
paused: Boolean,
duration: Int,
) {
val dappsItemList = remember {
listOf(
R.drawable.dapps0,
R.drawable.dapps1,
R.drawable.dapps2,
R.drawable.dapps3,
R.drawable.dapps4,
R.drawable.dapps5,
)
}
val screenWidth = LocalConfiguration.current.screenWidthDp.dp
val decreaseRate = remember { 1f / dappsItemList.size }
val designItemHeight = 75.dp
LightenBox {
Column(modifier = Modifier.graphicsLayer(clip = false)) {
dappsItemList.forEachIndexed { index, drawableResId ->
val painter = painterResource(id = drawableResId)
val scaledItemSize = scaleToDesignSize(painter.dpSize(), designItemHeight = designItemHeight)
val itemOversizedScreenWidthBy = scaledItemSize.width - screenWidth
val moveItemToStartOfScreen = itemOversizedScreenWidthBy / 2
val chessOffset = if (index.isEven()) 0.dp else scaledItemSize.width / 3
val animateFrom = chessOffset - moveItemToStartOfScreen
val animateTo = 70.dp - (70.dp * index * decreaseRate)
HorizontalSlidingImage(
paused = paused,
duration = duration,
painter = painter,
itemSize = scaledItemSize,
startOffset = animateFrom.toPx(),
targetOffset = animateTo.toPx(),
contentDescription = "Web3 row",
)
}
}
}
}
@Composable
private fun LightenBox(content: @Composable () -> Unit) {
Box() {
content()
Box(
modifier = Modifier
.fillMaxSize()
.padding(top = 250.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.White.copy(alpha = 0f),
Color.White.copy(alpha = 0.75f),
Color.White.copy(alpha = 0.95f),
Color.White
)
)
)
) {}
}
}
private fun scaleToDesignSize(itemSize: DpSize, designItemHeight: Dp): DpSize {
val scaleRate = itemSize.height / designItemHeight
return itemSize / scaleRate
}

View file

@ -0,0 +1,163 @@
package com.tangem.tap.features.home.compose.content
import androidx.annotation.StringRes
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.tap.common.compose.FontSizeRange
import com.tangem.tap.common.compose.TextAutoSize
import com.tangem.tap.features.home.compose.StoriesBottomImageAnimation
import com.tangem.tap.features.home.compose.StoriesTextAnimation
import com.tangem.wallet.R
@Composable
fun FirstStoriesContent(
isPaused: Boolean, duration: Int = 8_000,
hideContent: (Boolean) -> Unit
) {
val screenState = remember { mutableStateOf(StartingScreenState.INIT) }
val progress = remember { Animatable(0f) }
LaunchedEffect(isPaused) {
if (isPaused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 2f,
animationSpec = tween(
durationMillis = duration,
easing = LinearEasing
)
)
}
}
when (progress.value) {
in 0f..0.2f -> screenState.value = StartingScreenState.INIT
in 0.2f..0.3f -> screenState.value = StartingScreenState.BUY
in 0.3f..0.4f -> screenState.value = StartingScreenState.STORE
in 0.4f..0.5f -> screenState.value = StartingScreenState.SEND
in 0.5f..0.6f -> screenState.value = StartingScreenState.PAY
in 0.6f..0.7f -> screenState.value = StartingScreenState.EXCHANGE
in 0.7f..0.8f -> screenState.value = StartingScreenState.BORROW
in 0.8f..1f -> screenState.value = StartingScreenState.LEND
in 1f..1.2f -> screenState.value = StartingScreenState.SHOW_CARD
in 1.2f..2f -> screenState.value = StartingScreenState.MEET_TANGEM
}
if (screenState.value == StartingScreenState.INIT) hideContent(true)
if (screenState.value == StartingScreenState.BUY) hideContent(false)
val style = TextStyle(
fontSize = 60.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
textAlign = TextAlign.Center,
)
val textId = screenState.textId()
Box(modifier = Modifier.fillMaxSize()) {
if (screenState.isSplashingTextDisplaying()) {
TextAutoSize(
modifier = Modifier
.align(Alignment.Center)
.padding(start = 20.dp, end = 20.dp, bottom = 100.dp),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(20.sp, 60.sp)
)
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.weight(0.7f)
) {
if (screenState.isMeetTangemDisplaying()) {
StoriesTextAnimation(
slideInDelay = 0,
) { modifier ->
TextAutoSize(
modifier = modifier
.padding(start = 20.dp, end = 20.dp, top = 50.dp)
.alpha(if (screenState.value == StartingScreenState.SHOW_CARD) 0f else 1f),
text = textId?.let { stringResource(textId) } ?: "",
textStyle = style,
fontSizeRange = FontSizeRange(30.sp, 50.sp)
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1.2f)
.wrapContentSize(),
) {
StoriesBottomImageAnimation(
totalDuration = duration,
firstStepDuration = 400,
) { modifier ->
Image(
modifier = modifier.fillMaxWidth(),
painter = painterResource(id = R.drawable.meet_tangem),
contentDescription = "Tangem Wallet card",
)
}
}
}
}
}
}
private enum class StartingScreenState {
INIT, BUY, STORE, SEND, PAY, EXCHANGE, BORROW, LEND, SHOW_CARD, MEET_TANGEM
}
@StringRes
private fun MutableState<StartingScreenState>.textId(): Int? = when (this.value) {
StartingScreenState.INIT -> null
StartingScreenState.BUY -> R.string.story_meet_buy
StartingScreenState.STORE -> R.string.story_meet_store
StartingScreenState.SEND -> R.string.story_meet_send
StartingScreenState.PAY -> R.string.story_meet_pay
StartingScreenState.EXCHANGE -> R.string.story_meet_exchange
StartingScreenState.BORROW -> R.string.story_meet_borrow
StartingScreenState.LEND -> R.string.story_meet_lend
StartingScreenState.SHOW_CARD -> R.string.story_meet_title
StartingScreenState.MEET_TANGEM -> R.string.story_meet_title
}
private fun MutableState<StartingScreenState>.isSplashingTextDisplaying(): Boolean {
return this.value != StartingScreenState.MEET_TANGEM &&
this.value != StartingScreenState.SHOW_CARD
}
private fun MutableState<StartingScreenState>.isMeetTangemDisplaying(): Boolean {
return this.value == StartingScreenState.MEET_TANGEM
}
@Preview
@Composable
fun FirstStoriesPreview() {
FirstStoriesContent(false, 8000) {}
}

View file

@ -0,0 +1,103 @@
package com.tangem.tap.features.home.compose.content
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import com.tangem.tap.common.compose.extensions.AnimatedValue
import com.tangem.tap.common.compose.extensions.asImageBitmap
import com.tangem.tap.common.compose.extensions.toAnimatable
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
@Composable
fun FloatingCardsContent(
isPaused: Boolean,
stepDuration: Int,
) {
val imageBitmap = asImageBitmap(R.drawable.card_placeholder_wallet)
val cards = listOf(
FloatingCard.first(),
FloatingCard.second(),
FloatingCard.third(),
)
Box() {
cards.forEach { floatingCard ->
FloatingCard.Item(
isPaused = isPaused,
imageBitmap = imageBitmap,
cardValues = floatingCard,
stepDuration = stepDuration,
)
}
}
}
private data class CardValues(
val translateX: AnimatedValue = AnimatedValue(0f, 0f),
val translateY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationX: AnimatedValue = AnimatedValue(0f, 0f),
val rotationY: AnimatedValue = AnimatedValue(0f, 0f),
val rotationZ: AnimatedValue = AnimatedValue(0f, 0f),
val scale: AnimatedValue = AnimatedValue(1f, 1f),
)
private class FloatingCard {
companion object {
@Composable
fun Item(
isPaused: Boolean,
stepDuration: Int,
imageBitmap: ImageBitmap,
cardValues: CardValues,
) {
Image(
bitmap = imageBitmap,
contentDescription = "Floating Tangem card",
modifier = Modifier
.graphicsLayer(
translationX = cardValues.translateX.toAnimatable(isPaused, stepDuration).value,
translationY = cardValues.translateY.toAnimatable(isPaused, stepDuration).value,
rotationX = cardValues.rotationX.toAnimatable(isPaused, stepDuration).value,
rotationY = cardValues.rotationY.toAnimatable(isPaused, stepDuration).value,
rotationZ = cardValues.rotationZ.toAnimatable(isPaused, stepDuration).value,
scaleX = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
scaleY = cardValues.scale.toAnimatable(isPaused, stepDuration).value,
),
)
}
fun first(): CardValues = CardValues(
translateX = -400f to -350f,
translateY = 30f to 32f,
rotationX = 10f to 15f,
rotationY = 15f to 15f,
rotationZ = 40f to 27f,
scale = 0.6f to 0.6f,
)
fun second(): CardValues = CardValues(
translateX = 350f to 300f,
translateY = -70f to 0f,
rotationX = 30f to 48f,
rotationY = 0f to 5f,
rotationZ = -34f to -42f,
scale = 0.47f to 0.35f,
)
fun third(): CardValues = CardValues(
translateX = 320f to 250f,
translateY = 500f to 500f,
rotationX = 0f to 3f,
rotationY = 10f to 10f,
rotationZ = -45f to -30f,
scale = 0.6f to 0.75f,
)
}
}

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row

View file

@ -1,4 +1,4 @@
package com.tangem.tap.features.home.compose
package com.tangem.tap.features.home.compose.views
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing

View file

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

View file

@ -15,6 +15,8 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.BELARUS_COUNTRY_CODE
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.home.redux.HomeMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
@ -30,19 +32,19 @@ class HomeMiddleware {
companion object {
val handler = homeMiddleware
const val CARD_SHOP_URI = "http://cards.tangem.com/"
const val BUY_WALLET_URL = "https://mv.tangem.com/"
const val BUY_WALLET_URL = "https://tangem.com/ru/resellers/"
}
}
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
private val homeMiddleware: Middleware<AppState> = { _, _ ->
{ next ->
{ action ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.InitCurrencyExchangeManager)
store.dispatch(GlobalAction.ExchangeManager.Init)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
store.dispatch(GlobalAction.FetchUserCountry)
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
@ -55,8 +57,9 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
// store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomTokens))
}
is HomeAction.GoToShop -> {
when (action.regionProvider.getRegion()?.toLowerCase()) {
"ru" -> store.dispatchOpenUrl(BUY_WALLET_URL)
when (action.userCountryCode) {
RUSSIA_COUNTRY_CODE, BELARUS_COUNTRY_CODE ->
store.dispatchOpenUrl(BUY_WALLET_URL)
else -> store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
}
store.state.globalState.analyticsHandlers?.triggerEvent(

View file

@ -83,7 +83,7 @@ class OnboardingManager(
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown, null),
val currency: Currency = Currency.Blockchain(Blockchain.Unknown, null),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,

View file

@ -10,7 +10,7 @@ import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.extensions.fadeIn
import com.tangem.tangem_sdk_new.extensions.fadeOut
@ -67,11 +67,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
override fun newState(state: OnboardingNoteState) {
if (activity == null || view == null) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.store
import org.rekotlin.StateType
@ -26,8 +25,8 @@ data class OnboardingNoteState(
val progress: Int
get() = steps.indexOf(currentStep)
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
}
}

View file

@ -7,7 +7,7 @@ import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
@ -54,11 +54,11 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
if (activity == null || view == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
binding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.store
@ -56,8 +55,8 @@ data class TwinCardsState(
val showAlert: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
}
}

View file

@ -9,13 +9,18 @@ import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.beginDelayedTransition
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
@ -75,24 +80,28 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), deviceScaleFactorForWelcomeState) {
twinsWidget = TwinsCardWidget(
LeapfrogWidget(binding.onboardingTopContainer.cardsContainer),
deviceScaleFactorForWelcomeState
) {
285f * deviceScaleFactorForWelcomeState
}
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
btnRefreshBalanceWidget =
RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
binding.toolbar.title = getText(R.string.twins_recreate_toolbar)
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvTwinFrontCard)
binding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.error(R.drawable.card_placeholder_white)
.placeholder(R.drawable.card_placeholder_white)
?.into(binding.onboardingTopContainer.imvTwinBackCard)
binding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2) {
placeholder(R.drawable.card_placeholder_white)
error(R.drawable.card_placeholder_white)
fallback(R.drawable.card_placeholder_white)
}
}
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =

View file

@ -91,7 +91,12 @@ private fun handleWalletAction(action: Action) {
BlockchainNetwork(Blockchain.Bitcoin, result.data.card),
BlockchainNetwork(Blockchain.Ethereum, result.data.card)
)
store.dispatchOnMain(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks,
cardId = result.data.card.cardId
)
)
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingWalletAction.ProceedBackup)
}
@ -272,7 +277,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
BlockchainNetwork(Blockchain.Bitcoin, result.data),
BlockchainNetwork(Blockchain.Ethereum, result.data)
)
store.dispatchOnMain(WalletAction.MultiWallet.SaveCurrencies(blockchainNetworks))
store.dispatchOnMain(
WalletAction.MultiWallet.SaveCurrencies(
blockchainNetworks = blockchainNetworks, cardId = result.data.cardId
)
)
if (backupService.currentState == BackupService.State.Finished) {
store.dispatchOnMain(BackupAction.FinishBackup)
} else {

View file

@ -11,9 +11,9 @@ import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
import coil.load
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.squareup.picasso.Picasso
import com.tangem.common.CardIdFormatter
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
@ -22,7 +22,12 @@ import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -124,11 +129,11 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
private fun loadImageIntoImageView(url: String?, view: ImageView) {
Picasso.get()
.load(url)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(view)
view.load(url) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
private fun setupCreateWalletState() = with(binding) {

View file

@ -7,6 +7,7 @@ import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.store
import java.math.BigDecimal
@ -15,10 +16,6 @@ import java.math.BigDecimal
*/
class ReceiptReducer : SendInternalReducer {
companion object {
const val EMPTY = "-"
}
private lateinit var sendState: SendState
private lateinit var amountState: AmountState
private lateinit var feeState: FeeState
@ -137,9 +134,9 @@ class ReceiptReducer : SendInternalReducer {
)
} else {
ReceiptTokenFiat(
amountFiat = EMPTY,
feeFiat = EMPTY,
totalFiat = EMPTY,
amountFiat = UNKNOWN_AMOUNT_SIGN,
feeFiat = UNKNOWN_AMOUNT_SIGN,
totalFiat = UNKNOWN_AMOUNT_SIGN,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
@ -169,7 +166,7 @@ class ReceiptReducer : SendInternalReducer {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString().addPrecisionSign(),
totalFiat = EMPTY,
totalFiat = UNKNOWN_AMOUNT_SIGN,
symbols = symbols
)
}
@ -206,7 +203,7 @@ class ReceiptReducer : SendInternalReducer {
return when {
!isToken && sendState.coinIsConvertible() -> sendState.coinConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
isToken && sendState.tokenIsConvertible() -> sendState.tokenConverter!!.toFiatWithPrecision(value).stripZeroPlainString()
else -> EMPTY
else -> UNKNOWN_AMOUNT_SIGN
}
}

View file

@ -2,8 +2,8 @@ package com.tangem.tap.features.send.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.feedback.SendTransactionFailedEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.SendTransactionFailedEmail
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -19,7 +19,7 @@ class SendTransactionFailsDialog {
setTitle(R.string.alert_failed_to_send_transaction_title)
setMessage(context.getString(R.string.alert_failed_to_send_transaction_message, dialog.errorMessage))
setNeutralButton(R.string.alert_button_send_feedback) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage)))
store.dispatch(GlobalAction.SendEmail(SendTransactionFailedEmail(dialog.errorMessage)))
}
setPositiveButton(R.string.common_no) { _, _ -> }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }

View file

@ -17,12 +17,13 @@ import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.reducers.ReceiptReducer
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.features.send.ui.FeeUiHelper
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.send.ui.dialogs.SendTransactionFailsDialog
import com.tangem.tap.features.send.ui.dialogs.TezosWarningDialog
import com.tangem.tap.features.wallet.redux.WalletState.Companion.ROUGH_SIGN
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.store
import com.tangem.wallet.R
@ -257,12 +258,12 @@ class SendStateSubscriber(fragment: BaseStoreFragment) :
val mainLayout = clReceiptContainer as ViewGroup
val totalLayout = llTotalContainer.llTotal as ViewGroup
val totalTokenLayout = llTotalContainer.flTotalTokenCrypto as ViewGroup
fun getString(id: Int, vararg formatStrings: String): String =
mainLayout.context.getString(id, *formatStrings)
val rough = getString(R.string.sign_rough)
fun roughOrEmpty(value: String): String =
if (value == ReceiptReducer.EMPTY) value else "$rough $value"
fun getString(id: Int, vararg formatStrings: String): String = mainLayout.getString(id, *formatStrings)
fun roughOrEmpty(value: String): String {
return if (value == UNKNOWN_AMOUNT_SIGN) value else "$ROUGH_SIGN $value"
}
when (state.visibleTypeOfReceipt) {
ReceiptLayoutType.FIAT -> {

View file

@ -28,8 +28,6 @@ sealed class TokensAction : Action {
val wallets: List<WalletData>, val derivationStyle: DerivationStyle?
) : TokensAction()
data class SetNonRemovableCurrencies(val wallets: List<WalletData>) : TokensAction()
data class SaveChanges(
val addedTokens: List<TokenWithBlockchain>,
val addedBlockchains: List<Blockchain>

View file

@ -44,13 +44,13 @@ class TokensMiddleware {
when (action) {
is TokensAction.LoadCurrencies -> handleLoadCurrencies(action.scanResponse)
is TokensAction.SaveChanges -> handleSaveChanges(action)
is TokensAction.PrepareAndNavigateToAddCustomToken -> handleAddingCustomToken(
action
)
is TokensAction.PrepareAndNavigateToAddCustomToken -> {
handleAddingCustomToken(action)
}
is TokensAction.SetSearchInput -> {
handleLoadCurrencies(
scanResponse = store.state.globalState.scanResponse,
action.searchInput
newSearchInput = action.searchInput
)
}
is TokensAction.LoadMore -> {
@ -138,7 +138,9 @@ class TokensMiddleware {
)
)
if (tokensToAdd.isEmpty() && blockchainsToAdd.isEmpty()) {
if (tokensToAdd.isEmpty() && tokensToRemove.isEmpty()
&& blockchainsToAdd.isEmpty() && blockchainsToRemove.isEmpty()
) {
store.dispatchDebugErrorNotification("Nothing to save")
store.dispatch(NavigationAction.PopBackTo())
return
@ -308,12 +310,10 @@ class TokensMiddleware {
private fun removeCurrenciesIfNeeded(currencies: List<Currency>) {
if (currencies.isNotEmpty()) {
currencies.forEach { currency ->
store.state.walletState.getWalletData(currency)?.let {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(
walletData = it,
fromWalletDetails = false
))
}
store.dispatch(WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.AddTokens
))
}
}
}

View file

@ -43,13 +43,6 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
derivationStyle = action.derivationStyle
)
}
is TokensAction.SetNonRemovableCurrencies -> {
tokensState.copy(
nonRemovableBlockchains = action.wallets.toNonCustomBlockchains(tokensState.derivationStyle),
nonRemovableTokens = action.wallets.toTokensContractAddresses(),
)
}
is TokensAction.AllowToAddTokens -> {
tokensState.copy(allowToAdd = action.allow)
}

View file

@ -14,8 +14,6 @@ data class TokensState(
val addedWallets: List<WalletData> = emptyList(),
val addedTokens: List<TokenWithBlockchain> = emptyList(),
val addedBlockchains: List<Blockchain> = emptyList(),
val nonRemovableTokens: List<ContractAddress> = emptyList(),
val nonRemovableBlockchains: List<Blockchain> = emptyList(),
val currencies: List<Currency> = emptyList(),
val searchInput: String? = null,
val allowToAdd: Boolean = true,

View file

@ -21,6 +21,8 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.common.compose.Keyboard
import com.tangem.tap.common.compose.extensions.addAndNotify
import com.tangem.tap.common.compose.extensions.removeAndNotify
import com.tangem.tap.common.compose.keyboardAsState
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.pixelsToDp
@ -30,6 +32,7 @@ import com.tangem.tap.features.tokens.redux.ContractAddress
import com.tangem.tap.features.tokens.redux.LoadCoinsState
import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -40,30 +43,30 @@ fun CurrenciesScreen(
onNetworkItemClicked: (ContractAddress) -> Unit,
onLoadMore: () -> Unit
) {
val context = LocalContext.current
val tokensAddedOnMainScreen = remember { tokensState.value.addedTokens }
val blockchainsAddedOnMainScreen = remember { tokensState.value.addedBlockchains }
val addedTokensState = remember { mutableStateOf(tokensState.value.addedTokens) }
val addedBlockchainsState = remember { mutableStateOf(tokensState.value.addedBlockchains) }
val isKeyboardOpen by keyboardAsState()
val onAddCurrencyToggleClick = { currency: Currency, token: TokenWithBlockchain? ->
if (token != null) {
val mutableList = addedTokensState.value.toMutableList()
if (mutableList.contains(token)) {
mutableList.remove(token)
} else {
mutableList.add(token)
}
addedTokensState.value = mutableList
} else {
val blockchain = Blockchain.fromNetworkId(currency.id)
val mutableList = addedBlockchainsState.value.toMutableList()
if (mutableList.contains(blockchain)) {
mutableList.remove(blockchain)
} else {
blockchain?.let { mutableList.add(blockchain) }
}
addedBlockchainsState.value = mutableList
val blockchain = Blockchain.fromNetworkId(currency.id)
if (blockchain != null && token == null) {
toggleBlockchain(
blockchain,
blockchainsAddedOnMainScreen,
addedBlockchainsState,
addedTokensState.value,
)
} else if (token != null) {
toggleToken(
token,
tokensAddedOnMainScreen,
addedTokensState,
tokensState.value,
)
}
}
@ -100,23 +103,11 @@ fun CurrenciesScreen(
ListOfCurrencies(
header = { if (showHeader) CurrenciesWarning() },
currencies = tokensState.value.currencies,
nonRemovableTokens = tokensState.value.nonRemovableTokens,
nonRemovableBlockchains = tokensState.value.nonRemovableBlockchains,
addedTokens = addedTokensState.value,
addedBlockchains = addedBlockchainsState.value,
allowToAdd = tokensState.value.allowToAdd,
onAddCurrencyToggled = { currency, token ->
onAddCurrencyToggleClick(currency, token)
token?.let {
if (!tokensState.value.canHandleToken(it)) {
val dialog = AppDialog.SimpleOkDialog(
header = context.getString(R.string.common_warning),
message = context.getString(R.string.alert_manage_tokens_unsupported_message)
) { onAddCurrencyToggleClick(currency, it) }
store.dispatchDialogShow(dialog)
}
}
},
onNetworkItemClicked = onNetworkItemClicked,
onLoadMore = onLoadMore
@ -127,6 +118,68 @@ fun CurrenciesScreen(
}
}
private fun toggleBlockchain(
blockchain: Blockchain,
blockchainsAddedOnMainScreen: List<Blockchain>,
addedBlockchainsState: MutableState<List<Blockchain>>,
addedTokens: List<TokenWithBlockchain>
) {
val isTryingToRemove = addedBlockchainsState.value.contains(blockchain)
val isAddedOnMainScreen = blockchainsAddedOnMainScreen.contains(blockchain)
val isTokenWithSameBlockchainFound = addedTokens.any { it.blockchain == blockchain }
if (isTryingToRemove) {
if (isTokenWithSameBlockchainFound) {
store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog(
currencyTitle = blockchain.name,
currencySymbol = blockchain.currency
))
} else {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = blockchain.name,
onOk = { addedBlockchainsState.removeAndNotify(blockchain) }
))
} else {
addedBlockchainsState.removeAndNotify(blockchain)
}
}
} else {
addedBlockchainsState.addAndNotify(blockchain)
}
}
private fun toggleToken(
token: TokenWithBlockchain,
tokensAddedOnMainScreen: List<TokenWithBlockchain>,
addedTokensState: MutableState<List<TokenWithBlockchain>>,
tokensState: TokensState,
) {
val isTryingToRemove = addedTokensState.value.contains(token)
val isAddedOnMainScreen = tokensAddedOnMainScreen.contains(token)
val isUnsupportedToken = !tokensState.canHandleToken(token)
if (isTryingToRemove) {
if (isAddedOnMainScreen) {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = token.token.name,
onOk = { addedTokensState.removeAndNotify(token) }
))
} else {
addedTokensState.removeAndNotify(token)
}
} else {
if (isUnsupportedToken) {
store.dispatchDialogShow(AppDialog.SimpleOkDialogRes(
headerId = R.string.common_warning,
messageId = R.string.alert_manage_tokens_unsupported_message,
))
} else {
addedTokensState.addAndNotify(token)
}
}
}
@Composable
fun SaveChangesButton(keyboardState: Keyboard, onSaveChanges: () -> Unit) {

View file

@ -9,8 +9,6 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@Composable
fun CurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
@ -22,8 +20,6 @@ fun CurrencyItem(
if (expanded) {
ExpandedCurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
nonRemovableBlockchains = nonRemovableBlockchains,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,

View file

@ -28,8 +28,6 @@ import com.tangem.wallet.R
@Composable
fun ExpandedCurrencyItem(
currency: Currency,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
@ -138,17 +136,12 @@ fun ExpandedCurrencyItem(
} else {
addedBlockchains.contains(blockchain)
}
val canBeRemoved = if (contract.address != null) {
!nonRemovableTokens.contains(contract.address)
} else {
!nonRemovableBlockchains.contains(blockchain)
}
NetworkItem(
currency = currency,
contract = contract,
blockchain = blockchain, allowToAdd = allowToAdd,
blockchain = blockchain,
allowToAdd = allowToAdd,
added = added,
canBeRemoved = canBeRemoved,
onAddCurrencyToggled = onAddCurrencyToggled,
onNetworkItemClicked = onNetworkItemClicked,
)

View file

@ -22,8 +22,6 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
fun ListOfCurrencies(
header: @Composable () -> Unit,
currencies: List<Currency>,
nonRemovableTokens: List<ContractAddress>,
nonRemovableBlockchains: List<Blockchain>,
addedTokens: List<TokenWithBlockchain>,
addedBlockchains: List<Blockchain>,
allowToAdd: Boolean,
@ -58,8 +56,6 @@ fun ListOfCurrencies(
itemsIndexed(currencies) { index, currency ->
CurrencyItem(
currency = currency,
nonRemovableTokens = nonRemovableTokens,
nonRemovableBlockchains = nonRemovableBlockchains,
addedTokens = addedTokens,
addedBlockchains = addedBlockchains,
allowToAdd = allowToAdd,

View file

@ -35,9 +35,11 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NetworkItem(
currency: Currency, contract: Contract,
blockchain: Blockchain, allowToAdd: Boolean,
added: Boolean, canBeRemoved: Boolean,
currency: Currency,
contract: Contract,
blockchain: Blockchain,
allowToAdd: Boolean,
added: Boolean,
onAddCurrencyToggled: (Currency, TokenWithBlockchain?) -> Unit,
onNetworkItemClicked: (ContractAddress) -> Unit
) {
@ -126,7 +128,6 @@ fun NetworkItem(
Switch(
checked = added,
enabled = canBeRemoved,
onCheckedChange = { onAddCurrencyToggled(currencyToSave, tokenWithBlockchain) },
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
colors = SwitchDefaults.colors(

View file

@ -1,8 +1,6 @@
package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.redux.global.CryptoCurrencyName

View file

@ -11,6 +11,7 @@ import com.tangem.common.card.Card
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
@ -64,7 +65,9 @@ sealed class WalletAction : Action {
MultiWallet()
data class AddToken(val token: Token, val blockchain: BlockchainNetwork) : MultiWallet()
data class SaveCurrencies(val blockchainNetworks: List<BlockchainNetwork>) : MultiWallet()
data class SaveCurrencies(
val blockchainNetworks: List<BlockchainNetwork>, val cardId: String? = null
) : MultiWallet()
// object FindTokensInUse : MultiWallet()
// object FindBlockchainsInUse : MultiWallet()
@ -75,10 +78,18 @@ sealed class WalletAction : Action {
) : MultiWallet()
data class SelectWallet(val walletData: WalletData?) : MultiWallet()
data class RemoveWallet(val walletData: WalletData, val fromWalletDetails: Boolean = true) : MultiWallet()
data class TryToRemoveWallet(val walletData: WalletData) : MultiWallet()
data class TryToRemoveWallet(val currency: Currency) : MultiWallet()
data class RemoveWallet(
val currency: Currency,
val fromScreen: AppScreen
) : MultiWallet()
data class SetPrimaryBlockchain(val blockchain: Blockchain) : MultiWallet()
data class SetPrimaryToken(val token: Token) : MultiWallet()
data class ShowWalletBackupWarning(val show: Boolean) : MultiWallet()
object BackupWallet : MultiWallet()
}
sealed class Warnings : WalletAction() {
@ -99,8 +110,6 @@ sealed class WalletAction : Action {
}
class CheckRemainingSignatures(val remainingSignatures: Int?) : Warnings()
object RestoreFundsWarningClosed : Warnings()
}
data class LoadFiatRate(
@ -147,6 +156,7 @@ sealed class WalletAction : Action {
object SignedHashesMultiWalletDialog : DialogAction()
object ChooseTradeActionDialog : DialogAction()
data class ChooseCurrency(val amounts: List<Amount>?) : DialogAction()
object RussianCardholdersWarningDialog : DialogAction()
object Hide : DialogAction()
}
@ -157,8 +167,10 @@ sealed class WalletAction : Action {
object EmptyWallet : WalletAction()
sealed class TradeCryptoAction : WalletAction() {
object Buy : TradeCryptoAction()
object Sell : TradeCryptoAction()
data class Buy(
val checkUserLocation: Boolean = true,
) : TradeCryptoAction()
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
data class SendCrypto(
val currencyId: String,

View file

@ -1,19 +1,18 @@
package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.*
import com.tangem.blockchain.common.Blockchain
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.common.extensions.isZero
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.domain.extensions.sellIsAllowed
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.*
@ -41,6 +40,7 @@ data class WalletState(
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val totalBalance: TotalBalance? = null,
val showBackupWarning: Boolean = false,
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -59,7 +59,7 @@ data class WalletState(
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
@ -304,16 +304,11 @@ data class WalletState(
)
} else this
}
}
sealed interface WalletDialog : StateDialog {
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog
object SignedHashesMultiWalletDialog : WalletDialog
object ChooseTradeActionDialog : WalletDialog
data class CurrencySelectionDialog(
val currenciesList: List<FiatCurrency>,
val currentAppCurrency: FiatCurrency,
) : WalletDialog
companion object {
const val UNKNOWN_AMOUNT_SIGN = ""
const val ROUGH_SIGN = ""
}
}
enum class ProgressState : WidgetState { Loading, Refreshing, Done, Error }
@ -358,18 +353,21 @@ data class Artwork(
}
data class TradeCryptoState(
val sellingAllowed: Boolean = false,
val buyingAllowed: Boolean = false,
val isAvailableToSell: () -> Boolean = { false },
val isAvailableToBuy: () -> Boolean = { false },
) {
companion object {
fun from(
exchangeManager: CurrencyExchangeManager?,
walletData: WalletData
): TradeCryptoState {
val status = exchangeManager ?: return walletData.tradeCryptoState
val exchanger = exchangeManager ?: return walletData.tradeCryptoState
val currency = walletData.currency
return TradeCryptoState(status.sellIsAllowed(currency), status.buyIsAllowed(currency))
return TradeCryptoState(
isAvailableToSell = { exchanger.availableForSell(currency) },
isAvailableToBuy = { exchanger.availableForBuy(currency) },
)
}
}
}

View file

@ -5,11 +5,14 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
@ -30,7 +33,6 @@ class MultiWalletMiddleware {
action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?,
) {
val globalState = globalState ?: return
// val tapWalletManager = globalState.tapWalletManager
when (action) {
is WalletAction.MultiWallet.AddBlockchains -> {
@ -75,71 +77,75 @@ class MultiWalletMiddleware {
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
globalState.scanResponse?.card?.cardId?.let {
currenciesRepository.saveCurrencies(it, action.blockchainNetworks)
}
val cardId = action.cardId ?: globalState.scanResponse?.card?.cardId ?: return
currenciesRepository.saveCurrencies(cardId, action.blockchainNetworks)
}
is WalletAction.MultiWallet.TryToRemoveWallet -> {
val walletState = store.state.walletState
val currency = action.walletData.currency
val walletCanBeRemoved = store.state.walletState.canBeRemoved(
store.state.walletState.getSelectedWalletData()
)
if (walletCanBeRemoved) {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(action.walletData))
val currency = action.currency
val walletManager = walletState?.getWalletManager(currency).guard {
store.dispatchErrorNotification(TapError.UnsupportedState("walletManager is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
val walletManager = walletState.getWalletManager(currency).guard {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(action.walletData))
return
if (currency.isBlockchain() && walletManager.cardTokens.isNotEmpty()) {
store.dispatchDialogShow(WalletDialog.TokensAreLinkedDialog(
currencyTitle = currency.currencyName,
currencySymbol = currency.currencySymbol
))
} else {
store.dispatchDialogShow(WalletDialog.RemoveWalletDialog(
currencyTitle = currency.currencyName,
onOk = {
store.dispatch(WalletAction.MultiWallet.RemoveWallet(
currency = currency,
fromScreen = AppScreen.WalletDetails
))
store.dispatch(NavigationAction.PopBackTo())
}
))
}
val dialog = when {
currency is Currency.Blockchain &&
walletManager.cardTokens.isNotEmpty() ->
WalletDialog.TokensAreLinkedDialog(
currency.currencyName, currency.currencySymbol
)
else ->
WalletDialog.RemoveWalletDialog(currency.currencyName, action.walletData)
}
store.dispatchDialogShow(dialog)
}
is WalletAction.MultiWallet.RemoveWallet -> {
val cardId = globalState.scanResponse?.card?.cardId
when (val currency = action.walletData.currency) {
val currency = action.currency
val cardId = globalState.scanResponse?.card?.cardId.guard {
store.dispatchErrorNotification(TapError.UnsupportedState("cardId is NULL"))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
return
}
when (currency) {
is Currency.Blockchain -> {
cardId?.let {
currenciesRepository.removeBlockchain(
cardId = it,
blockchainNetwork = BlockchainNetwork(
currency.blockchain, currency.derivationPath,
emptyList()
)
currenciesRepository.removeBlockchain(
cardId = cardId,
blockchainNetwork = BlockchainNetwork(
blockchain = currency.blockchain,
derivationPath = currency.derivationPath,
tokens = emptyList()
)
}
)
}
is Currency.Token -> {
val walletManager = walletState?.getWalletManager(currency)
if (walletManager != null) {
walletManager.removeToken(currency.token)
cardId?.let {
currenciesRepository.removeToken(
cardId = it,
token = currency.token,
blockchainNetwork = BlockchainNetwork.fromWalletManager(
walletManager
)
)
}
currenciesRepository.removeToken(
cardId = cardId,
token = currency.token,
blockchainNetwork = BlockchainNetwork.fromWalletManager(walletManager)
)
}
}
}
if (action.fromWalletDetails) {
if (action.fromScreen == AppScreen.AddTokens) {
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
store.dispatch(NavigationAction.PopBackTo())
}
}
is WalletAction.MultiWallet.ShowWalletBackupWarning -> Unit
is WalletAction.MultiWallet.BackupWallet -> {
store.state.globalState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false))
}
}
// is WalletAction.MultiWallet.FindBlockchainsInUse -> {

View file

@ -8,12 +8,13 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.buyErc20Tokens
import com.tangem.tap.network.exchangeServices.buyErc20TestnetTokens
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
@ -24,51 +25,70 @@ class TradeCryptoMiddleware {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is WalletAction.TradeCryptoAction.Buy -> startExchange(action)
is WalletAction.TradeCryptoAction.Sell -> startExchange(action)
is WalletAction.TradeCryptoAction.Buy -> proceedBuyAction(state, action)
is WalletAction.TradeCryptoAction.Sell -> proceedSellAction()
is WalletAction.TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
is WalletAction.TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
}
}
private fun startExchange(action: WalletAction.TradeCryptoAction) {
val selectedWalletData = store.state.walletState.getSelectedWalletData()
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
val addresses = selectedWalletData?.walletAddresses ?: return
if (addresses.list.isEmpty()) return
val appCurrency = store.state.globalState.appCurrency
val defaultAddress = addresses.list[0].address
val currency = selectedWalletData.currency
val currencySymbol = selectedWalletData.currency.currencySymbol
val exchangeAction = if (action is WalletAction.TradeCryptoAction.Buy) {
CurrencyExchangeManager.Action.Buy
} else {
CurrencyExchangeManager.Action.Sell
private fun proceedBuyAction(
state: () -> AppState?,
action: WalletAction.TradeCryptoAction.Buy,
) {
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(
WalletAction.DialogAction.RussianCardholdersWarningDialog
)
return
}
if (exchangeAction == CurrencyExchangeManager.Action.Buy &&
currency is Currency.Token && currency.blockchain.isTestnet()
) {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
val walletManager = store.state.walletState.getWalletManager(currency)
if (walletManager !is EthereumWalletManager) {
store.dispatchDebugErrorNotification("Testnet tokens available only for the ETH")
store.dispatchDebugErrorNotification("Testnet tokens available only for the Ethereum")
return
}
scope.launch { exchangeManager.buyErc20Tokens(walletManager, currency.token) }
scope.launch { exchangeManager.buyErc20TestnetTokens(walletManager, currency.token) }
return
}
exchangeManager.getUrl(
action = exchangeAction,
action = CurrencyExchangeManager.Action.Buy,
blockchain = currency.blockchain,
cryptoCurrencyName = currencySymbol,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = defaultAddress
walletAddress = addresses[0].address
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
}
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
fiatCurrencyName = appCurrency.code,
walletAddress = addresses[0].address
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
}
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
@ -89,7 +109,7 @@ class TradeCryptoMiddleware {
}
private fun openReceiptUrl(transactionId: String) {
val exchangeManager = store.state.globalState.currencyExchangeManager ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
store.dispatchOnMain(NavigationAction.PopBackTo())
exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let {

View file

@ -27,10 +27,13 @@ class WalletDialogsMiddleware {
is WalletAction.DialogAction.ChooseCurrency -> {
store.dispatchDialogShow(
WalletDialog.SelectAmountToSendDialog(
amounts = action.amounts
amounts = action.amounts
)
)
}
is WalletAction.DialogAction.RussianCardholdersWarningDialog -> {
store.dispatchDialogShow(WalletDialog.RussianCardholdersWarningDialog)
}
is WalletAction.DialogAction.Hide -> {
store.dispatchDialogHide()
}

View file

@ -8,7 +8,6 @@ import com.tangem.common.card.Card
import com.tangem.common.card.FirmwareVersion
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.isGreaterThan
@ -26,15 +25,15 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.wallet.R
import java.math.BigDecimal
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
class WarningsMiddleware {
fun handle(action: WalletAction.Warnings, globalState: GlobalState?) {
when (action) {
WalletAction.Warnings.Update -> setWarningMessages()
is WalletAction.Warnings.Update -> setWarningMessages()
is WalletAction.Warnings.CheckIfNeeded -> {
showCardWarningsIfNeeded(globalState)
val readyToShow = preferencesStorage.appRatingLaunchObserver.isReadyToShow()
@ -66,9 +65,11 @@ class WarningsMiddleware {
)
}
}
is WalletAction.Warnings.RestoreFundsWarningClosed -> {
preferencesStorage.saveRestoreFundsWarningClosed()
}
is WalletAction.Warnings.AppRating,
is WalletAction.Warnings.CheckHashesCount,
is WalletAction.Warnings.CheckHashesCount.ConfirmHashesCount,
is WalletAction.Warnings.CheckHashesCount.NeedToCheckHashesCountOnline,
is WalletAction.Warnings.Set -> Unit
}
}
@ -93,9 +94,6 @@ class WarningsMiddleware {
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
return@let
}
if (card.useOldStyleDerivation && !preferencesStorage.wasRestoreFundsWarningClosed()) {
addWarningMessage(warning = WarningMessagesManager.restoreFundsWarning())
}
showWarningLowRemainingSignaturesIfNeeded(card)
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
@ -190,6 +188,7 @@ class WarningsMiddleware {
true
)
}
null -> Unit
}
}
}

View file

@ -3,9 +3,6 @@ package com.tangem.tap.features.wallet.redux.models
import com.tangem.blockchain.common.Amount
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.store
import com.tangem.wallet.R
sealed interface WalletDialog : StateDialog {
@ -19,21 +16,20 @@ sealed interface WalletDialog : StateDialog {
data class RemoveWalletDialog(
val currencyTitle: String,
private val walletData: WalletData
): WalletDialog {
val onOk: () -> Unit
) : WalletDialog {
val messageRes: Int = R.string.token_details_hide_alert_message
val titleRes: Int = R.string.token_details_hide_alert_title
val primaryButtonRes: Int = R.string.token_details_hide_alert_hide
val action = { store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData)) }
}
data class TokensAreLinkedDialog(
val currencyTitle: String,
val currencySymbol: String
): WalletDialog {
) : WalletDialog {
val messageRes: Int = R.string.token_details_unable_hide_alert_message
val titleRes: Int = R.string.token_details_unable_hide_alert_title
}
object RussianCardholdersWarningDialog : WalletDialog
}

View file

@ -7,12 +7,9 @@ import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -127,9 +124,8 @@ class MultiWalletReducer {
action.amount.decimals, action.amount.currencySymbol
),
fiatAmountFormatted = tokenWalletData.fiatRate?.let {
action.amount.value
?.toFiatString(it, store.state.globalState.appCurrency.symbol)
},
action.amount.value?.toFiatString(it, store.state.globalState.appCurrency.symbol)
} ?: UNKNOWN_AMOUNT_SIGN,
blockchainAmount = wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
@ -148,19 +144,20 @@ class MultiWalletReducer {
is WalletAction.MultiWallet.SelectWallet ->
state.copy(selectedCurrency = action.walletData?.currency)
is WalletAction.MultiWallet.TryToRemoveWallet -> state
is WalletAction.MultiWallet.RemoveWallet -> {
state.removeWallet(action.walletData)
state.removeWallet(state.getWalletData(action.currency))
}
is WalletAction.MultiWallet.SetPrimaryBlockchain ->
state.copy(primaryBlockchain = action.blockchain)
is WalletAction.MultiWallet.SetPrimaryToken ->
state.copy(primaryToken = action.token)
// is WalletAction.MultiWallet.FindTokensInUse -> state
// is WalletAction.MultiWallet.FindBlockchainsInUse -> state
is WalletAction.MultiWallet.SaveCurrencies -> state
is WalletAction.MultiWallet.TryToRemoveWallet -> state
is WalletAction.MultiWallet.ShowWalletBackupWarning -> state.copy(
showBackupWarning = action.show
)
is WalletAction.MultiWallet.BackupWallet -> state
}
}

View file

@ -14,11 +14,11 @@ import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
import com.tangem.tap.store
import java.math.RoundingMode
class OnWalletLoadedReducer {
@ -38,7 +38,7 @@ class OnWalletLoadedReducer {
val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState
val fiatCurrency = store.state.globalState.appCurrency
val exchangeManager = store.state.globalState.currencyExchangeManager
val exchangeManager = store.state.globalState.exchangeManager
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
@ -55,6 +55,8 @@ class OnWalletLoadedReducer {
}
val fiatAmount = walletData.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol) ?: UNKNOWN_AMOUNT_SIGN
val newWalletData = walletData.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus,
@ -64,7 +66,7 @@ class OnWalletLoadedReducer {
amount = coinAmountValue,
amountFormatted = formattedAmount,
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
fiatAmountFormatted = fiatAmountFormatted,
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled),
@ -82,8 +84,9 @@ class OnWalletLoadedReducer {
else -> BalanceStatus.VerifiedOnline
}
val tokenAmountValue = wallet.getTokenAmount(token)?.value
val tokenFiatAmount =
tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate) }
val tokenFiatAmount = tokenWalletData?.fiatRate?.let { tokenAmountValue?.toFiatValue(it) }
val tokenFiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
?: UNKNOWN_AMOUNT_SIGN
val isTokenSendButtonEnabled = newWalletData.shouldEnableTokenSendButton()
&& pendingTransactions.isEmpty()
@ -97,7 +100,7 @@ class OnWalletLoadedReducer {
token.symbol
),
fiatAmount = tokenFiatAmount,
fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrency.symbol)
fiatAmountFormatted = tokenFiatAmountFormatted,
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
@ -115,7 +118,7 @@ class OnWalletLoadedReducer {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencyName = store.state.globalState.appCurrency.code
val exchangeManager = store.state.globalState.currencyExchangeManager
val exchangeManager = store.state.globalState.exchangeManager
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
@ -141,9 +144,8 @@ class OnWalletLoadedReducer {
wallet.blockchain.decimals(),
wallet.blockchain.currency
)
val fiatRate = walletState.primaryWallet?.fiatRate
val fiatAmountRaw = fiatRate?.multiply(amount)?.setScale(2, RoundingMode.DOWN)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencyName) }
val fiatAmount = walletState.primaryWallet?.fiatRate?.let { amount?.toFiatValue(it) }
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencyName) ?: UNKNOWN_AMOUNT_SIGN
val pendingTransactions = wallet.getPendingTransactions()
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
@ -160,8 +162,8 @@ class OnWalletLoadedReducer {
blockchainAmount = amount,
amount = amount,
amountFormatted = formattedAmount,
fiatAmountFormatted = fiatAmount,
fiatAmount = fiatAmountRaw
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmountFormatted
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),

View file

@ -6,17 +6,14 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.models.Currency
@ -32,8 +29,8 @@ import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import java.math.BigDecimal
import org.rekotlin.Action
import java.math.BigDecimal
class WalletReducer {
companion object {
@ -49,7 +46,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
if (action !is WalletAction) return state.walletState
val exchangeManager = store.state.globalState.currencyExchangeManager
val exchangeManager = store.state.globalState.exchangeManager
var newState = state.walletState
when (action) {
@ -217,8 +214,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currency = walletBlockchain.currency,
),
fiatAmount = fiatAmount,
fiatAmountFormatted = fiatAmount.toFiatRateString(
fiatCurrencyName = store.state.globalState.appCurrency.symbol
fiatAmountFormatted = fiatAmount.toFormattedFiatValue(
fiatCurrencyName = state.globalState.appCurrency.symbol
),
amountToCreateAccount = action.amountToCreateAccount,
)
@ -439,7 +436,7 @@ private fun setMultiWalletFiatRate(
wallet?.getTokenAmount(currency.token)?.value?.toFiatValue(rate)
}
if (currencyData.status == BalanceStatus.NoAccount && fiatAmount == null) {
fiatAmount = BigDecimal.ZERO
fiatAmount = BigDecimal.ZERO.setScale(2)
}
val fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(appCurrency.symbol)

View file

@ -26,8 +26,8 @@ data class BalanceWidgetData(
val blockchainAmount: BigDecimal? = BigDecimal.ZERO,
val amount: BigDecimal? = null,
val amountFormatted: String? = null,
val fiatAmountFormatted: String? = null,
val fiatAmount: BigDecimal? = null,
val fiatAmountFormatted: String? = null,
val token: TokenData? = null,
val amountToCreateAccount: String? = null,
val errorMessage: String? = null

View file

@ -11,7 +11,7 @@ import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import com.tangem.tangem_sdk_new.extensions.dpToPx
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.extensions.*
@ -22,8 +22,10 @@ import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.images.loadCurrencyIcon
import com.tangem.tap.features.wallet.ui.test.TestWalletDetails
import com.tangem.tap.store
import com.tangem.wallet.R
@ -90,7 +92,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
private fun setupButtons() = with(binding) {
rowButtons.onBuyClick = {
store.dispatch(WalletAction.TradeCryptoAction.Buy)
store.dispatch(WalletAction.TradeCryptoAction.Buy())
}
rowButtons.onSellClick = {
store.dispatch(WalletAction.TradeCryptoAction.Sell)
@ -127,18 +129,18 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
handleCurrencyIcon(selectedWallet)
handleWarnings(selectedWallet)
updateViewMeasurements()
binding.srlWalletDetails.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(
WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList()
)
store.dispatch(WalletAction.LoadWallet(
blockchain = BlockchainNetwork(
selectedWallet.currency.blockchain,
selectedWallet.currency.derivationPath,
emptyList()
)
)
)
}
}
@ -147,14 +149,26 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
}
private fun updateViewMeasurements() {
val tvFiatAmount = binding.lWalletDetails.lBalance.tvFiatAmount
val paddingStart = when (tvFiatAmount.text) {
UNKNOWN_AMOUNT_SIGN -> 16f
else -> 12f
}
tvFiatAmount.setPadding(
tvFiatAmount.dpToPx(paddingStart).toInt(),
tvFiatAmount.paddingTop,
tvFiatAmount.paddingEnd,
tvFiatAmount.paddingBottom,
)
}
private fun setupCurrency(currencyData: BalanceWidgetData, currency: Currency) = with(binding) {
tvCurrencyTitle.text = currencyData.currency
if (currency is Currency.Token) {
binding.tvCurrencySubtitle.text = currency.blockchain.tokenDisplayName()
binding.tvCurrencySubtitle.show()
} else {
binding.tvCurrencySubtitle.hide()
}
tvCurrencySubtitle.text = tvCurrencySubtitle.getString(
R.string.wallet_currency_subtitle,
currency.blockchain.fullName
)
}
private fun setupButtons(selectedWallet: WalletData) = with(binding) {
@ -170,8 +184,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
rowButtons.updateButtonsVisibility(
buyAllowed = selectedWallet.tradeCryptoState.buyingAllowed,
sellAllowed = selectedWallet.tradeCryptoState.sellingAllowed,
buyAllowed = selectedWallet.tradeCryptoState.isAvailableToBuy(),
sellAllowed = selectedWallet.tradeCryptoState.isAvailableToSell(),
sendAllowed = selectedWallet.mainButton.enabled,
)
}
@ -185,9 +199,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
private fun handleCurrencyIcon(wallet: WalletData) = with(binding.lWalletDetails.lBalance) {
Picasso.get().loadCurrenciesIcon(
imageView = ivCurrency,
textView = tvTokenLetter,
loadCurrencyIcon(
currencyImageView = ivCurrency,
currencyTextView = tvTokenLetter,
blockchain = wallet.currency.blockchain,
token = (wallet.currency as? Currency.Token)?.token
)
@ -303,7 +317,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
return when (item.itemId) {
R.id.menu_remove -> {
store.state.walletState.getSelectedWalletData()?.let { walletData ->
store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData))
store.dispatch(WalletAction.MultiWallet.TryToRemoveWallet(walletData.currency))
true
}
false

View file

@ -12,7 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.squareup.picasso.Picasso
import coil.load
import com.tangem.tap.MainActivity
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
@ -165,11 +165,11 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
private fun setupCardImage(cardImage: Artwork?) {
Picasso.get()
.load(cardImage?.artworkId)
.placeholder(R.drawable.card_placeholder_black)
?.error(R.drawable.card_placeholder_black)
?.into(binding.ivCard)
binding.ivCard.load(cardImage?.artworkId) {
placeholder(R.drawable.card_placeholder_black)
error(R.drawable.card_placeholder_black)
fallback(R.drawable.card_placeholder_black)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View file

@ -61,10 +61,11 @@ class PendingTransactionsAdapter
PendingTransactionType.Outgoing -> R.drawable.ic_arrow_right_20
PendingTransactionType.Unknown -> return
}
binding.tvPendingTransaction.text = binding.root.getString(transactionDescriptionRes)
binding.tvPendingTransaction.text =
binding.root.getString(transactionDescriptionRes).let { "$it " }
transaction.amountValueUi?.let { binding.tvPendingTransactionAmount.text = "$it " }
binding.tvPendingTransactionCurrency.text = "${transaction.currency}"
binding.tvPendingTransactionCurrency.text = transaction.currency
if (transaction.address != null) {
binding.tvPendingTransactionAddress.text =

View file

@ -6,18 +6,17 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.images.loadCurrencyIcon
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.ItemCurrencyWalletBinding
@ -98,16 +97,16 @@ class WalletAdapter
lContent.root.show()
}
Picasso.get().loadCurrenciesIcon(
imageView = ivCurrency,
textView = tvTokenLetter,
loadCurrencyIcon(
currencyImageView = ivCurrency,
currencyTextView = tvTokenLetter,
token = (wallet.currency as? Currency.Token)?.token,
blockchain = wallet.currency.blockchain,
)
lContent.tvCurrency.text = wallet.currencyData.currency
lContent.tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted ?: ""
lContent.tvAmount.text = wallet.currencyData.amountFormatted ?: ""
lContent.tvAmountFiat.text = wallet.currencyData.fiatAmountFormatted
lContent.tvAmount.text = wallet.currencyData.amountFormatted
lContent.tvStatus.isVisible = statusMessage != null
lContent.tvStatus.text = statusMessage

View file

@ -1,25 +1,21 @@
package com.tangem.tap.features.wallet.ui.adapters
import android.content.res.Resources
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.ConfigurationCompat
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.play.core.review.ReviewManagerFactory
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getActivity
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.feedback.RateCanBeBetterEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.feedback.RateCanBeBetterEmail
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -89,26 +85,12 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
binding.btnClose.hide()
val buttonAction =
when {
warning.titleResId == R.string.warning_important_security_info -> {
when (warning.titleResId) {
R.string.warning_important_security_info -> {
View.OnClickListener {
store.dispatch(WalletAction.DialogAction.SignedHashesMultiWalletDialog)
}
}
warning.messageResId == R.string.alert_funds_restoration_message -> {
binding.btnClose.show()
binding.btnClose.setOnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(WalletAction.Warnings.RestoreFundsWarningClosed)
}
val locale = ConfigurationCompat
.getLocales(Resources.getSystem().configuration)
.get(0)
val url = WarningMessagesManager.getRestoreFundsGuideUrl(locale.language)
View.OnClickListener {
store.dispatchOpenUrl(url)
}
}
else -> {
View.OnClickListener {
store.dispatch(GlobalAction.HideWarningMessage(warning))
@ -135,7 +117,7 @@ class WarningMessageVH(val binding: LayoutWarningCardActionBinding) : RecyclerVi
analyticsHandler?.triggerEvent(AnalyticsEvent.APP_RATING_NEGATIVE)
store.dispatch(WalletAction.Warnings.AppRating.SetNeverToShow)
store.dispatch(GlobalAction.HideWarningMessage(warning))
store.dispatch(GlobalAction.SendFeedback(RateCanBeBetterEmail()))
store.dispatch(GlobalAction.SendEmail(RateCanBeBetterEmail()))
}
binding.btnReallyCool.setOnClickListener {
val activity = binding.root.context.getActivity() ?: return@setOnClickListener

View file

@ -27,12 +27,12 @@ class ChooseTradeActionBottomSheetDialog(context: Context) : BottomSheetDialog(c
}
binding!!.dialogBtnBuy.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy)
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Buy())
}
binding!!.dialogBtnSell.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Sell)
dismiss()
store.dispatch(WalletAction.TradeCryptoAction.Sell)
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.databinding.DialogRussiansCardholdersWarningBinding
class RussianCardholdersWarningBottomSheetDialog(context: Context) : BottomSheetDialog(context) {
private var binding: DialogRussiansCardholdersWarningBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogRussiansCardholdersWarningBinding
.inflate(LayoutInflater.from(context))
.also { setContentView(it.root) }
}
override fun show() {
super.show()
setOnDismissListener {
binding = null
store.dispatchDialogHide()
}
binding?.btnYes?.setOnClickListener {
store.dispatch(WalletAction.TradeCryptoAction.Buy(checkUserLocation = false))
dismiss()
}
binding?.btnNo?.setOnClickListener {
store.dispatch(NavigationAction.OpenUrl(INSTRUCTION_URL))
dismiss()
}
}
companion object {
private const val INSTRUCTION_URL = "https://tangem.com/howtobuy.html"
}
}

View file

@ -3,8 +3,8 @@ package com.tangem.tap.features.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.feedback.ScanFailsEmail
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.ScanFailsEmail
import com.tangem.tap.store
import com.tangem.wallet.R
@ -19,7 +19,7 @@ class ScanFailsDialog {
setTitle(context.getString(R.string.common_warning))
setMessage(R.string.alert_troubleshooting_scan_card_title)
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail()))
store.dispatch(GlobalAction.SendEmail(ScanFailsEmail()))
}
setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }

View file

@ -0,0 +1,167 @@
package com.tangem.tap.features.wallet.ui.images
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.utils.widget.ImageFilterView
import coil.imageLoader
import coil.request.ImageRequest
import coil.transform.RoundedCornersTransformation
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.IconsUtil
import com.tangem.blockchain.common.Token
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.tap.common.extensions.getColor
import com.tangem.tap.common.extensions.getRoundIconRes
import com.tangem.tap.common.extensions.getTextColor
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.tokens.getIconUrl
import com.tangem.wallet.R
private const val QCX = "QCX"
private const val VOYR = "VOYRME"
fun loadCurrencyIcon(
currencyImageView: ImageFilterView,
currencyTextView: TextView,
token: Token?,
blockchain: Blockchain,
) {
CurrencyIconLoader(
currencyImageView = currencyImageView,
currencyTextView = currencyTextView,
token = token,
blockchain = blockchain
)
.load()
}
private class CurrencyIconLoader(
private val currencyImageView: ImageFilterView,
private val currencyTextView: TextView,
private val token: Token?,
private val blockchain: Blockchain,
) {
fun load() {
when {
token == null && blockchain.isTestnet() -> loadTestnetBlockchainIcon()
token == null -> loadBlockchainIcon()
blockchain.isTestnet() -> loadTestnetTokenIcon()
else -> loadTokenIcon()
}
}
private fun loadBlockchainIcon() {
loadBlockchainIconBase(
onStart = {
currencyImageView.colorFilter = null
}
)
}
private fun loadTestnetBlockchainIcon() {
loadBlockchainIconBase(
onStart = {
currencyImageView.saturation = 0f
}
)
}
private fun loadTokenIcon() {
loadTokenIconBase(
onStart = {
currencyImageView.setColorFilter(it.getColor())
},
onSuccess = {
currencyImageView.colorFilter = null
}
)
}
private fun loadTestnetTokenIcon() {
loadTokenIconBase(
onStart = {
currencyImageView.saturation = 0f
}
)
}
private inline fun loadBlockchainIconBase(
crossinline onStart: (Blockchain) -> Unit = {},
crossinline onSuccess: (Blockchain) -> Unit = {},
crossinline onError: (Blockchain) -> Unit = {},
) {
currencyImageView.loadIcon(
data = getIconUrl(blockchain.toNetworkId()),
placeholderRes = blockchain.getRoundIconRes(),
onStart = { onStart(blockchain) },
onSuccess = { onSuccess(blockchain) },
onError = { onError(blockchain) },
)
}
private inline fun loadTokenIconBase(
crossinline onStart: (Token) -> Unit = {},
crossinline onSuccess: (Token) -> Unit = {},
crossinline onError: (Token) -> Unit = {},
) {
if (token == null) return
currencyImageView.loadIcon(
data = getTokenIcon(token, blockchain),
placeholderRes = R.drawable.shape_circle,
onStart = {
currencyTextView.text = token.symbol.take(1)
currencyTextView.setTextColor(token.getTextColor())
onStart(token)
},
onSuccess = {
currencyTextView.text = null
onSuccess(token)
},
onError = { onError(token) },
)
}
}
private inline fun ImageView.loadIcon(
data: Any?,
placeholderRes: Int,
crossinline onStart: () -> Unit = {},
crossinline onSuccess: () -> Unit = {},
crossinline onError: () -> Unit = {},
) {
ImageRequest.Builder(context)
.data(data)
.placeholder(placeholderRes)
.error(placeholderRes)
.fallback(placeholderRes)
.transformations(
RoundedCornersTransformation(
topLeft = 32f,
topRight = 32f,
bottomLeft = 32f,
bottomRight = 32f
)
)
.listener(
onStart = { onStart() },
onSuccess = { _, _ -> onSuccess() },
onError = { _, _ -> onError() }
)
.target(imageView = this)
.build()
.also(context.imageLoader::enqueue)
}
private fun getTokenIcon(token: Token, blockchain: Blockchain): Any? {
return when (token.symbol) {
QCX -> R.drawable.ic_qcx
VOYR -> R.drawable.ic_voyr
else -> {
token.id?.let(::getIconUrl)
?: token.getCustomIconUrl()
?: IconsUtil.getTokenIconUri(blockchain, token)
?.toString()
}
}
}

View file

@ -91,6 +91,7 @@ class MultiWalletView : WalletView {
val binding = binding ?: return
handleTotalBalance(binding, state.totalBalance)
handleBackupWarning(binding, state.showBackupWarning)
walletsAdapter.submitList(state.walletsData, state.primaryBlockchain, state.primaryToken)
binding.btnAddToken.setOnClickListener {
@ -111,15 +112,21 @@ class MultiWalletView : WalletView {
derivationStyle = card.derivationStyle
)
)
store.dispatch(
TokensAction.SetNonRemovableCurrencies(
state.walletsData.filterNot { state.canBeRemoved(it) })
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
handleErrorStates(state = state, binding = binding, fragment = fragment)
}
private fun handleBackupWarning(
binding: FragmentWalletBinding,
showBackupWarning: Boolean
) = with(binding.lWalletBackupWarning) {
root.isVisible = showBackupWarning
root.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.BackupWallet)
}
}
private fun handleTotalBalance(
binding: FragmentWalletBinding,
totalBalance: TotalBalance?,

View file

@ -118,9 +118,8 @@ class SingleWalletView : WalletView {
setupButtonsType(state, binding)
val btnConfirm = if (state.tradeCryptoState.sellingAllowed ||
state.tradeCryptoState.buyingAllowed
) {
val tradeState = state.tradeCryptoState
val btnConfirm = if (tradeState.isAvailableToSell() || tradeState.isAvailableToBuy()) {
lButtonsShort.btnConfirm
} else {
lButtonsLong.btnConfirmLong
@ -148,10 +147,10 @@ class SingleWalletView : WalletView {
}
private fun setupTradeButton(binding: FragmentWalletBinding, tradeCryptoState: TradeCryptoState) {
val allowedToBuy = tradeCryptoState.buyingAllowed
val allowedToSell = tradeCryptoState.sellingAllowed
val allowedToBuy = tradeCryptoState.isAvailableToBuy()
val allowedToSell = tradeCryptoState.isAvailableToSell()
val action = when {
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy()
!allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell
allowedToBuy && allowedToSell -> WalletAction.DialogAction.ChooseTradeActionDialog
else -> null
@ -176,9 +175,7 @@ class SingleWalletView : WalletView {
}
private fun setupButtonsType(state: WalletData, binding: FragmentWalletBinding) = with(binding) {
if (state.tradeCryptoState.sellingAllowed ||
state.tradeCryptoState.buyingAllowed
) {
if (state.tradeCryptoState.isAvailableToSell() || state.tradeCryptoState.isAvailableToBuy()) {
lButtonsLong.root.hide()
lButtonsShort.root.show()
} else {

Some files were not shown because too many files have changed in this diff Show more