Updated on 2026-08-14
This commit is contained in:
commit
9ba18ee14f
157 changed files with 3478 additions and 2220 deletions
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal file
108
app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = ""))
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
50
app/src/main/java/com/tangem/tap/common/images/Coil.kt
Normal file
50
app/src/main/java/com/tangem/tap/common/images/Coil.kt
Normal 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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = ""))
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) =
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue