diff --git a/app/build.gradle b/app/build.gradle index 48f34abcb7..3f12b8e345 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,10 +93,9 @@ dependencies { implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:develop-88' -// implementation 'com.tangem:blockchain:0.0.1' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154' + implementation "com.tangem:blockchain:${versions.tamgem_blockchain_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tamgem_card_sdk}" // WebView implementation "androidx.browser:browser:1.3.0" @@ -118,8 +117,9 @@ dependencies { // Camera implementation 'com.otaliastudios:cameraview:2.6.3' - // Image Loading - Picasso - implementation 'com.squareup.picasso:picasso:2.71828' + // Image Loading + implementation 'io.coil-kt:coil:2.1.0' + implementation 'io.coil-kt:coil-compose:2.1.0' // Firebase implementation platform('com.google.firebase:firebase-bom:26.0.0') @@ -144,8 +144,11 @@ dependencies { implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.okhttp3:logging-interceptor") -// -// //Crypto + // Support + implementation("com.zendesk:support:5.0.9") + implementation("com.zendesk:messaging:5.2.4") + + //Crypto implementation "com.madgag.spongycastle:core:1.58.0.0" // animation @@ -172,9 +175,6 @@ dependencies { implementation "com.github.skydoves:androidveil:1.1.2" - implementation("io.coil-kt:coil:2.0.0-rc01") - implementation("io.coil-kt:coil-compose:2.0.0-rc01") - implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6' testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4dfca87b23..d180642a75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,14 +28,15 @@ @@ -127,6 +128,21 @@ + + + + + + + + + + - \ No newline at end of file + diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 64ae04d208..59ee0bff06 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 64ae04d2086e5a605dd4da3cba4af32a60d46c79 +Subproject commit 59ee0bff06ee54f3202421c28f7fb6e3d421dad8 diff --git a/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt new file mode 100644 index 0000000000..806769df5f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/ForegroundActivityObserver.kt @@ -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, 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) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index a663e66e7a..7593f303e7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt index b58485ab88..518ad410c4 100644 --- a/app/src/main/java/com/tangem/tap/TapApplication.kt +++ b/app/src/main/java/com/tangem/tap/TapApplication.kt @@ -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, - ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index fda1f1a294..511c10d5dc 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -135,8 +135,10 @@ class DialogManager : StoreSubscriber { 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() diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt b/app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt similarity index 93% rename from app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt rename to app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt index 2bb6c5b8d5..324fd3877e 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/AutoSizeText.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/AutoSizeText.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt index b31a15c4ae..ce0b190598 100644 --- a/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt +++ b/app/src/main/java/com/tangem/tap/common/compose/Spacer.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt new file mode 100644 index 0000000000..1952c6a058 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/AnimatedValue.kt @@ -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 + +@Composable +fun AnimatedValue.toAnimatable( + isPaused: Boolean, + duration: Int, + easing: Easing = LinearEasing, +): Animatable { + return animatable( + values = this, + isPaused = isPaused, + duration = duration, + easing = easing, + ) +} + +@Composable +fun animatable( + values: AnimatedValue, + duration: Int, + isPaused: Boolean = false, + easing: Easing = LinearEasing, +): Animatable { + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt new file mode 100644 index 0000000000..42acdcf306 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Dp.kt @@ -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 \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt new file mode 100644 index 0000000000..1692b4c51f --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/ImageBitmap.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt new file mode 100644 index 0000000000..459086b9c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/MutableState.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.common.compose.extensions + +import androidx.compose.runtime.MutableState + +/** +[REDACTED_AUTHOR] + */ +fun MutableState>.addAndNotify(value: T) { + this.value = this.value.toMutableList().apply { add(value) } +} + +fun MutableState>.removeAndNotify(value: T) { + this.value = this.value.toMutableList().apply { remove(value) } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt new file mode 100644 index 0000000000..fa91934a30 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/compose/extensions/Painter.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt index 5cf9b2d3ad..3cb5aa96c8 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt @@ -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 -> "" } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt b/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt deleted file mode 100644 index dbeefadbe8..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Picasso.kt +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt index b431b780ad..219e9339da 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Specific.kt @@ -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" } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Token.kt b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt index fdfb83ef77..10897bc069 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Token.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Token.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt index 38b9d9fa48..617f69c36d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/UI.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/UI.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt index d671b60a29..40acdd1b7d 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/WalletManager.kt @@ -49,11 +49,11 @@ suspend fun WalletManager.safeUpdate(): Result = 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, diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt new file mode 100644 index 0000000000..ddc7a41b23 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt @@ -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() + internal val tokens = mutableMapOf>() + 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) { + 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" + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt new file mode 100644 index 0000000000..dc2a7d07d5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackData.kt @@ -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() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt new file mode 100644 index 0000000000..9e5378e9e5 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackDataBuilder.kt @@ -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 = "")) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt new file mode 100644 index 0000000000..dccb805723 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt @@ -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 { + 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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt new file mode 100644 index 0000000000..64b7b76734 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -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) } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/images/Picasso.kt b/app/src/main/java/com/tangem/tap/common/images/Picasso.kt deleted file mode 100644 index 81c52ca693..0000000000 --- a/app/src/main/java/com/tangem/tap/common/images/Picasso.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt new file mode 100644 index 0000000000..c1ae08ebc2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/log/TangemLogCollector.kt @@ -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() + 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 = synchronized(mutex) { logs.toList() } + + fun clearLogs() { + synchronized(mutex) { logs.clear() } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index b871b7853f..4927405ccd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -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) : 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() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 1e104b5dd4..17cb291621 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -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 -> +private val globalMiddlewareHandler: Middleware = { 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() + ) + ) + } + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 67e54a6042..449d47ae10 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -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 } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 75504f3ef2..62e5217866 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt new file mode 100644 index 0000000000..aad872b7df --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/zendesk/ZendeskConfig.kt @@ -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, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt index 0c38cd5a46..ff19d10171 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapErrors.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapErrors.kt @@ -39,21 +39,26 @@ sealed class TapError( object DustChange : TapError(R.string.send_error_dust_change) data class CreateAccountUnderfunded(override val args: List) : 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, - override val builder: (List) -> String + override val errorList: List, + override val builder: (List) -> String ) : TapError(-1), MultiMessageError } diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 123c26f691..92fc72c0bf 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -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) } } diff --git a/app/src/main/java/com/tangem/tap/domain/UrlBitmapLoader.kt b/app/src/main/java/com/tangem/tap/domain/UrlBitmapLoader.kt deleted file mode 100644 index c06d9b0001..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/UrlBitmapLoader.kt +++ /dev/null @@ -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) -> 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() - -// It adds the ability to trigger multiple downloads with a unique callback. -open class DownloadTarget( - val callback: (Result) -> 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?) { - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt index cb587f277b..795e76fe06 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/ConfigManager.kt @@ -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, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt index 41d1ea2022..fc63ed5e9a 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/config/JsonModels.kt @@ -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?, val infuraProjectId: String?, val appsFlyerDevKey: String, - val shopifyShop: ShopifyShop? + val shopifyShop: ShopifyShop?, + val zendesk: ZendeskConfig?, ) class ConfigModel(val features: FeatureModel?, val configValues: ConfigValueModel?) { diff --git a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt index ac2d922c0d..10dbcb2cbb 100644 --- a/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/configurable/warningMessage/WarningMessagesManager.kt @@ -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 - } - } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt deleted file mode 100644 index ca04907f47..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/extensions/CurrencyExchangeManager.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index 06afdcbd3c..982ed86857 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -112,31 +112,44 @@ private class CreateWalletTangemWallet : ProductCommandProcessor 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, + session: CardSession, + callback: (result: CompletionResult) -> 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, session: CardSession, diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index 6888b7f648..2dafa32075 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt index a0be7dc18a..5b806beecc 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/LoadAvailableCoinsService.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt index 8180023cb9..543640ad2f 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectRepository.kt @@ -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 { 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) { 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() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a095d46d7e..952e2bb317 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -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 ) } diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 2789b0069d..71b6baa710 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -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 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() - 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 = 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() - internal val tokens = mutableMapOf>() - 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) { - 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 = "")) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt index 2b26faeeba..c453cdadc4 100644 --- a/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/home/HomeFragment.kt @@ -38,7 +38,9 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber 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) {} -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt index 4bfac1e8a0..183392a4b4 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/Stories.kt @@ -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.duration(): Int = when (this.value) { + 1 -> 8000 + else -> 6000 +} @Preview @Composable -fun InstagramScreenPreview() { +fun StoriesScreenPreview() { StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {}, onSearchTokensClick = {}) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt new file mode 100644 index 0000000000..58f9356349 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesAnimation.kt @@ -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 } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt deleted file mode 100644 index 410a8118aa..0000000000 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesGeneralContent.kt +++ /dev/null @@ -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) } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt new file mode 100644 index 0000000000..9ee80263c3 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/Content.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt new file mode 100644 index 0000000000..198e71d324 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/CurrenciesWeb3Content.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt new file mode 100644 index 0000000000..393642f976 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FirstStoriesContent.kt @@ -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.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.isSplashingTextDisplaying(): Boolean { + return this.value != StartingScreenState.MEET_TANGEM && + this.value != StartingScreenState.SHOW_CARD +} + +private fun MutableState.isMeetTangemDisplaying(): Boolean { + return this.value == StartingScreenState.MEET_TANGEM +} + + +@Preview +@Composable +fun FirstStoriesPreview() { + FirstStoriesContent(false, 8000) {} +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt new file mode 100644 index 0000000000..8528982857 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/home/compose/content/FloatingCardsContent.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt rename to app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt index 04fb1f2647..d49013db87 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/HomeButtons.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/HomeButtons.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt similarity index 97% rename from app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt rename to app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt index d121949529..096b87cb0b 100644 --- a/app/src/main/java/com/tangem/tap/features/home/compose/StoriesProgressBar.kt +++ b/app/src/main/java/com/tangem/tap/features/home/compose/views/StoriesProgressBar.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt index a37aa49595..1328156afa 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeAction.kt @@ -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() diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt index e4472de543..374b8d4406 100644 --- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt @@ -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 = { dispatch, state -> +private val homeMiddleware: Middleware = { _, _ -> { 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 = { 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( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index 26d719d0c6..8284efcd3d 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -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, diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt index 3125784ba9..b31dd02832 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt @@ -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() { 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 diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt index 77b12445f7..a9ebb0e757 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt @@ -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 { thisRef, property -> - store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false + val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> + store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt index 996fdd67fe..e2b1e015c4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt @@ -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 { thisRef, property -> - store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false + val isBuyAllowed: Boolean by ReadOnlyProperty { _, _ -> + store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false } } diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt index 38809f8d57..7827a4f196 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt @@ -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() { 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) = diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index 88121813ad..391d65cb96 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index b9691488c5..056d5a28bb 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -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) { diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt index 28a7f06b08..c15813fdaa 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/reducers/ReceiptReducer.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt index 8f4b106e97..6eccfef665 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/dialogs/SendTransactionFailsDialog.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 3c49da49fb..2b71c1ba31 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 7e1e4bee95..79f171b03f 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -28,8 +28,6 @@ sealed class TokensAction : Action { val wallets: List, val derivationStyle: DerivationStyle? ) : TokensAction() - data class SetNonRemovableCurrencies(val wallets: List) : TokensAction() - data class SaveChanges( val addedTokens: List, val addedBlockchains: List diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index dd40f7074e..2ec703e629 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -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) { 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 + )) } } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt index 374f6ad820..007a1e9711 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -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) } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt index 5fcc7510f2..ab1b3731a5 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensState.kt @@ -14,8 +14,6 @@ data class TokensState( val addedWallets: List = emptyList(), val addedTokens: List = emptyList(), val addedBlockchains: List = emptyList(), - val nonRemovableTokens: List = emptyList(), - val nonRemovableBlockchains: List = emptyList(), val currencies: List = emptyList(), val searchInput: String? = null, val allowToAdd: Boolean = true, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt index ef87846c21..49886cbf86 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrenciesScreen.kt @@ -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, + addedBlockchainsState: MutableState>, + addedTokens: List +) { + 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, + addedTokensState: MutableState>, + 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) { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt index 9fd962bce7..5ba1f8b683 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/CurrencyItem.kt @@ -9,8 +9,6 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain @Composable fun CurrencyItem( currency: Currency, - nonRemovableTokens: List, - nonRemovableBlockchains: List, addedTokens: List, addedBlockchains: List, allowToAdd: Boolean, @@ -22,8 +20,6 @@ fun CurrencyItem( if (expanded) { ExpandedCurrencyItem( currency = currency, - nonRemovableTokens = nonRemovableTokens, - nonRemovableBlockchains = nonRemovableBlockchains, addedTokens = addedTokens, addedBlockchains = addedBlockchains, allowToAdd = allowToAdd, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt index ef8c92c104..cecf873bf9 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ExpandedCurrencyItem.kt @@ -28,8 +28,6 @@ import com.tangem.wallet.R @Composable fun ExpandedCurrencyItem( currency: Currency, - nonRemovableTokens: List, - nonRemovableBlockchains: List, addedTokens: List, addedBlockchains: List, 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, ) diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt index 4892cf5e13..14f36c1dd4 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/ListOfCurrencies.kt @@ -22,8 +22,6 @@ import com.tangem.tap.features.tokens.redux.TokenWithBlockchain fun ListOfCurrencies( header: @Composable () -> Unit, currencies: List, - nonRemovableTokens: List, - nonRemovableBlockchains: List, addedTokens: List, addedBlockchains: List, 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, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt index 9b75a86359..0f45d867bf 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/compose/NetworkItem.kt @@ -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( diff --git a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt index 76945c588f..75f752b278 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/models/Currency.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index eefae0bcf6..89d77cd33f 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -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) : MultiWallet() + data class SaveCurrencies( + val blockchainNetworks: List, 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?) : 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, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt index 8581a61e6a..21489b82f3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletState.kt @@ -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 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?) : WalletDialog - object SignedHashesMultiWalletDialog : WalletDialog - object ChooseTradeActionDialog : WalletDialog - data class CurrencySelectionDialog( - val currenciesList: List, - 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) }, + ) } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 97a35f59c2..4030bae09b 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -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 -> { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt index 9590452975..a7ee50446d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt index 9ae3127317..d53809be9c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletDialogsMiddleware.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt index 5197928d6e..7aa45f997d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt @@ -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 } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt index 5e2458c15d..68718acd45 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/models/WalletDialog.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt index f61c3b6a0b..e8f9551910 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/MultiWalletReducer.kt @@ -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 } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt index d97e1e9942..482285de76 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/OnWalletLoadedReducer.kt @@ -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), diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt index 8690684197..0990cd2e78 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt @@ -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) diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt index 2be2f19145..c627aa2e32 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/BalanceWidget.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt index a01e075a8b..73eb9ea393 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt index 6a113fb7a8..176fa48a14 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt @@ -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 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 = diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt index e9173dafa0..8514f1e384 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WalletAdapter.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt index 9014d5d4ff..dee8fef67c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/adapters/WarningMessagesAdapter.kt @@ -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 diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt index 1c65315d3a..3fc5905990 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ChooseTradeActionBottomSheetDialog.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt new file mode 100644 index 0000000000..a41dce9228 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/RussianCardholdersWarningBottomSheetDialog.kt @@ -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" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt index 496a8ff731..91afcc3819 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/dialogs/ScanFailsDialog.kt @@ -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() } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt new file mode 100644 index 0000000000..fb75bc6474 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/images/CurrencyIconRequest.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index 5646b437f0..95560bd43d 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -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?, diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index 2e0a84785f..dadfaa1bc2 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -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 { diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt index 7a89edc4b0..811c9c0545 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt @@ -11,6 +11,7 @@ import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.CryptoCurrencyName import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.TangemSigner +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.store import com.tangem.tap.tangemSdk import java.math.BigDecimal @@ -18,57 +19,20 @@ import java.math.BigDecimal /** [REDACTED_AUTHOR] */ -interface ExchangeService { - suspend fun isBuyAllowed(): Boolean - suspend fun availableToBuy(): List - suspend fun isSellAllowed(): Boolean - suspend fun availableToSell(): List -} - -interface ExchangeUrlBuilder { - fun getUrl( - action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: CryptoCurrencyName, - fiatCurrencyName: String, - walletAddress: String, - ): String? - - fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? - - companion object { - const val SCHEME = "https" - const val URL_SELL = "sell.moonpay.com" - const val SUCCESS_URL = "tangem://success.tangem.com" - } -} - class CurrencyExchangeManager( - private val onramperService: ExchangeService, - private val moonPayService: ExchangeService, + private val buyService: ExchangeService, + private val sellService: ExchangeService, ) : ExchangeService, ExchangeUrlBuilder { - var status: CurrencyExchangeStatus? = null - private set - - suspend fun getStatus(): CurrencyExchangeStatus { - val isBuyAllowed = isBuyAllowed() - val isSellAllowed = isSellAllowed() - val availableToBuy = availableToBuy() - val availableToSell = availableToSell() - status = CurrencyExchangeStatus( - isBuyAllowed, - isSellAllowed, - availableToBuy, - availableToSell, - ) - return status!! + override suspend fun update() { + buyService.update() + sellService.update() } - override suspend fun isBuyAllowed(): Boolean = onramperService.isBuyAllowed() - override suspend fun availableToBuy(): List = onramperService.availableToBuy() - override suspend fun isSellAllowed(): Boolean = moonPayService.isSellAllowed() - override suspend fun availableToSell(): List = moonPayService.availableToSell() + override fun isBuyAllowed(): Boolean = buyService.isBuyAllowed() + override fun isSellAllowed(): Boolean = sellService.isSellAllowed() + override fun availableForBuy(currency: Currency): Boolean = buyService.availableForBuy(currency) + override fun availableForSell(currency: Currency): Boolean = sellService.availableForSell(currency) override fun getUrl( action: Action, @@ -96,22 +60,15 @@ class CurrencyExchangeManager( private fun getExchangeUrlBuilder(action: Action): ExchangeUrlBuilder { return when (action) { - Action.Buy -> onramperService - Action.Sell -> moonPayService + Action.Buy -> buyService + Action.Sell -> sellService } as ExchangeUrlBuilder } enum class Action { Buy, Sell } } -data class CurrencyExchangeStatus( - val isBuyAllowed: Boolean, - val isSellAllowed: Boolean, - val availableToBuy: List, - val availableToSell: List, -) - -suspend fun CurrencyExchangeManager.buyErc20Tokens(walletManager: EthereumWalletManager, token: Token) { +suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(walletManager: EthereumWalletManager, token: Token) { walletManager.safeUpdate() val amountToSend = Amount(walletManager.wallet.blockchain) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt new file mode 100644 index 0000000000..712fd2d5e0 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/ExchangeService.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.network.exchangeServices + +import com.tangem.blockchain.common.Blockchain +import com.tangem.tap.features.wallet.models.Currency + +interface ExchangeService { + suspend fun update() + fun isBuyAllowed(): Boolean + fun isSellAllowed(): Boolean + fun availableForBuy(currency: Currency):Boolean + fun availableForSell(currency: Currency):Boolean +} + +interface ExchangeUrlBuilder { + fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: String, + fiatCurrencyName: String, + walletAddress: String, + ): String? + + fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? + + companion object { + const val SCHEME = "https" + const val SUCCESS_URL = "tangem://success.tangem.com" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt new file mode 100644 index 0000000000..45494e0114 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoApi.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.network.exchangeServices.mercuryo + +import com.squareup.moshi.Json +import retrofit2.http.GET +import retrofit2.http.Path + +/** +[REDACTED_AUTHOR] + */ + + + +private val CurrenciesUrl = "https://api.mercuryo.io/v1.6/lib/currencies" + + + +interface MercuryoApi { + + @GET("{apiVersion}/lib/currencies") + suspend fun currencies( + @Path("apiVersion") apiVersion: String, + ): MercuryoCurrenciesResponse + + companion object { + const val BASE_URL = "https://api.mercuryo.io/" + const val API_VERSION = "v1.6" + } +} + +data class MercuryoCurrenciesResponse( + val status: Int, + val data: Data, +) { + data class Data( + val fiat: List, + val crypto: List, + val config: Config + ) + + data class Config( + val base: Map, + @Json(name = "has_withdrawal_fee") + val hasWithdrawalFee: Map, + @Json(name = "display_options") + val displayOptions: Map, + val icons: Map, + ) + + data class DisplayOption( + @Json(name = "fullname") + val fullName: String, + @Json(name = "total_digits") + val totalDigits: Int, + @Json(name = "display_digits") + val displayDigits: Int, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt new file mode 100644 index 0000000000..97190aa9fd --- /dev/null +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/mercuryo/MercuryoService.kt @@ -0,0 +1,135 @@ +package com.tangem.tap.network.exchangeServices.mercuryo + +import android.net.Uri +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.extensions.calculateSha512 +import com.tangem.common.extensions.toHexString +import com.tangem.common.services.Result +import com.tangem.common.services.performRequest +import com.tangem.network.common.createRetrofitInstance +import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.models.Currency +import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager +import com.tangem.tap.network.exchangeServices.ExchangeService +import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder + +/** +[REDACTED_AUTHOR] + */ +class MercuryoService( + private val apiVersion: String, + private val mercuryoWidgetId: String, + private val secret: String, +) : ExchangeService, ExchangeUrlBuilder { + + private val api: MercuryoApi = createRetrofitInstance(MercuryoApi.BASE_URL) + .create(MercuryoApi::class.java) + + private val blockchainsAvailableToBuy = mutableListOf() + private val tokensAvailableToBy = mutableMapOf>() + + override suspend fun update() { + when (val result = performRequest { api.currencies(apiVersion) }) { + is Result.Success -> { + val response = result.data + if (response.status == 200) { + // all currencies which can be bought + val currenciesAvailableToBy = response.data.crypto + // tokens which can be bought only from specific blockchain network + val supportedTokensWithNetwork = response.data.config.base + + currenciesAvailableToBy.forEach { currencyName -> + val blockchain = blockchainFromCurrencyName(currencyName) + if (blockchain == null) { + // suppose its a token + supportedTokensWithNetwork[currencyName]?.let { + blockchainFromCurrencyName(it) + }?.let { blockchainNetwork -> + val supportedInBlockchainsNetwork = tokensAvailableToBy[currencyName] + ?: mutableListOf() + supportedInBlockchainsNetwork.add(blockchainNetwork) + tokensAvailableToBy[currencyName] = supportedInBlockchainsNetwork + } + } else { + blockchainsAvailableToBuy.add(blockchain) + } + } + } + } + is Result.Failure -> { + blockchainsAvailableToBuy.clear() + tokensAvailableToBy.clear() + } + } + } + + override fun isBuyAllowed(): Boolean = true + + override fun isSellAllowed(): Boolean = false + + override fun availableForBuy(currency: Currency): Boolean { + if (!isBuyAllowed()) return false + + // blockchains which cant be defined by mercuryo service + val unsupportedBlockchains = listOf(Blockchain.Unknown, Blockchain.Binance, Blockchain.Arbitrum) + val blockchain = currency.blockchain + + return when (currency) { + is Currency.Blockchain -> { + when { + blockchain.isTestnet() -> blockchain.getTestnetTopUpUrl() != null + unsupportedBlockchains.contains(blockchain) -> false + else -> { + blockchainsAvailableToBuy.contains(currency.blockchain) + } + } + } + is Currency.Token -> { + val supportedInBlockchains = tokensAvailableToBy[currency.currencySymbol] ?: return false + supportedInBlockchains.contains(currency.blockchain) + } + } + } + + override fun availableForSell(currency: Currency): Boolean = false + + override fun getUrl( + action: CurrencyExchangeManager.Action, + blockchain: Blockchain, + cryptoCurrencyName: CryptoCurrencyName, + fiatCurrencyName: String, + walletAddress: String + ): String { + if (action == CurrencyExchangeManager.Action.Sell) throw UnsupportedOperationException() + + val builder = Uri.Builder() + .scheme(ExchangeUrlBuilder.SCHEME) + .authority("exchange.mercuryo.io") + .appendQueryParameter("widget_id", mercuryoWidgetId) + .appendQueryParameter("type", action.name.lowercase()) + .appendQueryParameter("currency", cryptoCurrencyName) + .appendQueryParameter("address", walletAddress) + .appendQueryParameter("signature", signature(walletAddress)) + .appendQueryParameter("fix_currency", "true") + .appendQueryParameter("return_url", ExchangeUrlBuilder.SUCCESS_URL) + + val url = builder.build().toString() + return url + } + + private fun signature(address: String): String { + return (address + secret).calculateSha512().toHexString().lowercase() + } + + + override fun getSellCryptoReceiptUrl( + action: CurrencyExchangeManager.Action, + transactionId: String + ): String? = null + + private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) { + "BNB" -> Blockchain.BSC + "ETH" -> Blockchain.Ethereum + else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() } + } +} diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 54fd9c558e..fa96286e84 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -5,15 +5,15 @@ import android.util.Base64 import com.tangem.blockchain.common.Blockchain import com.tangem.common.services.Result import com.tangem.common.services.performRequest +import com.tangem.domain.common.extensions.withIOContext import com.tangem.network.common.createRetrofitInstance import com.tangem.tap.common.extensions.urlEncode import com.tangem.tap.common.redux.global.CryptoCurrencyName +import com.tangem.tap.features.wallet.models.Currency import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager import com.tangem.tap.network.exchangeServices.ExchangeService import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME -import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.URL_SELL -import kotlinx.coroutines.coroutineScope import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec @@ -29,14 +29,14 @@ class MoonPayService( private var status: MoonPayStatus? = null - private suspend fun updateStatus() { - try { - coroutineScope { + override suspend fun update() { + withIOContext { + performRequest { val userStatusResult = performRequest { api.getUserStatus(apiKey) } - if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult + if (userStatusResult is Result.Failure) return@performRequest val currenciesResult = performRequest { api.getCurrencies(apiKey) } - if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult + if (currenciesResult is Result.Failure) return@performRequest val userStatus = (userStatusResult as Result.Success).data val currencies = (currenciesResult as Result.Success).data @@ -65,29 +65,31 @@ class MoonPayService( status = MoonPayStatus(currenciesToSell, userStatus, currencies) } - } catch (error: Error) { - status = null - Result.Failure(error) } } - override suspend fun isBuyAllowed(): Boolean = false + override fun isBuyAllowed(): Boolean = false - override suspend fun availableToBuy(): List = listOf() - - override suspend fun isSellAllowed(): Boolean { - refreshStatus() + override fun isSellAllowed(): Boolean { return status?.responseUserStatus?.isSellAllowed ?: false } - override suspend fun availableToSell(): List { - refreshStatus() - return status?.availableToSell ?: emptyList() - } + override fun availableForBuy(currency: Currency): Boolean = false - private suspend fun refreshStatus() { - if (status == null) { - updateStatus() + override fun availableForSell(currency: Currency): Boolean { + val availableForSell = status?.availableForSell ?: 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 -> availableForSell.contains(currency.currencySymbol) + } + } + is Currency.Token -> false } } @@ -133,10 +135,14 @@ class MoonPayService( val sha256encoded = sha256Hmac.doFinal(data.toByteArray()) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) } + + companion object { + const val URL_SELL = "sell.moonpay.com" + } } private data class MoonPayStatus( - val availableToSell: List, + val availableForSell: List, val responseUserStatus: MoonPayUserStatus, val responseCurrencies: List ) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt deleted file mode 100644 index 30a65ed706..0000000000 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnnramperApi.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.tangem.tap.network.exchangeServices.onramper - -import com.squareup.moshi.JsonClass -import retrofit2.http.GET -import retrofit2.http.Path - -interface OnramperApi { - @GET("gateways") - suspend fun gateways(): GatewaysResponse - - @GET("rate/{fromCurrency}/{toCurrency}/{paymentMethod}/{amount}") - suspend fun rate( - @Path("fromCurrency") fromCurrency: String, - @Path("toCurrency") toCurrency: String, - @Path("paymentMethod") paymentMethod: String, - @Path("amount") amount: Int, - ): RateResponse - - companion object { - val BASE_URL = "https://onramper.tech/" - } -} - -@JsonClass(generateAdapter = true) -data class GatewaysResponse( - val gateways: List -) - -@JsonClass(generateAdapter = true) -data class OnramperGateway( - val identifier: String, - val paymentMethods: List, - val fiatCurrencies: List, - val cryptoCurrencies: List -) - -@JsonClass(generateAdapter = true) -data class OnramperCurrency( - val id: String, - val code: String, - val precision: Int -) - -@JsonClass(generateAdapter = true) -data class RateResponse( - val identifier: String, - val duration: OnramperDuration, - val available: Boolean, - val error: OnramperError? = null, - val rate: Double? = null, - val fees: Double? = null, - val requiredKYC: List? = null, - val receivedCrypto: Double? = null, - val nextStep: OnramperNextStep? = null, -) - -@JsonClass(generateAdapter = true) -data class OnramperNextStep( - val type: String, - val url: String, - val message: String, - val extraData: List -) - -@JsonClass(generateAdapter = true) -data class OnramperExtraData( - val type: String, - val name: String, - val humanName: String, -) - -@JsonClass(generateAdapter = true) -data class OnramperDuration( - val seconds: Long, - val message: String -) - -@JsonClass(generateAdapter = true) -data class OnramperError( - val type: String, - val message: String, - val limit: Double -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt deleted file mode 100644 index 94bdae395e..0000000000 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/onramper/OnramperService.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.tangem.tap.network.exchangeServices.onramper - -import android.net.Uri -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.services.Result -import com.tangem.common.services.performRequest -import com.tangem.network.common.AddHeaderInterceptor -import com.tangem.network.common.createRetrofitInstance -import com.tangem.tap.common.extensions.urlEncode -import com.tangem.tap.common.redux.global.CryptoCurrencyName -import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager -import com.tangem.tap.network.exchangeServices.ExchangeService -import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder -import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SCHEME -import com.tangem.tap.network.exchangeServices.ExchangeUrlBuilder.Companion.SUCCESS_URL -import kotlinx.coroutines.coroutineScope -import java.util.* - -/** -[REDACTED_AUTHOR] - */ -class OnramperService( - val apiKey: String -) : ExchangeService, ExchangeUrlBuilder { - - private val api: OnramperApi by lazy { - createRetrofitInstance( - baseUrl = OnramperApi.BASE_URL, - interceptors = listOf( - AddHeaderInterceptor(mapOf("Authorization" to "Basic $apiKey")), - ) - ).create(OnramperApi::class.java) - } - - private var status: OnramperStatus? = null - - private suspend fun updateStatus() { - try { - coroutineScope { - val result = performRequest { api.gateways() } - if (result is Result.Failure) return@coroutineScope result - - val response = (result as Result.Success).data - val currenciesToBuy = extractCurrenciesToBuy(response).sorted() - val status = OnramperStatus(currenciesToBuy, response) - this@OnramperService.status = status - } - } catch (error: Error) { - status = null - Result.Failure(error) - } - } - - private fun extractCurrenciesToBuy(response: GatewaysResponse): List { - return response.gateways.map { gateway -> - gateway.cryptoCurrencies.map { currency -> currency.code } - }.flatten().toMutableSet().toList() - } - - override suspend fun isBuyAllowed(): Boolean { - refreshStatus() - return status != null - } - - override suspend fun availableToBuy(): List { - refreshStatus() - return status?.availableToBuy ?: emptyList() - } - - private suspend fun refreshStatus() { - if (status == null) { - updateStatus() - } - } - - override suspend fun isSellAllowed(): Boolean = false - - override suspend fun availableToSell(): List = listOf() - - override fun getUrl( - action: CurrencyExchangeManager.Action, - blockchain: Blockchain, - cryptoCurrencyName: CryptoCurrencyName, - fiatCurrency: String, - walletAddress: String, - ): String? { - var languageCode = Locale.getDefault().language - if (languageCode.isEmpty()) languageCode = "en" - - val builder = Uri.Builder() - .scheme(SCHEME) - .authority("widget.onramper.com") - .appendQueryParameter("apiKey", this.apiKey.urlEncode()) - .appendQueryParameter("defaultCrypto", cryptoCurrencyName) - .appendQueryParameter("wallets", "${blockchain.currency}:$walletAddress".urlEncode()) - .appendQueryParameter("redirectURL", SUCCESS_URL) - .appendQueryParameter("defaultFiat", fiatCurrency) - .appendQueryParameter("language", languageCode) - - status?.apply { - val gateways = responseGateways.gateways.joinToString(",") { it.identifier }.urlEncode() - builder.appendQueryParameter("onlyGateways", gateways) - - } - - val url = builder.build().toString() - return url - } - - override fun getSellCryptoReceiptUrl(action: CurrencyExchangeManager.Action, transactionId: String): String? = null -} - -private data class OnramperStatus( - val availableToBuy: List, - val responseGateways: GatewaysResponse -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt index f676467852..15073fb931 100644 --- a/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt +++ b/app/src/main/java/com/tangem/tap/persistence/PreferencesStorage.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit import com.tangem.common.json.MoshiJsonConverter -import java.util.* +import java.util.Calendar class PreferencesStorage(applicationContext: Application) { @@ -53,20 +53,11 @@ class PreferencesStorage(applicationContext: Application) { preferences.edit { putInt(APP_LAUNCH_COUNT_KEY, ++count) } } - fun wasRestoreFundsWarningClosed(): Boolean { - return preferences.getBoolean(RESTORE_FUNDS_CLOSED_KEY, false) - } - - fun saveRestoreFundsWarningClosed() { - preferences.edit { putBoolean(RESTORE_FUNDS_CLOSED_KEY, true) } - } - companion object { private const val PREFERENCES_NAME = "tapPrefs" private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted" private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown" private const val APP_LAUNCH_COUNT_KEY = "launchCount" - private const val RESTORE_FUNDS_CLOSED_KEY = "restoreFundsClosed" } } diff --git a/app/src/main/res/drawable/currency0.png b/app/src/main/res/drawable/currency0.png new file mode 100644 index 0000000000..0d293ec8bb Binary files /dev/null and b/app/src/main/res/drawable/currency0.png differ diff --git a/app/src/main/res/drawable/currency1.png b/app/src/main/res/drawable/currency1.png new file mode 100644 index 0000000000..37144efffe Binary files /dev/null and b/app/src/main/res/drawable/currency1.png differ diff --git a/app/src/main/res/drawable/currency2.png b/app/src/main/res/drawable/currency2.png new file mode 100644 index 0000000000..bd75790221 Binary files /dev/null and b/app/src/main/res/drawable/currency2.png differ diff --git a/app/src/main/res/drawable/currency3.png b/app/src/main/res/drawable/currency3.png new file mode 100644 index 0000000000..c517578807 Binary files /dev/null and b/app/src/main/res/drawable/currency3.png differ diff --git a/app/src/main/res/drawable/currency4.png b/app/src/main/res/drawable/currency4.png new file mode 100644 index 0000000000..9a39d5651d Binary files /dev/null and b/app/src/main/res/drawable/currency4.png differ diff --git a/app/src/main/res/drawable/dapps0.png b/app/src/main/res/drawable/dapps0.png new file mode 100644 index 0000000000..6152b1215b Binary files /dev/null and b/app/src/main/res/drawable/dapps0.png differ diff --git a/app/src/main/res/drawable/dapps1.png b/app/src/main/res/drawable/dapps1.png new file mode 100644 index 0000000000..2d4effaf8b Binary files /dev/null and b/app/src/main/res/drawable/dapps1.png differ diff --git a/app/src/main/res/drawable/dapps2.png b/app/src/main/res/drawable/dapps2.png new file mode 100644 index 0000000000..f7643c4ec7 Binary files /dev/null and b/app/src/main/res/drawable/dapps2.png differ diff --git a/app/src/main/res/drawable/dapps3.png b/app/src/main/res/drawable/dapps3.png new file mode 100644 index 0000000000..9e0110179b Binary files /dev/null and b/app/src/main/res/drawable/dapps3.png differ diff --git a/app/src/main/res/drawable/dapps4.png b/app/src/main/res/drawable/dapps4.png new file mode 100644 index 0000000000..4eae3c0985 Binary files /dev/null and b/app/src/main/res/drawable/dapps4.png differ diff --git a/app/src/main/res/drawable/dapps5.png b/app/src/main/res/drawable/dapps5.png new file mode 100644 index 0000000000..0c581624c7 Binary files /dev/null and b/app/src/main/res/drawable/dapps5.png differ diff --git a/app/src/main/res/drawable/ic_cross_rounded_24.xml b/app/src/main/res/drawable/ic_cross_rounded_24.xml new file mode 100644 index 0000000000..ad1cab5a84 --- /dev/null +++ b/app/src/main/res/drawable/ic_cross_rounded_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/img_flag_ru_24.xml b/app/src/main/res/drawable/img_flag_ru_24.xml new file mode 100644 index 0000000000..deff0dc4bb --- /dev/null +++ b/app/src/main/res/drawable/img_flag_ru_24.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/img_warning_triangle_24.xml b/app/src/main/res/drawable/img_warning_triangle_24.xml new file mode 100644 index 0000000000..c54d7d8f8b --- /dev/null +++ b/app/src/main/res/drawable/img_warning_triangle_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/card_total_balance.xml b/app/src/main/res/layout/card_total_balance.xml deleted file mode 100644 index 1f97649e18..0000000000 --- a/app/src/main/res/layout/card_total_balance.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_russians_cardholders_warning.xml b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml new file mode 100644 index 0000000000..8f2196c2d5 --- /dev/null +++ b/app/src/main/res/layout/dialog_russians_cardholders_warning.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 58ac038846..2171b91e61 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -302,8 +302,22 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" /> + + - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml index 17bf714628..1545257fc9 100644 --- a/app/src/main/res/layout/fragment_wallet.xml +++ b/app/src/main/res/layout/fragment_wallet.xml @@ -57,12 +57,13 @@ android:layout_marginTop="8dp" android:layout_marginEnd="16dp" android:layout_marginBottom="12dp" - android:adjustViewBounds="true" - android:elevation="3dp" - android:maxHeight="215dp" - android:scaleType="fitCenter" - android:src="@drawable/card_placeholder_black" - app:layout_constraintTop_toTopOf="parent" /> + android:adjustViewBounds="true" + android:elevation="3dp" + android:maxHeight="215dp" + android:scaleType="fitCenter" + android:src="@drawable/card_placeholder_black" + android:contentDescription="@null" + app:layout_constraintTop_toTopOf="parent" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:minHeight="0dp" + android:nestedScrollingEnabled="false" + android:overScrollMode="never" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/barrier" /> + android:id="@+id/l_wallet_backup_warning" + layout="@layout/layout_wallet_backup_warning" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" /> + + + android:id="@+id/rv_multiwallet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:nestedScrollingEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" /> + + + + @@ -171,28 +208,6 @@ android:text="@string/main_manage_tokens" android:elevation="4dp" app:cornerRadius="8dp" - tools:visibility="gone" /> - - - - + app:layout_constraintBottom_toBottomOf="parent" /> diff --git a/app/src/main/res/layout/layout_balance.xml b/app/src/main/res/layout/layout_balance.xml index 0e3f124d94..6addd2be86 100644 --- a/app/src/main/res/layout/layout_balance.xml +++ b/app/src/main/res/layout/layout_balance.xml @@ -35,7 +35,8 @@ android:visibility="gone" app:drawableStartCompat="@drawable/ic_ok" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/tv_currency" /> + app:layout_constraintTop_toBottomOf="@id/tv_currency" + tools:visibility="visible" /> + tools:text="3.58 USD" /> + app:layout_constraintStart_toEndOf="@+id/tv_status_verified" + app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" + tools:text="123.12387628 XTZ" /> + app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" /> + tools:text="0.00434143 ETH" /> + tools:text="496304.28 RUB" /> + app:layout_constraintTop_toBottomOf="@id/tv_fiat_amount" + tools:text="0.43 BTC" /> + app:layout_constraintTop_toBottomOf="@id/tv_amount" + tools:text="Some error" /> diff --git a/app/src/main/res/layout/layout_card_total_balance.xml b/app/src/main/res/layout/layout_card_total_balance.xml new file mode 100644 index 0000000000..fe3484f373 --- /dev/null +++ b/app/src/main/res/layout/layout_card_total_balance.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/layout_wallet_backup_warning.xml b/app/src/main/res/layout/layout_wallet_backup_warning.xml new file mode 100644 index 0000000000..119ab12734 --- /dev/null +++ b/app/src/main/res/layout/layout_wallet_backup_warning.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/wallet_details.xml b/app/src/main/res/menu/wallet_details.xml index 08ba8df389..d49dd52b30 100644 --- a/app/src/main/res/menu/wallet_details.xml +++ b/app/src/main/res/menu/wallet_details.xml @@ -3,6 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a74c2f7407..99c83c09c5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,6 +1,5 @@ - Nein Änderungen speichern Warnung Erneut versuchen @@ -35,10 +34,10 @@ Diese Karte wird nicht unterstützt Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App Die Transaktion läuft… - Senden\u0020 - an %s - Erhalten\u0020 - von %s + Senden + an %s + Erhalten + von %s Adresse erkunden PayString erstellen %s Wallet @@ -177,4 +176,4 @@ RECHTLICHER HAFTUNGSAUSSCHLUSS Ihr Kontostand in dieser Brieftasche ist nicht Null, oder Sie haben unbestätigte Transaktionen. Es ist unmöglich die Funktion der Brieftasche zu löschen. Dies ist die derzeit aktive Wahl - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings_final.xml b/app/src/main/res/values-de/strings_final.xml index b944fee969..7f8c9a2625 100644 --- a/app/src/main/res/values-de/strings_final.xml +++ b/app/src/main/res/values-de/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -49,6 +51,7 @@ Attention The server is not available, please try again later + Hide token Hide %s Hide You hide the token from the main screen, but you can add it back at any time. @@ -57,4 +60,5 @@ %s network not found. Please, add it first and try again. Blockchain is not supported. Please select another blockchain. + %s network \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3b06614517..17a6a3cce0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,107 +1,106 @@ - Non - Sauvegarder les modifications - Alerte - Réessayer - Exécuté - Annuler + Sauvegarder les modifications + Alerte + Réessayer + Exécuté + Annuler J\'accepte - L\'accès à la caméra est refusé - Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité - Paramètres + L\'accès à la caméra est refusé + Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité + Paramètres Bienvenue à Tangem. Avez-vous une de nos cartes - Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer. - Oui! Scannez-la. - Magasin + Encore une fois, bienvenue dans Tangem. \nScannez votre carte pour commencer. + Oui! Scannez-la. + Magasin Préparez-vous à appuyer votre carte au dos de votre téléphone. - Appuyez - Scannez la carte - Scanner - Envoyer - Créer un portefeuille + Appuyez + Scannez la carte + Scanner + Envoyer + Créer un portefeuille Tangem - Choisir une variante de portefeuille - Jeton d\'authentification - Jetons d\'authentification - Solde confirmé - Solde est en cours de téléchargement … - Blockchain est inaccessible - Compte n\'est pas créé - Téléchargez %s +%s pour créer un compte - Carte vide - Créez un portefeuille pour commencer à utiliser votre carte Tangem - Cette carte n\'est pas prise en charge - Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente + Choisir une variante de portefeuille + Jeton d\'authentification + Jetons d\'authentification + Solde confirmé + Solde est en cours de téléchargement … + Blockchain est inaccessible + Compte n\'est pas créé + Téléchargez %s +%s pour créer un compte + Carte vide + Créez un portefeuille pour commencer à utiliser votre carte Tangem + Cette carte n\'est pas prise en charge + Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente Transaction en cours… - Envoi\u0020 - jusqu\'à %s - Réception \u0020 - de %s - Explorer l\'adresse + Envoi + jusqu\'à %s + Réception + de %s + Explorer l\'adresse Créer PayString Portefeuille %s Créer PayString Carte: %s Dénomination PayString $payid.tangem.com - Créer + Créer Votre PayString est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN. - PayString a été créé avec succès et copié dans le presse-papiers - Mauvaise réponse lors de la création de PayString + PayString a été créé avec succès et copié dans le presse-papiers + Mauvaise réponse lors de la création de PayString Ce PayString existe déjà. Essayez une autre variante. Adresse ou PayString Adresse - Envoyer - Commissions du réseau - Somme - Commissions - Total - Somme maximale - Bas - Normal - Priorité - Inclure les commissions + Envoyer + Commissions du réseau + Somme + Commissions + Total + Somme maximale + Bas + Normal + Priorité + Inclure les commissions Sera envoyé %s %s ≈ %s (incl. les commissions : %s) Sera envoyé %s %s et %s %s Solde : %s %s - La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps - Référénces - ID de la carte - Emetteur - Signé - %s hashes - Monnaie de l\'application - Paramètres - Carte - Valider la carte - Gestion de la sécurité - Annuler le portefeuille + La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps + Référénces + ID de la carte + Emetteur + Signé + %s hashes + Monnaie de l\'application + Paramètres + Carte + Valider la carte + Gestion de la sécurité + Annuler le portefeuille Cette action est irréversible. Si, après avoir annulé le portefeuille, quelqu\'un lui envoie des fonds, vous ne pourrez pas les retirer. - Si vous oubliez le code, vous perdrez la possibilité d\'utiliser la carte. Si vous perdez le code, il sera impossible de le restaurer ou modifier. - Gestion de la sécurité + Si vous oubliez le code, vous perdrez la possibilité d\'utiliser la carte. Si vous perdez le code, il sera impossible de le restaurer ou modifier. + Gestion de la sécurité Tenez la carte fermement Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement. - Mot de passe + Mot de passe Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe. - Code d\'accès - Vous devrez entrer le mot de passe correct avant de scanner la carte - Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de l\'extraction + Code d\'accès + Vous devrez entrer le mot de passe correct avant de scanner la carte + Vous pouvez rencontrer des problèmes NFC avec certains iPhone 7/7 + lors de l\'extraction Cette carte a déjà été rechargée et a signé des transactions avant. Envisagez la possibilité de retirer tous les fonds immédiatement si vous avez reçu cette carte d\'une source non fiable. Attention Cette carte n\'est pas conçue pour fonctionner avec Tangem - La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement - Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. - Touchez pour signer - Appuyez votre carte au téléphone comme indiqué ci-dessus - Touchez, pour annuler un portefeuille - Touchez, pour créer un portefeuille - Touchez, pour modifier le code d\'accès - Touchez, pour modifier le mot de passe - PayString incorrect + La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement + Les cartes Tangem émises avant septembre 2019 ne peuvent actuellement pas être extraites à l\'aide de l\'iPhone. Nous travaillons activement avec Apple pour rendre cela possible dans les futures versions d\'iOS. + Touchez pour signer + Appuyez votre carte au téléphone comme indiqué ci-dessus + Touchez, pour annuler un portefeuille + Touchez, pour créer un portefeuille + Touchez, pour modifier le code d\'accès + Touchez, pour modifier le mot de passe + PayString incorrect @@ -154,7 +153,7 @@ Avertissement \n\nCette avertissement a été modifié, pour la dernière fois, le 1 octobre 2020. - Conditions d\'utilisation + Conditions d\'utilisation Pas de connexion internet La vérification de PayString a échoué @@ -170,4 +169,4 @@ Avertissement Cette option est déjà active - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings_final.xml b/app/src/main/res/values-fr/strings_final.xml index 571a03b91a..284dbb30b7 100644 --- a/app/src/main/res/values-fr/strings_final.xml +++ b/app/src/main/res/values-fr/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -49,6 +51,7 @@ Notice Attention + Hide token Hide %s Hide You hide the token from the main screen, but you can add it back at any time. @@ -57,4 +60,5 @@ %s network not found. Please, add it first and try again. Blockchain is not supported. Please select another blockchain. + %s network \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b45dafe795..4d699600d3 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,7 +1,6 @@ - No Mantieni le modifiche Avviso Riprova @@ -34,10 +33,10 @@ Questa carta non è supportata La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta Transazione in corso… - Invia\u0020 - su %s - Ricevi\u0020 - da %s + Invia + su %s + Ricevi + da %s Cerca indirizzo Crea PayString portafoglio %s @@ -177,4 +176,4 @@ Questa nota legale è stata modificata l\'ultima volta 01.10.2020. Il tuo saldo su questo portafoglio non è pari a zero o hai transazioni non confermate. Non è possibile eliminare la funzione portafoglio. E\' l\'opzione attualmente attiva - \ No newline at end of file + diff --git a/app/src/main/res/values-it/strings_final.xml b/app/src/main/res/values-it/strings_final.xml index f534f5caf5..d58a04b1e3 100644 --- a/app/src/main/res/values-it/strings_final.xml +++ b/app/src/main/res/values-it/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -49,6 +51,7 @@ Notice Attention + Hide token Hide %s Hide You hide the token from the main screen, but you can add it back at any time. @@ -58,4 +61,5 @@ %s network not found. Please, add it first and try again. Blockchain is not supported. Please select another blockchain. + %s network \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f336695b54..1fbdfd7a66 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,7 +1,6 @@ Tangem - Нет Сохранить изменения Предупреждение Повторить попытку @@ -38,8 +37,8 @@ Транзакция в процессе… до %s от %s - Получение\u0020 - Отправка\u0020 + Получение + Отправка Посмотреть адрес Создать PayString %s кошелек @@ -182,7 +181,6 @@ Добавить пользовательский Популярные токены Добавлен - Удалить токен Значок токена Добавить токен Адрес контракта @@ -331,7 +329,7 @@ %1s (%2s) Эта карта не является векселем на предъявителя. В настоящее время мы не можем сопоставить количество подписей на карте с информацией в блокчейне. Это нормально, но в редких случаях может означать, что предыдущий владелец скрывает автономную подпись, что является проблемой безопасности.\n\nНе принимайте эту карту в качестве физического платежа от кого-то, кому Вы не доверяете.\n\nВо всех остальных отношениях - это совершенно безопасно.\n\nTangem — единственный аппаратный кошелек, предлагающий защиту от подсчета подписей. WalletConnect - Запрос на создание транзакции для %2s\n%3s\n\nСумма: %4s\nКомиссия: %5s\nВсего: %6s\nБаланс: %7s + Карта %1s\nЗапрос на создание транзакции для %2s\n%3s\n\nСумма: %4s\nКомиссия: %5s\nВсего: %6s\nБаланс: %7s Запрос на запуск сеанса для карты с идентификатором %1s\nдля %2s\n\nURL: %3s Сессии WalletConnect Сеанс WalletConnect открыт с %s @@ -366,4 +364,5 @@ Привет, команда поддержки, Пожалуйста, расскажите нам больше о Вашей проблеме. Каждая деталь может быть полезной. Информация ниже не является обязательной. Вы можете стереть её, если хотите. + Нет diff --git a/app/src/main/res/values-ru/strings_final.xml b/app/src/main/res/values-ru/strings_final.xml index 4cb3e5fbbb..de998d28b4 100644 --- a/app/src/main/res/values-ru/strings_final.xml +++ b/app/src/main/res/values-ru/strings_final.xml @@ -23,7 +23,9 @@ Революционный аппаратный кошелек Держите свои криптосбережения в безопасности. Приватные ключи надежно хранятся на карте. Все ключи в безопасности - До трех карт с одним кошельком + До + трех карт + с одним кошельком Тысячи криптовалют Аппаратный кошелек для ваших биткоинов, эфира и многих других валют одновременно — все в одной карте Поддержка DeFi @@ -54,6 +56,7 @@ Управление токенами Нет цены + Скрыть токен Скрыть %s Скрыть Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно. @@ -63,4 +66,14 @@ Сеть %s не найдена. Пожалуйста, добавьте её и попробуйте заново. Сеть не поддерживается. Пожалуйста, выберите другую сеть. + Сеть %s + + Бэкап кошелька не был произведен + Чтобы защитить свои активы, мы советуем вам выполнить эту процедуру + + Карты банков РФ в данный момент не принимаются + У вас есть карта банка другой страны или платежной системы UnionPay? + Да + + Задать вопрос diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23ff1e496f..60d0c22f1d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,6 @@ Tangem - No Save changes Warning Retry @@ -45,8 +44,8 @@ Transaction is in progress… to %s from %s - Receiving\u0020 - Sending\u0020 + Receiving + Sending Explore address Create PayString @@ -187,4 +186,4 @@ Your balance on this wallet is not zero, or you have unconfirmed transactions This is the currently active option - \ No newline at end of file + diff --git a/app/src/main/res/values/strings_final.xml b/app/src/main/res/values/strings_final.xml index 5fe04101e5..f62ca8c1c6 100644 --- a/app/src/main/res/values/strings_final.xml +++ b/app/src/main/res/values/strings_final.xml @@ -23,7 +23,9 @@ Revolutionary Hardware Wallet Store your crypto assets secure while keeping private keys contained in your card Ultra Secure Backup - Up to 3 physical cards to one wallet + Up to + 3 physical cards + to one wallet Thousands of Currencies A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card DeFi Compatible @@ -54,12 +56,26 @@ Manage tokens No rate + Hide token Hide %s Hide - You hide the token from the main screen, but you can add it back at any time. + You are about to hide this token from the main screen. You can add it back anytime. Unable to hide %s The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list. %s network not found. Please, add it first and try again. Blockchain is not supported. Please select another blockchain. + + %s network + + Your wallet has not been backed up + To protect your assets, we advise you to carry out this procedure + Remove token + + Russian bank cards are not accepted at the moment + Do you have a bank card of another country or a UnionPay card? + Yes + No + + Ask a question diff --git a/app/src/main/res/values/strings_untranslatable.xml b/app/src/main/res/values/strings_untranslatable.xml deleted file mode 100644 index ff1f090a6f..0000000000 --- a/app/src/main/res/values/strings_untranslatable.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index 649081be00..84f36c07bf 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -79,7 +79,6 @@ Custom tokens Popular tokens Added - Remove token Token icon Add Token @@ -141,7 +140,7 @@ Card: %s Tap card to bind to wallet connect - Card: %1s + Card: %1s\n Request to create transaction for %2s \n%3s diff --git a/app/src/main/res/values/zendesk_styles.xml b/app/src/main/res/values/zendesk_styles.xml new file mode 100644 index 0000000000..ebb4fcab6b --- /dev/null +++ b/app/src/main/res/values/zendesk_styles.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000000..2bbb4f525b --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + api.tangem.com + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 23b9e4d0bc..64f57c2248 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ allprojects { mavenLocal() maven { url "https://nexus.tangem-tech.com/repository/maven-releases/" } maven { url 'https://jitpack.io' } + maven { url 'https://zendesk.jfrog.io/zendesk/repo' } } } diff --git a/dependencies.gradle b/dependencies.gradle index d463106208..45200c5dcf 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,10 +1,12 @@ ext.versions = [ - kotlin : '1.6.10', - build_gradle: '7.1.3', + kotlin : '1.6.10', + build_gradle : '7.1.3', + tamgem_card_sdk : 'develop-154', + tamgem_blockchain_sdk: 'develop-93', ] ext.environmentConfig = [ - environment: "ENVIRONMENT", + environment : "ENVIRONMENT", testActionEnabled: "TEST_ACTION_ENABLED", logEnabled : "LOG_ENABLED", ] \ No newline at end of file diff --git a/domain/build.gradle b/domain/build.gradle index 169aea23f3..d7a8832fd4 100644 --- a/domain/build.gradle +++ b/domain/build.gradle @@ -59,10 +59,9 @@ dependencies { implementation implementation(project(path: ':network')) implementation implementation(project(path: ':common')) - implementation 'com.tangem:blockchain:develop-88' -// implementation 'com.tangem:blockchain:0.0.1' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-154' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-154' + implementation "com.tangem:blockchain:${versions.tamgem_blockchain_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}" + implementation "com.tangem.tangem-sdk-kotlin:android:${versions.tamgem_card_sdk}" // Kotlin coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt b/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt index f841679ffb..3304349b69 100644 --- a/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt +++ b/domain/src/main/java/com/tangem/domain/common/extensions/TangemTechService.kt @@ -16,11 +16,13 @@ suspend fun TangemTechService.getTokens( suspend fun TangemTechService.getListOfCoins( networkIds: List, + active: Boolean? = null, searchText: String? = null, offset: Int? = null, limit: Int? = null ): Result = coins( networkIds = networkIds.joinToString(","), + active = active, searchText = searchText, offset = offset, limit = limit diff --git a/gradle.properties b/gradle.properties index 8f60b3bd0b..52a563ad69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,4 +13,4 @@ org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryErro org.gradle.parallel=true org.gradle.daemon=true android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/network/build.gradle b/network/build.gradle index b90ff59980..dce3040c09 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -12,7 +12,7 @@ dependencies { implementation implementation(project(path: ':common')) // Tangem sdk's - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-142' + implementation "com.tangem.tangem-sdk-kotlin:core:${versions.tamgem_card_sdk}" // Kotlin coroutines implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2' diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt index c21fb92726..9a4b2cd8b1 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/Responses.kt @@ -43,4 +43,8 @@ data class CurrenciesResponse(val currencies: List) { val unit: String, // $, €, ₽ val type: String, ) : TangemTechResponse -} \ No newline at end of file +} + +data class GeoResponse( + val code: String +) : TangemTechResponse \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt index 0081f4ee24..80558732dd 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechApi.kt @@ -27,4 +27,6 @@ interface TangemTechApi { @GET("currencies") suspend fun currencies(): CurrenciesResponse + @GET("geo") + suspend fun geo(): GeoResponse } \ No newline at end of file diff --git a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt index 5790bdac3a..3bf3bd232e 100644 --- a/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt +++ b/network/src/main/java/com/tangem/network/api/tangemTech/TangemTechService.kt @@ -47,6 +47,10 @@ class TangemTechService { } } + suspend fun userCountry(): Result = withContext(Dispatchers.IO) { + performRequest { api.geo() } + } + suspend fun currencies(): Result = withContext(Dispatchers.IO) { performRequest { api.currencies() } }