Updated on 2026-08-14

This commit is contained in:
Tangem 2022-03-05 22:30:23 +03:00
commit 4efdd325be
245 changed files with 52320 additions and 19378 deletions

View file

@ -1,22 +1,22 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
android {
compileSdkVersion 30
compileSdkVersion 31
defaultConfig {
applicationId "com.tangem.wallet"
versionCode project.hasProperty('versionCode') ? project.property('versionCode') as int : 1
versionName project.hasProperty('versionName') ? project.property('versionName') : "1.0.0.-SNAPSHOT"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 31
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
androidExtensions {
experimental = true
buildFeatures {
compose true
viewBinding true
}
buildTypes {
release {
@ -47,12 +47,14 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
coreLibraryDesugaringEnabled false
}
composeOptions {
kotlinCompilerExtensionVersion '1.1.0'
}
packagingOptions {
exclude 'lib/x86_64/darwin/libscrypt.dylib'
exclude 'lib/x86_64/freebsd/libscrypt.so'
@ -67,19 +69,19 @@ repositories {
dependencies {
implementation fileTree(include: ['*.aar'], dir: 'libs')
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.core:core-ktx:1.6.0"
implementation 'com.google.android.play:core:1.10.2'
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.material:material:1.5.0'
implementation "androidx.core:core-ktx:1.7.0"
implementation 'com.google.android.play:core:1.10.3'
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-64'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-131'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-131'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-139'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-139'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -129,6 +131,24 @@ dependencies {
// animation
implementation "com.airbnb.android:lottie:3.4.0"
// Shopify
implementation('com.shopify.mobilebuysdk:buy3:12.0.0') {
exclude group: "com.shopify.graphql.support"
exclude module: 'joda-time'
}
// Google Pay
implementation 'com.google.android.gms:play-services-wallet:19.1.0'
//Compose
implementation 'androidx.activity:activity-compose:1.4.0'
implementation 'androidx.compose.material:material:1.1.0'
implementation 'androidx.compose.animation:animation:1.1.0'
implementation 'androidx.compose.ui:ui-tooling:1.1.0'
implementation "com.google.accompanist:accompanist-appcompat-theme:0.23.0"
implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'

View file

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

View file

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

View file

@ -39,8 +39,12 @@
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:fullBackupContent">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
<activity
android:exported="true"
android:name="com.tangem.tap.MainActivity"
android:launchMode="singleTask"
android:screenOrientation="portrait"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
[
{
"decimalCount": 18,
"customIconUrl": "https://cdn.worldvectorlogo.com/logos/usd-1.svg",
"symbol": "FUSD",
"name": "Fantom USD",
"contractAddress": "0x91ea991bd52EE3C40EdA2509701d905e1Ee54074"
},
{
"decimalCount": 18,
"customIconUrl": "https://cryptologos.cc/logos/fantom-ftm-logo.svg?v=003",
"symbol": "WFTM",
"name": "Wrapped Fantom",
"contractAddress": "0xf1277d1Ed8AD466beddF92ef448A132661956621"
}
]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

@ -1 +1 @@
Subproject commit 42803296690ad6914ec2c59c5fdc3f0fb30f8afc
Subproject commit 44ccf4e7766e51e869eabe8bc68026b5b7edd237

View file

@ -6,6 +6,7 @@ import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.snackbar.Snackbar
import com.tangem.TangemSdk
import com.tangem.operations.backup.BackupService
@ -18,10 +19,14 @@ import com.tangem.tap.common.redux.global.AndroidResources
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.shop.GooglePayService
import com.tangem.tap.common.shop.GooglePayService.Companion.LOAD_PAYMENT_DATA_REQUEST_CODE
import com.tangem.tap.common.shop.googlepay.GooglePayUtil.createPaymentsClient
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.shop.redux.ShopAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import com.tangem.wallet.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -59,6 +64,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
private var snackbar: Snackbar? = null
private val dialogManager = DialogManager()
private val intentHandler = IntentHandler()
private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -73,8 +79,14 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
store.dispatch(
ShopAction.CheckIfGooglePayAvailable(
GooglePayService(createPaymentsClient(this), this)
)
)
}
private fun getAndroidResources(): AndroidResources {
return AndroidResources(
AndroidResources.RString(
@ -92,12 +104,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
override fun onResume() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
notificationsHandler = NotificationsHandler(binding.fragmentContainer)
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
val shopOpened = store.state.shopState.total != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore && !shopOpened)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)
@ -128,7 +141,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
if (snackbar != null) return
snackbar = Snackbar.make(
fragment_container, getString(text), Snackbar.LENGTH_INDEFINITE
binding.fragmentContainer, getString(text), Snackbar.LENGTH_INDEFINITE
)
if (buttonTitle != null && action != null) {
snackbar?.setAction(getString(buttonTitle), action)
@ -140,4 +153,15 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
snackbar?.dismiss()
snackbar = null
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
LOAD_PAYMENT_DATA_REQUEST_CODE -> {
store.dispatch(
ShopAction.BuyWithGooglePay.HandleGooglePayResponse(resultCode, data)
)
}
}
}
}

View file

@ -12,6 +12,7 @@ import com.tangem.tap.common.images.PicassoHelper
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.shop.TangemShopService
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.config.FeaturesLocalLoader
import com.tangem.tap.domain.configurable.config.FeaturesRemoteLoader
@ -37,6 +38,7 @@ val store = Store(
lateinit var preferencesStorage: PreferencesStorage
lateinit var currenciesRepository: CurrenciesRepository
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
class TapApplication : Application() {
override fun onCreate() {
@ -72,7 +74,10 @@ class TapApplication : Application() {
val localLoader = FeaturesLocalLoader(this, moshi)
val remoteLoader = FeaturesRemoteLoader(moshi)
val configManager = ConfigManager(localLoader, remoteLoader)
configManager.load { store.dispatch(GlobalAction.SetConfigManager(configManager)) }
configManager.load {
store.dispatch(GlobalAction.SetConfigManager(configManager))
shopService = TangemShopService(this, configManager.config.shopify!!)
}
val warningsManager = WarningMessagesManager(RemoteWarningLoader(moshi))
warningsManager.load { store.dispatch(GlobalAction.SetWarningManager(warningsManager)) }
}

View file

@ -15,6 +15,7 @@ import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInPro
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -48,6 +49,8 @@ class DialogManager : StoreSubscriber<GlobalState> {
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.SimpleOkDialogRes -> SimpleOkDialog.create(state.dialog, context)
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)

View file

@ -5,7 +5,6 @@ import android.nfc.NfcAdapter
import android.nfc.Tag
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
@ -15,6 +14,11 @@ import timber.log.Timber
class IntentHandler {
private val TRANSACTION_ID_PARAM = "transactionId"
private val CURRENCY_CODE_PARAM = "baseCurrencyCode"
private val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
private val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
fun handleIntent(intent: Intent?) {
handleBackgroundScan(intent)
handleWalletConnectLink(intent)
@ -23,8 +27,8 @@ class IntentHandler {
private fun handleBackgroundScan(intent: Intent?) {
if (intent != null && (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action ||
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action
)
) {
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
if (tag != null) {
@ -45,13 +49,13 @@ class IntentHandler {
private fun handleSellCurrencyCallback(intent: Intent?) {
try {
val transactionID =
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
intent?.data?.getQueryParameter(TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
intent.data?.getQueryParameter(CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
intent.data?.getQueryParameter(CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
intent.data?.getQueryParameter(DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
@ -63,7 +67,7 @@ class IntentHandler {
transactionId = transactionID
))
} catch (exception: Exception) {
Timber.d("Not Moonpay URL")
Timber.d("Not MoonPay URL")
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.analytics
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.extensions.filterNotNull
@ -13,6 +14,11 @@ abstract class AnalyticsHandler {
params: Map<String, String> = emptyMap()
)
abstract fun triggerEvent(
event: String,
params: Map<String, String>
)
abstract fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
@ -71,6 +77,13 @@ abstract class AnalyticsHandler {
)
}
}
}
fun logShopifyOrder(order: Storefront.Order) {
triggerEvent(getOrderEvent(), getOrderParams(order))
}
protected abstract fun getOrderEvent(): String
protected abstract fun getOrderParams(order: Storefront.Order): Map<String, String>
}

View file

@ -1,7 +1,10 @@
package com.tangem.tap.common.analytics
import android.app.Application
import com.appsflyer.AFInAppEventParameterName
import com.appsflyer.AFInAppEventType
import com.appsflyer.AppsFlyerLib
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -30,4 +33,30 @@ class AppsFlyerAnalyticsHandler(val context: Application): AnalyticsHandler() {
//TODO
}
override fun getOrderEvent(): String = AFInAppEventType.PURCHASE
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
val discountParams = if (discountCode != null ){
mapOf(AFInAppEventParameterName.COUPON_CODE to discountCode)
} else {
mapOf()
}
return mapOf(
AFInAppEventParameterName.CONTENT_ID to sku,
AFInAppEventParameterName.REVENUE to order.totalPriceV2.amount,
AFInAppEventParameterName.CURRENCY to order.currencyCode.name
) + discountParams
}
override fun triggerEvent(event: String, params: Map<String, String>) {
AppsFlyerLib.getInstance().logEvent(context, event, params)
}
}

View file

@ -2,9 +2,11 @@ package com.tangem.tap.common.analytics
import android.os.Bundle
import androidx.core.os.bundleOf
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.ktx.Firebase
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -57,4 +59,30 @@ object FirebaseAnalyticsHandler : AnalyticsHandler() {
params.forEach { (key, value) -> FirebaseCrashlytics.getInstance().setCustomKey(key, value) }
FirebaseCrashlytics.getInstance().recordException(error)
}
override fun getOrderEvent(): String = FirebaseAnalytics.Event.PURCHASE
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
val sku = order.lineItems.edges.firstOrNull()?.node?.variant?.sku ?: "unknown"
val discountCode =
(order.discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication)?.code
val discountParams = if (discountCode != null ){
mapOf(FirebaseAnalytics.Param.DISCOUNT to discountCode)
} else {
mapOf()
}
return mapOf(
FirebaseAnalytics.Param.ITEM_ID to sku,
FirebaseAnalytics.Param.VALUE to order.totalPriceV2.amount,
FirebaseAnalytics.Param.CURRENCY to order.currencyCode.name
) + discountParams
}
override fun triggerEvent(event: String, params: Map<String, String>) {
Firebase.analytics.logEvent(event, params.toBundle())
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.analytics
import android.app.Application
import com.shopify.buy3.Storefront
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
@ -15,6 +16,10 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
analyticsHandlers.forEach { it.triggerEvent(event, card, blockchain) }
}
override fun triggerEvent(event: String, params: Map<String, String>) {
analyticsHandlers.forEach { it.triggerEvent(event, params) }
}
override fun logCardSdkError(
error: TangemSdkError,
actionToLog: Analytics.ActionToLog,
@ -28,6 +33,14 @@ class GlobalAnalyticsHandler(val analyticsHandlers: List<AnalyticsHandler>) :
analyticsHandlers.forEach { it.logError(error, params) }
}
override fun getOrderEvent(): String {
return ""
}
override fun getOrderParams(order: Storefront.Order): Map<String, String> {
return emptyMap()
}
companion object {
fun createDefaultAnalyticHandlers(context: Application): GlobalAnalyticsHandler {
return GlobalAnalyticsHandler(

View file

@ -19,6 +19,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCar
import com.tangem.tap.features.onboarding.products.twins.ui.TwinsCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -69,6 +70,7 @@ fun FragmentActivity.addOnBackPressedDispatcher(
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.Shop -> ShopFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()

View file

@ -1,12 +1,12 @@
package com.tangem.tap.common.extensions
import android.graphics.PorterDuff
import android.graphics.*
import android.net.Uri
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
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
@ -14,7 +14,7 @@ import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.wallet.R
fun Picasso.loadCurrenciesIcon(
imageView: ImageView,
imageView: ImageFilterView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain,
@ -42,6 +42,7 @@ fun Picasso.loadCurrenciesIcon(
setTokenImage(imageView, textView, token)
}
this.load(url)
.transform(RoundedCornersTransform())
.noPlaceholder()
?.into(imageView,
object : Callback {
@ -54,7 +55,7 @@ fun Picasso.loadCurrenciesIcon(
imageView.colorFilter = null
textView.text = null
}
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
if (blockchain.isTestnet()) imageView.saturation = 0f
}
})
}
@ -68,42 +69,64 @@ private const val QCX = "QCX"
private const val VOYR = "VOYRME"
private fun setOfflineCurrencyImage(
imageView: ImageView,
imageView: ImageFilterView,
textView: TextView,
token: Token?,
blockchain: Blockchain,
) {
if (token != null) {
setTokenImage(imageView, textView, token)
} else {
setBlockchainImage(imageView, textView, blockchain)
when (token) {
null -> setBlockchainImage(imageView, textView, blockchain)
else -> setTokenImage(imageView, textView, token)
}
if (blockchain.isTestnet()) imageView.tint(R.color.tint)
}
fun ImageView.tint(colorRes: Int) {
// val color = ContextCompat.getColor(context, colorRes);
// ImageViewCompat.setImageTintList(this, ColorStateList.valueOf(color));
this.setColorFilter(ContextCompat.getColor(context, colorRes), PorterDuff.Mode.DARKEN);
}
private fun setBlockchainImage(
imageView: ImageView,
imageView: ImageFilterView,
textView: TextView,
blockchain: Blockchain,
) {
imageView.setImageResource(blockchain.getIconRes())
imageView.colorFilter = null
if (blockchain.isTestnet()) imageView.saturation = 0f
textView.text = null
}
private fun setTokenImage(
imageView: ImageView,
imageView: ImageFilterView,
textView: TextView,
token: Token,
) {
imageView.setImageResource(R.drawable.shape_circle)
imageView.setColorFilter(token.getColor())
if (token.blockchain.isTestnet()) {
imageView.saturation = 0f
} else {
imageView.setColorFilter(token.getColor())
}
textView.text = token.symbol.take(1)
}
private class RoundedCornersTransform : Transformation {
override fun transform(source: Bitmap): Bitmap {
val size = source.width.coerceAtMost(source.height)
val x = (source.width - size) / 2
val y = (source.height - size) / 2
val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
if (squaredBitmap != source) source.recycle()
val paint = Paint().apply {
shader = BitmapShader(squaredBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
isAntiAlias = true
}
val rectF = RectF(0f, 0f, source.width.toFloat(), source.height.toFloat())
val radius = size / 8f
val bitmap = Bitmap.createBitmap(size, size, source.config)
Canvas(bitmap).drawRoundRect(rectF, radius, radius, paint)
squaredBitmap.recycle()
return bitmap
}
override fun key(): String = "rounded_corners"
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.extensions
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.text.Spannable
import android.text.style.ForegroundColorSpan
import androidx.core.content.ContextCompat
@ -21,24 +22,24 @@ fun String?.ellipsizeBeforeSpace(allowedSize: Int): String {
val startIndex = endIndex - sizeDifference
val newString = this.removeRange(startIndex, endIndex)
return newString.substring(0 until startIndex) + "..." +
newString.substring(startIndex until newString.length)
newString.substring(startIndex until newString.length)
}
fun String.colorSegment(
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
context: Context,
color: Int,
startIndex: Int = 0,
endIndex: Int = this.length
): Spannable {
return this.toSpannable()
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
.also { spannable ->
spannable.setSpan(
ForegroundColorSpan(ContextCompat.getColor(context, color)),
startIndex,
endIndex,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
fun String.toQrCode(): Bitmap {
@ -59,4 +60,6 @@ fun String.toQrCode(): Bitmap {
}
}
return bmp
}
}
fun String.urlEncode(): String = Uri.encode(this)

View file

@ -3,6 +3,20 @@ package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
fun StringBuilder.appendIf(value: String, predicate: (String) -> Boolean) {
fun StringBuilder.appendIf(value: String, predicate: (String) -> Boolean): StringBuilder {
if (predicate(value)) this.append(value)
return this
}
fun StringBuilder.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): StringBuilder {
if (value == null) return this
prefix?.let { append(it) }
append(value)
postfix?.let { append(it) }
return this
}
fun String.appendIfNotNull(value: String?, prefix: String? = null, postfix: String? = null): String {
return StringBuilder(this).apply { appendIfNotNull(value, prefix, postfix) }.toString()
}

View file

@ -37,10 +37,19 @@ fun Context.getColorCompat(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(this, colorRes)
}
@ColorInt
fun View.getColor(@ColorRes colorRes: Int): Int {
return ContextCompat.getColor(context, colorRes)
}
fun View.getString(@StringRes id: Int): String {
return context.getString(id)
}
fun View.getString(@StringRes id: Int, vararg formatArgs: String): String {
return context.getString(id, *formatArgs)
}
fun View.getResourceName(): String {
return try {
resources.getResourceEntryName(id)
@ -68,9 +77,16 @@ fun View.hide(invokeBeforeStateChanged: (() -> Unit)? = null) {
this.visibility = View.GONE
}
fun View.makeInvisible() {
if (this.visibility == View.INVISIBLE) return
this.visibility = View.INVISIBLE
fun View.invisible(invisible: Boolean = true, invokeBeforeStateChanged: (() -> Unit)? = null) {
if (invisible) {
if (this.visibility == View.INVISIBLE) return
invokeBeforeStateChanged?.invoke()
this.visibility = View.INVISIBLE
} else {
this.show(invokeBeforeStateChanged)
}
}
fun Context.dpToPixels(dp: Int): Int =
@ -158,4 +174,8 @@ fun Context.safeStartActivity(
} finally {
finally?.invoke()
}
}
fun View.getString(resId: Int, vararg formatArgs: Any?): String {
return context.getString(resId, formatArgs)
}

View file

@ -29,6 +29,10 @@ fun ViewGroup.beginDelayedTransition(transition: Transition = AutoTransition())
TransitionManager.beginDelayedTransition(this, transition)
}
fun View.beginDelayedTransition(transition: Transition = AutoTransition()) {
(this as? ViewGroup)?.beginDelayedTransition(transition)
}
fun ChipGroup.fitChipsByGroupWidth() {
val layoutStateHandler = GlobalLayoutStateHandler(this)
layoutStateHandler.onStateChanged = stateHandler@{

View file

@ -7,19 +7,29 @@ import com.tangem.common.services.Result
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.reducers.createAddressesData
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.store
import kotlinx.coroutines.delay
import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
update()
Result.Success(wallet)
if (isDemoWallet()) {
delay(500)
Result.Success(wallet)
} else {
update()
Result.Success(wallet)
}
} catch (exception: Exception) {
Timber.e(exception)
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
Result.Failure(TapError.NoInternetConnection)
} else {
@ -36,17 +46,17 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
}
fun WalletManager?.getToUpUrl(): String? {
val globalState = store.state.globalState
val currencyExchangeManager = globalState.currencyExchangeManager ?: return null
val wallet = this?.wallet ?: return null
val config = store.state.globalState.configManager?.config ?: return null
val defaultAddress = wallet.address
return TradeCryptoHelper.getUrl(
TradeCryptoHelper.Action.Buy,
wallet.blockchain,
wallet.blockchain.currency,
defaultAddress,
config.moonPayApiKey,
config.moonPayApiSecretKey
val defaultAddress = wallet.address
return currencyExchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,
fiatCurrency = globalState.appCurrency,
walletAddress = defaultAddress,
)
}

View file

@ -5,15 +5,18 @@ import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidgetState
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.test_leapfrog_fragment.*
import com.tangem.wallet.databinding.TestLeapfrogFragmentBinding
class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) {
private lateinit var twinsCardWidget: TwinsCardWidget
private val binding: TestLeapfrogFragmentBinding by viewBinding(TestLeapfrogFragmentBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -28,30 +31,29 @@ class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) {
val leapfrog = LeapfrogWidget(leapfrogContainer)
twinsCardWidget = TwinsCardWidget(leapfrog) { 200f }
btn_twin_welcome.setOnClickListener {
binding.btnTwinWelcome.setOnClickListener {
twinsCardWidget.toWelcome()
}
btn_twin_to_leapfrog.setOnClickListener {
binding.btnTwinToLeapfrog.setOnClickListener {
twinsCardWidget.toLeapfrog()
}
btn_twin_activate.setOnClickListener {
binding.btnTwinActivate.setOnClickListener {
twinsCardWidget.toActivate()
}
btn_lp_init.setOnClickListener {
binding.btnLpInit.setOnClickListener {
leapfrog.initViews()
}
btn_lp_unfold.setOnClickListener {
binding.btnLpUnfold.setOnClickListener {
leapfrog.unfold()
}
btn_lp_fold.setOnClickListener {
binding.btnLpFold.setOnClickListener {
leapfrog.fold()
}
btn_lp_leap.setOnClickListener {
binding.btnLpLeap.setOnClickListener {
leapfrog.leap()
}
btn_lp_leap_back.setOnClickListener {
binding.btnLpLeapBack.setOnClickListener {
leapfrog.leapBack()
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.shop.redux.ShopReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
import org.rekotlin.Action
@ -32,7 +33,8 @@ fun appReducer(action: Action, state: AppState?): AppState {
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState),
shopState = ShopReducer.reduce(action, state.shopState),
)
}

View file

@ -23,6 +23,8 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWallet
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.redux.middlewares.SendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.shop.redux.ShopMiddleware
import com.tangem.tap.features.shop.redux.ShopState
import com.tangem.tap.features.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
import com.tangem.tap.features.wallet.redux.WalletState
@ -44,6 +46,7 @@ data class AppState(
val disclaimerState: DisclaimerState = DisclaimerState(),
val tokensState: TokensState = TokensState(),
val walletConnectState: WalletConnectState = WalletConnectState(),
val shopState: ShopState = ShopState(),
) : StateType {
companion object {
@ -62,7 +65,8 @@ data class AppState(
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
@ -9,6 +10,8 @@ import com.tangem.tap.features.wallet.redux.Currency
interface StateDialog
sealed class AppDialog : StateDialog {
data class SimpleOkDialog(val header: String, val message: String, val onOk: VoidCallback? = null) : AppDialog()
data class SimpleOkDialogRes(val headerId: Int, val messageId: Int, val onOk: VoidCallback? = null) : AppDialog()
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,

View file

@ -14,7 +14,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
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.moonpay.MoonpayStatus
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -76,7 +76,7 @@ sealed class GlobalAction : Action {
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object GetMoonPayStatus : GlobalAction() {
data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction()
object InitCurrencyExchangeManager : GlobalAction() {
data class Success(val exchangeManager: CurrencyExchangeManager) : GlobalAction()
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.common.extensions.ifNotNull
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -12,10 +12,11 @@ import com.tangem.tap.common.redux.AppState
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.moonpay.MoonpayService
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.moonpay.MoonPayService
import com.tangem.tap.network.exchangeServices.onramper.OnramperService
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
class GlobalMiddleware {
companion object {
@ -69,19 +70,21 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
is GlobalAction.InitCurrencyExchangeManager -> {
val config = appState()?.globalState?.configManager?.config
ifNotNull(
config?.onramperApiKey,
config?.moonPayApiKey,
config?.moonPayApiSecretKey,
) { onramperKey, moonPayKey, moonPaySecretKey ->
scope.launch {
val result = MoonpayService().getMoonpayStatus(apiKey)
when (result) {
is Result.Success -> {
store.dispatchOnMain(GlobalAction.GetMoonPayStatus.Success(result.data))
}
is Result.Failure -> Timber.e(result.error)
}
val onramper = OnramperService(onramperKey)
val moonPay = MoonPayService(moonPayKey, moonPaySecretKey)
val exchangeManager = CurrencyExchangeManager(onramper, moonPay)
exchangeManager.getStatus()
store.dispatchOnMain(GlobalAction.InitCurrencyExchangeManager.Success(exchangeManager))
}
}
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
@ -19,7 +18,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.Onboarding.Start -> {
val onboardingManager = if (action.scanResponse != null) {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
OnboardingManager(action.scanResponse, usedCardsPrefStorage)
} else {
null
}
@ -67,19 +66,8 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayStatus.Success -> {
val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false
val moonpayStatus = if (fiatExchangeIsEnabled) {
action.moonPayStatus
} else {
MoonpayStatus(
isBuyAllowed = false,
isSellAllowed = false,
availableToBuy = emptyList(),
availableToSell = emptyList()
)
}
globalState.copy(moonpayStatus = moonpayStatus)
is GlobalAction.InitCurrencyExchangeManager.Success -> {
globalState.copy(currencyExchangeManager = action.exchangeManager)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)

View file

@ -11,7 +11,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import org.rekotlin.StateType
data class GlobalState(
@ -27,7 +27,7 @@ data class GlobalState(
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonpayStatus: MoonpayStatus? = null,
val currencyExchangeManager: CurrencyExchangeManager? = null,
val resources: AndroidResources = AndroidResources(),
val analyticsHandlers: AnalyticsHandler? = null,
) : StateType

View file

@ -11,6 +11,7 @@ data class NavigationState(
enum class AppScreen {
Home,
Shop,
Disclaimer,
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,

View file

@ -0,0 +1,230 @@
package com.tangem.tap.common.shop
import android.app.Application
import android.content.Intent
import com.google.android.gms.wallet.PaymentData
import com.shopify.buy3.Storefront
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.shop.data.ProductType
import com.tangem.tap.common.shop.data.TangemProduct
import com.tangem.tap.common.shop.data.TotalSum
import com.tangem.tap.common.shop.shopify.ShopifyService
import com.tangem.tap.common.shop.shopify.ShopifyShop
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import java.math.BigDecimal
import java.util.*
class TangemShopService(application: Application, shopifyShop: ShopifyShop) {
private val shopifyService = ShopifyService(application, shopifyShop)
private lateinit var product: Storefront.Product
private val checkouts = mutableMapOf<ProductType, Storefront.Checkout>()
private val variants = mutableMapOf<ProductType, Storefront.ProductVariant>()
private lateinit var googlePayService: GooglePayService
suspend fun getProducts(): Result<List<TangemProduct>> {
val result = shopifyService.getProducts()
result.onSuccess {
product = it.first {
val variantsSku = it.variants.edges.map { it.node.sku }
variantsSku.contains(ProductType.WALLET_2_CARDS.sku) && variantsSku.contains(
ProductType.WALLET_3_CARDS.sku
)
}
product.variants.edges.map { it.node }
.forEach { variant ->
if (variant.sku == ProductType.WALLET_2_CARDS.sku) {
variants[ProductType.WALLET_2_CARDS] = variant
} else if (variant.sku == ProductType.WALLET_3_CARDS.sku) {
variants[ProductType.WALLET_3_CARDS] = variant
}
}
val twoCardsProduct = TangemProduct(
type = ProductType.WALLET_2_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_2_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_2_CARDS]?.compareAtPriceV2?.format()
)
)
val threeCardsProduct = TangemProduct(
type = ProductType.WALLET_3_CARDS,
totalSum = TotalSum(
finalValue = variants[ProductType.WALLET_3_CARDS]?.priceV2?.format(),
beforeDiscount = variants[ProductType.WALLET_3_CARDS]?.compareAtPriceV2?.format()
)
)
createCheckouts()
return Result.success(listOf(twoCardsProduct, threeCardsProduct))
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun createCheckouts() {
variants.keys.map { coroutineScope { async { createCheckout(it) } } }.awaitAll()
}
private suspend fun createCheckout(productType: ProductType) {
val checkoutItem = CheckoutItem(variants[productType]!!.id, 1)
val result = shopifyService.createCheckout(listOf(checkoutItem))
result.onSuccess { checkout ->
checkouts[productType] = checkout
}
}
suspend fun checkIfGooglePayAvailable(googlePayService: GooglePayService): Result<Boolean> {
this.googlePayService = googlePayService
return googlePayService.checkIfGooglePayAvailable()
}
fun buyWithGooglePay(productType: ProductType) {
val totalPrice = checkouts[productType]!!.totalPriceV2.amount
googlePayService.payWithGooglePay(
totalPriceCents = totalPrice, currencyCode = checkouts[productType]!!.currencyCode.name,
merchantID = shopifyService.shop.merchantID
)
}
// fun subscribeToGooglePayResult(
// productType: ProductType,
// resultCallback: (Result<PaymentData>) -> Unit
// ) {
// googlePayService.responseCallback = { result ->
// result.onFailure { }
// result.onSuccess {
// completeTokenizedPayment(it, productType)
// }
// }
// }
suspend fun handleGooglePayResult(
resultCode: Int,
data: Intent?,
productType: ProductType
): Result<Unit> {
val result = googlePayService.handleResponseFromGooglePay(resultCode, data)
result.onSuccess {
val finalizePaymentResult = completeTokenizedPayment(it, productType)
finalizePaymentResult.onSuccess {
return Result.success(Unit)
}
return Result.failure(finalizePaymentResult.exceptionOrNull()!!)
}
return Result.failure(result.exceptionOrNull()!!)
}
private suspend fun completeTokenizedPayment(
paymentData: PaymentData,
productType: ProductType
): Result<Storefront.Checkout> {
val checkout = checkouts[productType]!!
val googlePayResponse =
googlePayService.parsePaymentData(paymentData)
?: return Result.failure(Exception("cannot parse GPay result"))
val amount =
Storefront.MoneyInput(checkout.totalPriceV2.amount, checkout.totalPriceV2.currencyCode)
val idempotencyKey = UUID.randomUUID().toString()
val addressGPay = googlePayResponse.billingAddress
val address = Storefront.MailingAddressInput().apply {
lastName = addressGPay.name
address1 = addressGPay.address1
address2 = addressGPay.address2 + addressGPay.address3
province = addressGPay.administrativeArea
zip = addressGPay.postalCode
phone = addressGPay.phoneNumber
}
val payment = Storefront.TokenizedPaymentInputV3(
amount,
idempotencyKey,
address,
paymentData.toJson(),
Storefront.PaymentTokenType.GOOGLE_PAY
)
.setTest(true)
return shopifyService.completeWithTokenizedPayment(
payment = payment,
checkoutID = checkout.id
)
}
suspend fun applyPromoCode(promoCode: String): Result<List<TangemProduct>> {
val products = variants.keys
.map { coroutineScope { async { applyPromoCode(promoCode, it) } } }
.awaitAll()
.map { result -> result.getOrElse { return Result.failure(it) } }
return Result.success(products)
}
suspend fun applyPromoCode(promoCode: String, productType: ProductType): Result<TangemProduct> {
val checkout = checkouts[productType] ?: return Result.failure(Exception("No checkout"))
val result = if (promoCode.isBlank()) {
shopifyService.removeDiscount(checkout.id)
} else {
shopifyService.applyDiscount(promoCode, checkout.id)
}
result.onSuccess {
checkouts[productType] = it
return Result.success(
TangemProduct(
productType,
TotalSum(
finalValue = it.totalPriceV2.format(),
beforeDiscount = variants[productType]!!.compareAtPriceV2.format()
),
appliedDiscount = it.getAppliedDiscount()
)
)
}
return Result.failure(result.exceptionOrNull()!!)
}
fun getCheckoutUrl(productType: ProductType): String {
return checkouts[productType]!!.webUrl
}
suspend fun waitForCheckout(
productType: ProductType,
analyticsHandler: AnalyticsHandler?
) {
val result = shopifyService.checkout(true, checkouts[productType]!!.id)
result.onSuccess {
if (it.order != null) {
analyticsHandler?.logShopifyOrder(order = it.order)
}
}
}
companion object {
const val TANGEM_WALLET_2_CARDS_SKU = "TG115x2"
const val TANGEM_WALLET_3_CARDS_SKU = "TG115x3"
}
}
private fun Storefront.MoneyV2.format(): String {
val currencySymbol = Currency.getInstance(currencyCode.name).symbol
val amountFormatted = BigDecimal(amount).setScale(2)
return currencySymbol + amountFormatted
}
private fun Storefront.Checkout.getAppliedDiscount(): String? {
val discountApplication =
discountApplications.edges.firstOrNull()?.node as? Storefront.DiscountCodeApplication
return discountApplication?.code
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.shop.data
import com.tangem.tap.common.shop.TangemShopService
enum class ProductType(val sku: String) {
WALLET_2_CARDS(TangemShopService.TANGEM_WALLET_2_CARDS_SKU),
WALLET_3_CARDS(TangemShopService.TANGEM_WALLET_3_CARDS_SKU)
}

View file

@ -0,0 +1,7 @@
package com.tangem.tap.common.shop.data
data class TangemProduct(
val type: ProductType,
val totalSum: TotalSum? = null,
val appliedDiscount: String? = null
)

View file

@ -0,0 +1,6 @@
package com.tangem.tap.common.shop.data
data class TotalSum(
val finalValue: String? = null,
val beforeDiscount: String? = null,
)

View file

@ -0,0 +1,148 @@
package com.tangem.tap.common.shop
import android.app.Activity
import android.app.Activity.RESULT_CANCELED
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.util.Log
import com.google.android.gms.common.api.ApiException
import com.google.android.gms.wallet.*
import com.tangem.common.core.TangemSdkError
import com.tangem.tap.common.shop.googlepay.GooglePayUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import timber.log.Timber
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class GooglePayService(private val paymentsClient: PaymentsClient, private val activity: Activity) {
// var responseCallback: ((Result<PaymentData>) -> Unit)? = null
suspend fun checkIfGooglePayAvailable(): Result<Boolean> {
val isReadyToPayJson = GooglePayUtil.isReadyToPayRequest() ?: return Result.success(false)
val request = IsReadyToPayRequest.fromJson(isReadyToPayJson.toString())
val task = paymentsClient.isReadyToPay(request)
return withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
task.addOnCompleteListener { completedTask ->
try {
val result = completedTask.getResult(ApiException::class.java)
continuation.resume(Result.success(true))
} catch (exception: ApiException) {
// Process error
Timber.w("isReadyToPay failed: $exception")
continuation.resume(Result.failure(exception))
}
}
}
}
}
fun payWithGooglePay(totalPriceCents: String, currencyCode: String, merchantID: String) {
val paymentDataRequestJson = GooglePayUtil.getPaymentDataRequest(
totalPriceCents,
currencyCode = currencyCode,
countryCode = "RU",
merchantID = merchantID
)
if (paymentDataRequestJson == null) {
Timber.e("RequestPayment: can't fetch payment data request")
return
}
val request = PaymentDataRequest.fromJson(paymentDataRequestJson.toString())
AutoResolveHelper.resolveTask(
paymentsClient.loadPaymentData(request), activity, LOAD_PAYMENT_DATA_REQUEST_CODE
)
}
fun handleResponseFromGooglePay(resultCode: Int, data: Intent?): Result<PaymentData> {
val result = when (resultCode) {
RESULT_OK -> {
val paymentData = data?.let { intent -> PaymentData.getFromIntent(intent) }
if (paymentData == null) {
Result.failure(Exception("No payment data"))
} else {
Result.success(paymentData)
}
}
RESULT_CANCELED -> {
Result.failure(TangemSdkError.UserCancelled())
}
AutoResolveHelper.RESULT_ERROR -> {
val statusCode = AutoResolveHelper.getStatusFromIntent(data)?.statusCode
if (statusCode == null) {
Result.failure(Exception("Unknown Status"))
} else {
Result.failure(Exception("$statusCode"))
}
}
else -> Result.failure(Exception("Unknown Status"))
}
// responseCallback?.invoke(result)
return result
}
fun parsePaymentData(paymentData: PaymentData): GooglePayResponse? {
val paymentInformation = paymentData.toJson()
try {
// Token will be null if PaymentDataRequest was not constructed using fromJson(String).
val paymentMethodData =
JSONObject(paymentInformation).getJSONObject("paymentMethodData")
val addressJson = paymentMethodData.getJSONObject("info")
.getJSONObject("billingAddress")
val address = Address(
name = addressJson.getString("name"),
postalCode = addressJson.getString("postalCode"),
countryCode = addressJson.getString("countryCode"),
phoneNumber = addressJson.getString("phoneNumber"),
address1 = addressJson.getString("address1"),
address2 = addressJson.getString("address2"),
address3 = addressJson.getString("address3"),
locality = addressJson.getString("locality"),
administrativeArea = addressJson.getString("administrativeArea"),
sortingCode = addressJson.getString("sortingCode"),
)
val token = paymentMethodData
.getJSONObject("tokenizationData")
.getString("token")
return GooglePayResponse(address, token)
} catch (e: JSONException) {
Log.e("handlePaymentSuccess", "Error: " + e.toString())
}
return null
}
companion object {
const val LOAD_PAYMENT_DATA_REQUEST_CODE = 315
}
}
data class GooglePayResponse(
val billingAddress: Address,
val token: String,
)
data class Address(
val name: String,
val postalCode: String,
val countryCode: String,
val phoneNumber: String,
val address1: String,
val address2: String,
val address3: String,
val locality: String,
val administrativeArea: String,
val sortingCode: String
)

View file

@ -0,0 +1,137 @@
package com.tangem.tap.common.shop.googlepay
import android.app.Activity
import com.google.android.gms.wallet.PaymentsClient
import com.google.android.gms.wallet.Wallet
import com.google.android.gms.wallet.WalletConstants
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
object GooglePayUtil {
private val baseRequest = JSONObject().apply {
put("apiVersion", 2)
put("apiVersionMinor", 0)
}
private fun gatewayTokenizationSpecification(merchantID: String): JSONObject {
return JSONObject().apply {
put("type", "PAYMENT_GATEWAY")
put(
"parameters", JSONObject(
mapOf(
"gateway" to "shopify",
"gatewayMerchantId" to merchantID
)
)
)
}
}
private val allowedCardNetworks = JSONArray(
listOf(
"AMEX",
"DISCOVER",
"INTERAC",
"JCB",
"MASTERCARD",
"VISA"
)
)
private val allowedCardAuthMethods = JSONArray(
listOf(
"PAN_ONLY",
"CRYPTOGRAM_3DS"
)
)
private fun baseCardPaymentMethod(): JSONObject {
return JSONObject().apply {
val parameters = JSONObject().apply {
put("allowedAuthMethods", allowedCardAuthMethods)
put("allowedCardNetworks", allowedCardNetworks)
put("billingAddressRequired", true)
put("billingAddressParameters", JSONObject().apply {
put("format", "FULL")
})
}
put("type", "CARD")
put("parameters", parameters)
}
}
private fun cardPaymentMethod(merchantID: String): JSONObject {
val cardPaymentMethod = baseCardPaymentMethod()
cardPaymentMethod.put("tokenizationSpecification", gatewayTokenizationSpecification(merchantID))
return cardPaymentMethod
}
fun createPaymentsClient(activity: Activity): PaymentsClient {
val walletOptions = Wallet.WalletOptions.Builder()
.setEnvironment(PAYMENTS_ENVIRONMENT)
.build()
return Wallet.getPaymentsClient(activity, walletOptions)
}
fun isReadyToPayRequest(): JSONObject? {
return try {
baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(baseCardPaymentMethod()))
}
} catch (e: JSONException) {
null
}
}
private fun getTransactionInfo(
price: String,
countryCode: String,
currencyCode: String
): JSONObject {
return JSONObject().apply {
put("totalPrice", price)
put("totalPriceStatus", "FINAL")
put("countryCode", countryCode)
put("currencyCode", currencyCode)
}
}
private val merchantInfo: JSONObject =
JSONObject().put("merchantName", "Example Merchant")
fun getPaymentDataRequest(
price: String,
countryCode: String,
currencyCode: String,
merchantID: String
): JSONObject? {
try {
return baseRequest.apply {
put("allowedPaymentMethods", JSONArray().put(cardPaymentMethod(merchantID)))
put("transactionInfo", getTransactionInfo(price, countryCode, currencyCode))
put("merchantInfo", merchantInfo)
// An optional shipping address requirement is a top-level property of the
// PaymentDataRequest JSON object.
val shippingAddressParameters = JSONObject().apply {
put("phoneNumberRequired", false)
// put("allowedCountryCodes", JSONArray(listOf("US", "GB")))
}
put("shippingAddressParameters", shippingAddressParameters)
put("shippingAddressRequired", true)
}
} catch (e: JSONException) {
return null
}
}
}
const val PAYMENTS_ENVIRONMENT = WalletConstants.ENVIRONMENT_TEST

View file

@ -0,0 +1,383 @@
package com.tangem.tap.common.shop.shopify
import android.app.Application
import com.shopify.buy3.GraphCallResult
import com.shopify.buy3.GraphClient
import com.shopify.buy3.RetryHandler
import com.shopify.buy3.Storefront.*
import com.shopify.graphql.support.ID
import com.shopify.graphql.support.Input
import com.tangem.tap.common.shop.shopify.data.CheckoutItem
import com.tangem.tap.common.shop.shopify.data.checkoutFieldsFragment
import com.tangem.tap.common.shop.shopify.data.collectionFieldsFragment
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class ShopifyService(private val application: Application, val shop: ShopifyShop) {
val client: GraphClient by lazy { initClient() }
suspend fun getShopName(): Result<String> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.shop { shopQuery: ShopQuery ->
shopQuery
.name()
}
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val name = result.response.data!!.shop.name
Result.success(name)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun getProducts(collectionTitleFilter: String? = null): Result<List<Product>> {
val filter = collectionTitleFilter?.let { "title:\"$it\"" }
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.collections(
{ arg -> arg.first(250).query(filter) },
) { collectionConnectionQuery ->
collectionConnectionQuery.collectionFieldsFragment()
}
}
return when (val result = queryAsync(query)) {
is GraphCallResult.Success -> {
val products = result.response.data!!.collections.edges
.map { it.node.products }
.flatMap { it.edges }
.map { it.node }
Result.success(products)
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun checkout(pollUntilOrder: Boolean, checkoutID: ID): Result<Checkout> {
val query = query { rootQuery: QueryRootQuery ->
rootQuery
.node(checkoutID) { query ->
query.onCheckout { checkoutQuery ->
with(checkoutQuery) {
checkoutFieldsFragment()
}
}
}
}
val retryHandler = RetryHandler.build<QueryRoot>(
1, TimeUnit.SECONDS
) {
this.retryWhen { result ->
when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data?.node as? Checkout
checkout?.order == null
}
is GraphCallResult.Failure -> false
}
}
}
val result = if (pollUntilOrder) queryAsync(query, retryHandler) else queryAsync(query)
return when (result) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.node as? Checkout
if (checkout != null) {
Result.success(checkout)
} else {
Result.failure(ShopifyError.Unknown)
}
}
is GraphCallResult.Failure -> {
Result.failure(result.error)
}
}
}
suspend fun createCheckout(
checkoutItems: List<CheckoutItem>,
checkoutID: ID? = null
): Result<Checkout> {
val storefrontLineItems: MutableList<CheckoutLineItemInput> = checkoutItems
.map { CheckoutLineItemInput(it.quantity, it.id) }.toMutableList()
val query = if (checkoutID != null) {
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutLineItemsReplace(
storefrontLineItems, checkoutID
) { payloadQuery: CheckoutLineItemsReplacePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.userErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
} else {
val input = CheckoutCreateInput()
.setLineItemsInput(
Input.value(storefrontLineItems)
)
mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCreate(
input
) { payloadQuery: CheckoutCreatePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
}
return runCheckoutMutation(query)
}
suspend fun applyDiscount(discountCode: String, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeApplyV2(
discountCode, checkoutID
) { payloadQuery: CheckoutDiscountCodeApplyV2PayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun removeDiscount(checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutDiscountCodeRemove(
checkoutID
) { payloadQuery: CheckoutDiscountCodeRemovePayloadQuery ->
payloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateAddress(
address: MailingAddress,
checkoutID: ID,
waitForShippingRates: Boolean
): Result<Checkout> {
val input = MailingAddressInput()
.setAddress1(address.address1)
.setAddress2(address.address2)
.setCity(address.city)
.setCountry(address.country)
.setFirstName(address.firstName)
.setLastName(address.lastName)
.setPhone(address.phone)
.setProvince(address.province)
.setZip(address.zip)
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutShippingAddressUpdateV2(
input, checkoutID
) { shippingAddressUpdatePayloadQuery: CheckoutShippingAddressUpdateV2PayloadQuery ->
shippingAddressUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateEmail(email: String?, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutEmailUpdateV2(
checkoutID, email
) { emailUpdatePayloadQuery: CheckoutEmailUpdateV2PayloadQuery ->
emailUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun updateShippingRate(handle: String?, checkoutID: ID): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutShippingLineUpdate(
checkoutID, handle
) { shippingLineUpdatePayloadQuery: CheckoutShippingLineUpdatePayloadQuery ->
shippingLineUpdatePayloadQuery
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery.checkoutFieldsFragment()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
suspend fun completeWithTokenizedPayment(
payment: TokenizedPaymentInputV3,
checkoutID: ID
): Result<Checkout> {
val query = mutation { mutationQuery: MutationQuery ->
mutationQuery
.checkoutCompleteWithTokenizedPaymentV3(
checkoutID, payment
) { payloadQuery: CheckoutCompleteWithTokenizedPaymentV3PayloadQuery ->
payloadQuery
.payment { paymentQuery: PaymentQuery ->
paymentQuery
.ready()
.errorMessage()
}
.checkout { checkoutQuery: CheckoutQuery ->
checkoutQuery
.ready()
}
.checkoutUserErrors() { userErrorQuery: CheckoutUserErrorQuery ->
userErrorQuery
.field()
.message()
}
}
}
return runCheckoutMutation(query)
}
fun startGooglePaySession() {
// PaySession()
}
private suspend fun runCheckoutMutation(mutation: MutationQuery): Result<Checkout> {
return when (val result = mutationQueryAsync(mutation)) {
is GraphCallResult.Success -> {
val checkout = result.response.data!!.checkoutCreate?.checkout
?: result.response.data!!.checkoutDiscountCodeApplyV2?.checkout
?: result.response.data!!.checkoutDiscountCodeRemove?.checkout
?: result.response.data!!.checkoutShippingAddressUpdateV2?.checkout
?: result.response.data!!.checkoutEmailUpdateV2?.checkout
?: result.response.data!!.checkoutShippingLineUpdate?.checkout
?: result.response.data!!.checkoutCompleteWithTokenizedPaymentV3.checkout
Result.success(checkout)
}
is GraphCallResult.Failure -> Result.failure(result.error)
}
}
private suspend fun queryAsync(
query: QueryRootQuery,
retryHandler: RetryHandler<QueryRoot>
): GraphCallResult<QueryRoot> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue(retryHandler = retryHandler) { result ->
continuation.resume(result)
}
}
}
private suspend fun queryAsync(
query: QueryRootQuery,
): GraphCallResult<QueryRoot> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.queryGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private suspend fun mutationQueryAsync(query: MutationQuery): GraphCallResult<Mutation> =
withContext(Dispatchers.IO) {
suspendCoroutine { continuation ->
client.mutateGraph(query).enqueue { result ->
continuation.resume(result)
}
}
}
private fun initClient(): GraphClient {
return GraphClient.build(
application,
shop.domain,
shop.storefrontApiKey
) {
// httpCache(application.filesDir) {
// cacheMaxSizeBytes = (1024 * 1024 * 10)
// defaultCachePolicy =
// HttpCachePolicy.Default.CACHE_FIRST.expireAfter(20, TimeUnit.MINUTES)
// }
}
}
}
sealed class ShopifyError : Throwable() {
object Unknown : ShopifyError()
object GooglePayFailed : ShopifyError()
class UserError(val errorMessage: String)
}

View file

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

View file

@ -0,0 +1,134 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
fun Storefront.CheckoutQuery.checkoutFieldsFragment() {
// id()
ready()
webUrl()
currencyCode()
lineItemsSubtotalPrice { it.amount() }
totalPriceV2 {
it.currencyCode()
it.amount() }
lineItems({ arg -> arg.first(250) }) {
it.edges {
it.node {
// it.id()
it.title()
it.quantity()
it.variant() {
it.priceV2() { it.amount() }
}
}
}
}
shippingLine {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
availableShippingRates {
it.ready()
it.shippingRates {
it.handle()
it.title()
it.priceV2 { it.amount() }
}
}
shippingAddress {
it.address1()
it.address2()
it.city()
// .company()
it.country()
// .countryCodeV2()
it.firstName()
// .formatted()
// .formattedArea()
// .id()
it.lastName()
// .latitude()
// .longitude()
// .name()
it.phone()
it.province()
// .provinceCode()
it.zip()
}
discountApplications({ arg -> arg.first(250) }) {
it.edges {
it.node {
it.onDiscountCodeApplication {
it.code()
// .applicable()
// .allocationMethod()
// .targetSelection()
// .targetType()
it.value {
it.onMoneyV2 {
it.amount()
}
it.onPricingPercentageValue {
it.percentage()
}
}
}
}
}
}
order {
it.cancelReason()
it.canceledAt()
it.currencyCode()
// .currentSubtotalPrice()
// .currentTotalDuties()
// .currentTotalPrice()
// .currentTotalTax()
it.customerLocale()
it.customerUrl()
// .discountApplications()
it.edited()
it.email()
it.financialStatus()
it.fulfillmentStatus()
// .id()
// .lineItems()
// .metafield()
// .metafields()
it.name()
it.orderNumber()
// .originalTotalDuties()
// .originalTotalPrice()
it.phone()
it.processedAt()
it.shippingAddress {
it.address1()
it.address2()
it.city()
it.company()
it.country()
it.countryCodeV2()
it.firstName()
it.formatted()
it.formattedArea()
// .id()
it.lastName()
it.latitude()
it.longitude()
it.name()
it.phone()
it.province()
it.provinceCode()
it.zip()
}
// .shippingDiscountAllocations()
it.statusUrl()
// .subtotalPriceV2()
// .successfulFulfillments()
// .totalPriceV2()
// .totalRefundedV2()
// .totalShippingPriceV2()
// .totalTaxV2()
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.graphql.support.ID
data class CheckoutItem(
val id: ID,
val quantity: Int
)

View file

@ -0,0 +1,50 @@
package com.tangem.tap.common.shop.shopify.data
import com.shopify.buy3.Storefront
fun Storefront.CollectionConnectionQuery.collectionFieldsFragment() {
edges { collectionEdgeQuery ->
collectionEdgeQuery
.node { collectionQuery ->
collectionQuery
.title()
.products({ arg -> arg.first(250) }
) { productConnectionQuery ->
productConnectionQuery
.edges { productEdgeQuery ->
productEdgeQuery
.node { productQuery ->
productQuery.title()
.productType()
.description()
.variants({ arg -> arg.first(10) }) { variantConnectionQuery ->
variantConnectionQuery.edges { variantQuery ->
variantQuery.node {
it.title()
it.sku()
it.currentlyNotInStock()
it.priceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
it.currencyCode()
}
it.compareAtPriceV2 {
it.amount()
}
it.product {
it.title()
.productType()
.description()
}
}
}
}
}
}
}
}
}
}

View file

@ -153,7 +153,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) {
tangemSdk.config.cardIdDisplayFormat = if (scanResponse.isTangemTwins()) {
CardIdDisplayFormat.LastLunh(4)
CardIdDisplayFormat.LastLuhn(4)
} else {
CardIdDisplayFormat.Full
}

View file

@ -1,24 +1,33 @@
package com.tangem.tap.domain
import com.tangem.blockchain.blockchains.solana.RentProvider
import com.tangem.blockchain.common.*
import com.tangem.common.services.Result
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.extensions.makeWalletManagersForApp
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.PendingTransactionType
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@ -30,7 +39,7 @@ class TapWalletManager {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
handleUpdateWalletResult(walletManager.safeUpdate(), walletManager)
@ -51,8 +60,8 @@ class TapWalletManager {
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
val currencies = wallet.getTokens()
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
loadFiatRate(fiatCurrency, currencies)
}
@ -89,7 +98,7 @@ class TapWalletManager {
configManager?.turnOff(ConfigManager.isSendingToPayIdEnabled)
configManager?.turnOff(ConfigManager.isTopUpEnabled)
} else if (blockchain == Blockchain.Bitcoin
|| data.walletData?.blockchain == Blockchain.Bitcoin.id) {
|| data.walletData?.blockchain == Blockchain.Bitcoin.id) {
configManager?.resetToDefault(ConfigManager.isSendingToPayIdEnabled)
configManager?.resetToDefault(ConfigManager.isTopUpEnabled)
} else {
@ -129,8 +138,7 @@ class TapWalletManager {
loadMultiWalletData(data, blockchain, null)
}
}
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadWallet())
store.dispatch(WalletAction.LoadFiatRate())
}
}
@ -156,7 +164,7 @@ class TapWalletManager {
CardCurrencies(blockchains = blockchains, tokens = emptyList())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList())
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
}
@ -165,12 +173,12 @@ class TapWalletManager {
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchainsWithoutPrimary)
.plus(primaryWalletManager)
.plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains)
}
@ -192,8 +200,7 @@ class TapWalletManager {
return@withContext
}
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadWallet())
store.dispatch(WalletAction.LoadFiatRate())
}
}
@ -207,6 +214,9 @@ class TapWalletManager {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
WalletAction.LoadData.Failure(TapError.UnknownBlockchain)
}
data.isDemoCard() -> {
return null
}
data.card.wallets.isEmpty() -> {
WalletAction.EmptyWallet
}
@ -217,7 +227,10 @@ class TapWalletManager {
private suspend fun handleUpdateWalletResult(result: Result<Wallet>, walletManager: WalletManager) {
withContext(Dispatchers.Main) {
when (result) {
is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data))
is Result.Success -> {
checkForRentWarning(walletManager)
store.dispatch(WalletAction.LoadWallet.Success(result.data))
}
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
@ -226,11 +239,11 @@ class TapWalletManager {
}
is TapError.WalletManagerUpdate.NoAccountError -> {
store.dispatch(WalletAction.LoadWallet
.NoAccount(walletManager.wallet, error.customMessage))
.NoAccount(walletManager.wallet, error.customMessage))
}
is TapError.WalletManagerUpdate.InternalError -> {
store.dispatch(WalletAction.LoadWallet
.Failure(walletManager.wallet, error.customMessage))
.Failure(walletManager.wallet, error.customMessage))
}
else -> {
store.dispatchDebugErrorNotification(error)
@ -241,6 +254,38 @@ class TapWalletManager {
}
}
private fun checkForRentWarning(walletManager: WalletManager) {
val rentProvider = walletManager as? RentProvider ?: return
scope.launch {
when (val result = rentProvider.minimalBalanceForRentExemption()) {
is com.tangem.blockchain.extensions.Result.Success -> {
fun isNeedToShowWarning(balance: BigDecimal, rentExempt: BigDecimal): Boolean = balance < rentExempt
val balance = walletManager.wallet.fundsAvailable(AmountType.Coin)
val outgoingTxs = walletManager.wallet.getPendingTransactions(PendingTransactionType.Outgoing)
val rentExempt = result.data
val show = if (outgoingTxs.isEmpty()) {
isNeedToShowWarning(balance, rentExempt)
} else {
val outgoingAmount = outgoingTxs.sumOf { it.amount ?: BigDecimal.ZERO }
val rest = balance.minus(outgoingAmount)
isNeedToShowWarning(rest, rentExempt)
}
if (!show) return@launch
val currency = walletManager.wallet.blockchain.currency
store.dispatchOnMain(WalletAction.SetWalletRent(
blockchain = walletManager.wallet.blockchain,
minRent = ("${rentProvider.rentAmount().stripZeroPlainString()} $currency"),
rentExempt = ("${rentExempt.stripZeroPlainString()} $currency")
))
}
is com.tangem.blockchain.extensions.Result.Failure -> {}
}
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<Currency, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {

View file

@ -63,6 +63,8 @@ object TapWorkarounds {
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.BSC,
"AB06" to Blockchain.XRP,
"AB07" to Blockchain.Bitcoin,
"AB08" to Blockchain.Ethereum,
)
}

View file

@ -2,6 +2,7 @@ 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.domain.configurable.Loader
/**
@ -10,13 +11,15 @@ 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 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 isCreatingTwinCardsAllowed: Boolean = false,
val shopify: ShopifyShop? = null
)
class ConfigManager(
@ -52,10 +55,10 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isSendingToPayIdEnabled -> config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}
@ -79,6 +82,7 @@ class ConfigManager(
config = config.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
onramperApiKey = values.onramperApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
@ -87,10 +91,12 @@ class ConfigManager(
infuraProjectId = values.infuraProjectId
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
)
defaultConfig = defaultConfig.copy(
coinMarketCapKey = values.coinMarketCapKey,
moonPayApiKey = values.moonPayApiKey,
onramperApiKey = values.onramperApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
@ -99,6 +105,7 @@ class ConfigManager(
infuraProjectId = values.infuraProjectId
),
appsFlyerDevKey = values.appsFlyerDevKey,
shopify = values.shopifyShop,
)
}

View file

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

View file

@ -10,7 +10,7 @@ import com.tangem.wallet.R
[REDACTED_AUTHOR]
*/
class WarningMessagesManager(
private val warningLoader: RemoteWarningLoader,
private val warningLoader: RemoteWarningLoader,
) {
private val warningsList: MutableList<WarningMessage> = mutableListOf()
@ -33,15 +33,15 @@ class WarningMessagesManager(
fun getWarnings(location: WarningMessage.Location, forBlockchains: List<Blockchain> = emptyList()): List<WarningMessage> {
return warningsList
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val list = it.blockchainList
when {
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val list = it.blockchainList
when {
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
}
}
fun hideWarning(warning: WarningMessage): Boolean {
@ -49,7 +49,7 @@ class WarningMessagesManager(
return when {
foundWarning == null -> false
foundWarning.type == WarningMessage.Type.Temporary
|| foundWarning.type == WarningMessage.Type.AppRating -> {
|| foundWarning.type == WarningMessage.Type.AppRating -> {
if (foundWarning.isHidden) {
false
} else {
@ -80,32 +80,32 @@ class WarningMessagesManager(
companion object {
fun devCardWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_developer_card,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_developer_card,
WarningMessage.Origin.Local
)
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local
)
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
title = "",
message = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
@ -117,15 +117,15 @@ class WarningMessagesManager(
)
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
@ -133,15 +133,15 @@ class WarningMessagesManager(
}
fun onlineVerificationFailed(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
@ -162,13 +162,25 @@ class WarningMessagesManager(
"",
type = WarningMessage.Type.TestCard,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
null,
R.string.alert_title,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local
)
fun demoCardWarning(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.alert_title,
R.string.alert_demo_message,
WarningMessage.Origin.Local
)
const val REMAINING_SIGNATURES_WARNING = 10
}
}

View file

@ -2,13 +2,22 @@ package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.CurrencyExchangeStatus
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
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 (!isBuyAllowed) return false
@ -25,16 +34,17 @@ fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
}
}
fun MoonpayStatus.sellIsAllowed(currency: Currency): Boolean {
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 -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToSell.contains(currency.currencySymbol)
val blockchain = currency.blockchain
when {
blockchain.isTestnet() -> false
blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC -> false
else -> availableToSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false

View file

@ -5,8 +5,10 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.hdWallet.DerivationPath
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
@ -16,10 +18,12 @@ import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.CreateWalletTask
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.product.CreateWalletsTask
import com.tangem.tap.domain.tasks.product.KeyWalletPublicKey
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
import com.tangem.tap.features.demo.DemoHelper
data class CreateProductWalletTaskResponse(
@ -96,25 +100,27 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private lateinit var card: Card
private var primaryCard: PrimaryCard? = null
private var createWalletResponse: CreateWalletResponse? = null
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
this.card = card
val curves = card.getCurvesForNonCreatedWallets()
CreateWalletsTask(curves).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createWalletResponse = result.data.createWalletResponses[0]
val createWalletResponses = result.data.createWalletResponses
when {
card.settings.isBackupAllowed -> {
linkPrimaryCard(session, callback)
linkPrimaryCard(createWalletResponses, session, callback)
}
card.settings.isHDWalletAllowed -> {
deriveKeys(session, callback)
deriveKeys(createWalletResponses, session, callback)
}
else -> {
callback(
@ -124,7 +130,6 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
)
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
@ -132,6 +137,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun linkPrimaryCard(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
@ -139,7 +145,20 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
when (result) {
is CompletionResult.Success -> {
primaryCard = result.data
deriveKeys(session, callback)
when {
card.settings.isHDWalletAllowed -> {
deriveKeys(createWalletResponse, session, callback)
}
else -> {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(
card = session.environment.card!!, primaryCard = primaryCard
)
)
)
}
}
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
@ -149,28 +168,26 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
private fun deriveKeys(
createWalletResponse: List<CreateWalletResponse>,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
.mapNotNull { it.derivationPath() }
val response = createWalletResponse.guard {
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
createWalletResponse.forEach { response ->
val blockchainsForCurve = getBlockchains(response.cardId).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath() }
if (derivationPaths.isNotEmpty()) {
map[response.wallet.publicKey.toMapKey()] = derivationPaths
}
}
if (map.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
return
}
if (derivationPaths.isNullOrEmpty()) {
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(
card = session.environment.card!!, primaryCard = primaryCard
)
)
)
return
}
DeriveMultipleWalletPublicKeysTask(mapOf(response.wallet.publicKey.toMapKey() to derivationPaths))
DeriveMultipleWalletPublicKeysTask(map)
.run(session) { result ->
when (result) {
is CompletionResult.Success -> {
@ -188,4 +205,12 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
}
}
}
private fun getBlockchains(cardId: String): List<Blockchain> {
return when {
DemoHelper.isDemoCardId(cardId) -> DemoHelper.config.demoBlockchains
card.isTestCard -> listOf(Blockchain.BitcoinTestnet, Blockchain.EthereumTestnet)
else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
}
}

View file

@ -1,6 +1,5 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.common.CompletionResult
@ -241,7 +240,7 @@ private class ScanWalletProcessor(
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
val derivations = collectDerivations(card)
if (derivations.isEmpty()) {
if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) {
callback(
CompletionResult.Success(
ScanResponse(

View file

@ -13,9 +13,12 @@ import com.tangem.tap.common.extensions.appendIf
import com.tangem.tap.common.extensions.readJsonFileToString
import com.tangem.tap.domain.extensions.getCustomIconUrl
import com.tangem.tap.domain.extensions.setCustomIconUrl
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.network.createMoshi
import timber.log.Timber
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
@ -28,10 +31,13 @@ class CurrenciesRepository(val context: Application) {
)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
val blockchains = loadSavedBlockchains(cardId)
val blockchains = loadSavedBlockchains(cardId).toMutableSet()
if (DemoHelper.isDemoCardId(cardId)) {
blockchains.addAll(DemoHelper.config.demoBlockchains)
}
if (blockchains.isEmpty()) return null
return CardCurrencies(loadSavedTokens(cardId), blockchains)
return CardCurrencies(loadSavedTokens(cardId), blockchains.toList())
}
fun saveCardCurrencies(cardId: String, currencies: CardCurrencies) {
@ -117,16 +123,21 @@ class CurrenciesRepository(val context: Application) {
getSupportedTokens()
}
val blockchainTokens = supportedTokens.map {
fromJsonToTokensDao(loadTokensJson(it), it)
val blockchainTokens = supportedTokens.mapNotNull { blockchain ->
loadTokensJson(blockchain)?.let { fromJsonToTokensDao(it, blockchain) }
}
val tokens = blockchainTokens.flatten().map { it.toToken() }
return tokens
}
private fun loadTokensJson(blockchain: Blockchain): String {
private fun loadTokensJson(blockchain: Blockchain): String? {
val fileName = getFileName(blockchain)
return context.assets.readJsonFileToString(fileName)
return try {
context.assets.readJsonFileToString(fileName)
} catch (ex: Exception) {
Timber.e(ex, "Tokens with the file name %s not found", fileName)
null
}
}
private fun getFileName(blockchain: Blockchain): String {
@ -143,12 +154,12 @@ class CurrenciesRepository(val context: Application) {
private fun getSupportedTokens(): List<Blockchain> {
return listOf(
Blockchain.Avalanche,
Blockchain.Binance,
Blockchain.BSC,
Blockchain.Ethereum,
Blockchain.BSC,
Blockchain.Binance,
Blockchain.Polygon,
Blockchain.Solana,
Blockchain.Avalanche,
Blockchain.Fantom,
)
}
@ -164,11 +175,11 @@ class CurrenciesRepository(val context: Application) {
return excludeUnsupportedBlockchains(blockchains)
}
//TODO: move to the App settings
// Use this list to temporarily exclude a blockchain from the list of tokens.
private fun excludeUnsupportedBlockchains(blockchains: List<Blockchain>): List<Blockchain> {
return blockchains.toMutableList().apply {
removeAll(listOf(
Blockchain.Fantom, Blockchain.FantomTestnet
// Any blockchain
))
}
}
@ -183,6 +194,13 @@ class CurrenciesRepository(val context: Application) {
}
}
fun Blockchain.getTokensName(): String {
return when (this) {
Blockchain.Fantom -> "Fantom Opera"
else -> this.fullName
}
}
@JsonClass(generateAdapter = true)
data class TokenDao(
val name: String,

View file

@ -1,45 +0,0 @@
package com.tangem.tap.domain.topup
import com.tangem.Message
import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TangemSigner
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import java.math.BigDecimal
class TopUpManager {
suspend fun topUpTestErc20Tokens(walletManager: EthereumWalletManager, token: Token) {
walletManager.update()
val amountToSend = Amount(walletManager.wallet.blockchain)
val destinationAddress = token.contractAddress
val feeResult = walletManager.getFee(amountToSend,
destinationAddress) as? Result.Success ?: return
val fee = feeResult.data[0]
if ((walletManager.wallet.amounts[AmountType.Coin]?.value ?: BigDecimal.ZERO) < fee.value) {
return
}
val transaction = walletManager.createTransaction(amountToSend, fee, destinationAddress)
val signer = TangemSigner(
tangemSdk = tangemSdk, Message()
) { signResponse ->
store.dispatch(
GlobalAction.UpdateWalletSignedHashes(
walletSignedHashes = signResponse.totalSignedHashes,
walletPublicKey = walletManager.wallet.publicKey.seedKey,
remainingSignatures = signResponse.remainingSignatures
)
)
}
walletManager.send(transaction, signer)
}
}

View file

@ -1,96 +0,0 @@
package com.tangem.tap.domain.topup
import android.net.Uri
import android.util.Base64
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
class TradeCryptoHelper {
companion object {
const val TRANSACTION_ID_PARAM = "transactionId"
const val CURRENCY_CODE_PARAM = "baseCurrencyCode"
const val CURRENCY_AMOUNT_PARAM = "baseCurrencyAmount"
const val DEPOSIT_WALLET_ADDRESS_PARAM = "depositWalletAddress"
private const val REDIRECT_URL_BUY = "tangem://success.tangem.com"
private const val REDIRECT_URL_SELL_REQUEST = "tangem://sell-request.tangem.com"
private const val BASE_URL_BUY =
"https://buy.moonpay.io"
// "https://buy-staging.moonpay.io" // TESTNET
private const val BASE_URL_SELL =
"https://sell.moonpay.com"
// "https://sell-staging.moonpay.com" // TESTNET
private const val API_KEY_PATH = "?apiKey="
private const val CURRENCY_PATH = "&currencyCode="
private const val WALLET_ADDRESS_PATH = "&walletAddress="
private const val REFUND_WALLET_ADDRESS_PATH = "&refundWalletAddress="
private const val BASE_CURRENCY_PATH = "&baseCurrencyCode="
private const val REDIRECT_URL_PATH = "&redirectURL="
private const val SIGNATURE_PATH = "&signature="
private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId="
private const val BASE_CURRENCY_USD = "USD"
fun getUrl(
action: Action,
blockchain: Blockchain?,
cryptoCurrencyName: CryptoCurrencyName,
walletAddress: String,
apiKey: String,
secretKey: String,
): String {
val originalQuery: String
val baseUrl: String
when (action) {
Action.Buy -> {
if (blockchain?.isTestnet() == true) {
return blockchain.getTestnetTopUpUrl() ?: ""
}
baseUrl = BASE_URL_BUY
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
BASE_CURRENCY_PATH + BASE_CURRENCY_USD.urlEncode() +
WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_BUY.urlEncode()
}
Action.Sell -> {
baseUrl = BASE_URL_SELL
originalQuery = API_KEY_PATH + apiKey.urlEncode() +
BASE_CURRENCY_PATH + cryptoCurrencyName.urlEncode() +
REFUND_WALLET_ADDRESS_PATH + walletAddress.urlEncode() +
REDIRECT_URL_PATH + REDIRECT_URL_SELL_REQUEST.urlEncode()
}
}
val signature = createSignature(originalQuery, secretKey)
return baseUrl + originalQuery + SIGNATURE_PATH + signature.urlEncode()
}
fun getSellCryptoReceiptUrl(transactionId: String): String {
return BASE_URL_SELL + TRANSACTION_RECEIPT_PATH + transactionId
}
private fun String.urlEncode(): String {
return Uri.encode(this)
}
private fun createSignature(data: String, key: String): String {
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKey)
val sha256encoded = sha256Hmac.doFinal(data.toByteArray())
return Base64.encodeToString(sha256encoded, Base64.NO_WRAP)
}
}
enum class Action { Buy, Sell }
}

View file

@ -16,6 +16,7 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
*/
class TwinsCardWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
val getTopOfAnchorViewForActivateState: () -> Float
) {
@ -87,20 +88,20 @@ class TwinsCardWidget(
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -120f,
yTranslation = -220f,
xTranslation = -120f * deviceScaleFactor,
yTranslation = -220f * deviceScaleFactor,
rotation = -3f,
elevation = 1f,
scale = 1f,
scale = deviceScaleFactor,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 170f,
yTranslation = 170f,
xTranslation = 170f * deviceScaleFactor,
yTranslation = 170f * deviceScaleFactor,
rotation = -3f,
elevation = 0f,
scale = 1f,
scale = deviceScaleFactor,
)
}
}
@ -123,7 +124,7 @@ class TwinsCardWidget(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.5f,
scale = (twinProperties.scale - 0.5f) * deviceScaleFactor,
)
}

View file

@ -15,6 +15,7 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.sign.SignHashCommand
import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.features.details.redux.walletconnect.*
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
@ -35,18 +36,11 @@ class WalletConnectSdkHelper {
id: Long,
type: WcTransactionType,
): WcTransactionData? {
val walletManager = getWalletManager(session) ?: return null
try {
walletManager.update()
} catch (exception: Exception) {
Timber.e(exception)
return null
}
walletManager.safeUpdate()
val wallet = walletManager.wallet
val balance =
wallet.amounts[AmountType.Coin]?.value ?: return null
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
val gas = transaction.gas?.hexToBigDecimal()
?: transaction.gasLimit?.hexToBigDecimal()

View file

@ -0,0 +1,203 @@
package com.tangem.tap.features.demo
import com.tangem.blockchain.blockchains.bitcoin.BitcoinWalletManager
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
/**
[REDACTED_AUTHOR]
*/
interface DemoMiddleware {
fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean
}
object DemoHelper {
val config = DemoConfig()
private val demoMiddlewares = listOf(
DemoOnboardingNoteMiddleware(),
)
private val disabledActionFeatures = listOf(
WalletConnectAction.StartWalletConnect::class.java,
WalletAction.TradeCryptoAction.Buy::class.java,
WalletAction.TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)
fun isDemoCardId(cardId: String): Boolean = config.isDemoCardId(cardId)
fun tryHandle(appState: () -> AppState?, action: Action): Boolean {
val scanResponse = getScanResponse(appState) ?: return false
if (!scanResponse.isDemoCard()) return false
demoMiddlewares.forEach {
if (it.tryHandle(config, scanResponse, action)) return true
}
disabledActionFeatures.firstOrNull { it == action::class.java }?.let {
store.dispatchNotification(R.string.alert_demo_feature_disabled)
return true
}
return false
}
fun injectDemoBalance(walletManager: WalletManager?) {
val manager = walletManager ?: return
val blockchain = walletManager.wallet.blockchain
val amount = config.getBalance(blockchain)
manager.wallet.setAmount(amount)
}
private fun getScanResponse(appState: () -> AppState?): ScanResponse? {
val state = appState() ?: return null
return state.globalState.onboardingState.onboardingManager?.scanResponse
?: state.globalState.scanResponse
}
}
class DemoConfig {
val demoBlockchains = listOf(
Blockchain.Bitcoin,
Blockchain.Ethereum,
Blockchain.Dogecoin,
Blockchain.Solana,
)
val demoCardIds: List<String> by lazy {
val demoIds = (releaseDemoCardIds + testDemoCardIds).toMutableList()
if (BuildConfig.DEBUG) demoIds.addAll(debugTestDemoCardIds)
return@lazy demoIds.distinct()
}
private val walletBalances: Map<Blockchain, Amount> = mapOf(
Blockchain.Bitcoin to Amount(0.028.toBigDecimal(), Blockchain.Bitcoin),
Blockchain.Ethereum to Amount(0.2311.toBigDecimal(), Blockchain.Ethereum),
Blockchain.Dogecoin to Amount(1450.025.toBigDecimal(), Blockchain.Dogecoin),
Blockchain.Solana to Amount(13.246.toBigDecimal(), Blockchain.Solana),
)
fun isDemoCardId(cardId: String): Boolean = demoCardIds.contains(cardId)
fun getBalance(blockchain: Blockchain): Amount = walletBalances[blockchain]?.copy()
?: Amount(BigDecimal.ZERO, blockchain).copy()
private val releaseDemoCardIds = mutableListOf<String>(
// Tangem Wallet:
"AC01000000041100",
"AC01000000042462",
"AC01000000041647",
"AC01000000041621",
"AC01000000041217",
"AC01000000041225",
"AC01000000041209",
"AC01000000041092",
"AC01000000041472",
"AC01000000041662",
"AC01000000045754",
"AC01000000045960",
// Tangem Note BTC:
"AB0100000046530",
"AB0100000046720",
"AB0100000046746",
"AB0100000046498",
"AB0100000046753",
"AB0100000049608",
"AB0100000046761",
"AB0100000049574",
"AB0100000046605",
"AB0100000046571",
"AB0100000046704",
"AB0100000046647",
// Tangem Note Ethereum:
"AB02000000051000",
"AB02000000050986",
"AB02000000051026",
"AB02000000051042",
"AB02000000051091",
"AB02000000051083",
"AB02000000050960",
"AB02000000051034",
"AB02000000050911",
"AB02000000051133",
"AB02000000051158",
"AB02000000051059",
)
private val testDemoCardIds = listOf(
"FB20000000000186", // Note ETH
"FB10000000000196", // Note BTC
"FB30000000000176", // Wallet
//TODO: delete bellow ids before 3.28 release
"AB01000000045060", // Note BTC
"AB02000000045028", // Note ETH
"AC79000000000004", // Wallet 4.46
)
private val debugTestDemoCardIds = listOf<String>(
)
}
class DemoTransactionSender(
private val walletManager: WalletManager,
private val sender: TransactionSender = walletManager as TransactionSender
) : TransactionSender {
override suspend fun getFee(amount: Amount, destination: String): Result<List<Amount>> {
val blockchain = walletManager.wallet.blockchain
return when (walletManager) {
is BitcoinWalletManager -> Result.Success(listOf(
Amount(0.0001.toBigDecimal(), blockchain),
Amount(0.0003.toBigDecimal(), blockchain),
Amount(0.00055.toBigDecimal(), blockchain),
))
else -> sender.getFee(amount, destination)
}
}
override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult {
val dataToSign = randomString(32).toByteArray()
val signerResponse = signer.sign(dataToSign, walletManager.wallet.cardId, walletManager.wallet.publicKey)
return when (signerResponse) {
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID))
is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error)
}
}
private fun randomInt(from: Int, to: Int): Int = kotlin.random.Random.nextInt(from, to)
private fun randomString(length: Int): String {
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
return (1..length)
.map { randomInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
}
companion object {
val ID = DemoTransactionSender::class.java.simpleName
}
}

View file

@ -0,0 +1,58 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.launch
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
internal class DemoOnboardingNoteMiddleware : DemoMiddleware {
override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean {
val globalState = store.state.globalState
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard {
return false
}
store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val balanceAmount = config.getBalance(walletManager.wallet.blockchain)
val loadedBalance = noteState.walletBalance.copy(
value = balanceAmount.value!!,
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Done,
error = null,
criticalError = null
)
walletManager.wallet.setAmount(balanceAmount)
scope.launch {
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
return true
}
}
return false
}
}

View file

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

View file

@ -17,9 +17,11 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.walletconnect.BnbHelper
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.domain.walletconnect.WalletConnectNetworkUtils
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.Action
import org.rekotlin.Middleware
class WalletConnectMiddleware {
@ -28,138 +30,131 @@ class WalletConnectMiddleware {
val walletConnectMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
handle(state, action)
next(action)
}
}
}
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
}
private fun handle(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ClipboardOrScanQr(
action.wcUri
)
)
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
is WalletConnectAction.FailureEstablishingSession -> {
if (action.session != null) {
walletConnectManager.disconnect(action.session)
}
}
is WalletConnectAction.UnsupportedCard -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard))
}
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
)
}
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected
)
)
}
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
}
is WalletConnectAction.DisconnectSession -> {
walletConnectManager.disconnect(action.session)
}
is WalletConnectAction.HandleTransactionRequest -> {
walletConnectManager.handleTransactionRequest(
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
)
}
is WalletConnectAction.RejectRequest -> {
walletConnectManager.rejectRequest(action.session, action.id)
}
is WalletConnectAction.SendTransaction -> {
walletConnectManager.completeTransaction(action.session)
}
is WalletConnectAction.SignMessage -> {
walletConnectManager.sendSignedMessage(action.session)
}
is WalletConnectAction.BinanceTransaction.Trade -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Transfer -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Sign -> {
walletConnectManager.signBnb(
action.id, action.data, action.sessionData
)
when (action) {
is WalletConnectAction.RestoreSessions -> {
walletConnectManager.restoreSessions()
}
is WalletConnectAction.HandleDeepLink -> {
if (!action.wcUri.isNullOrBlank()) {
if (WalletConnectManager.isCorrectWcUri(action.wcUri)) {
store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri))
}
}
next(action)
}
is WalletConnectAction.StartWalletConnect -> {
val uri = action.activity.getFromClipboard()?.toString()
if (uri != null && WalletConnectManager.isCorrectWcUri(uri)) {
store.dispatchOnMain(WalletConnectAction.ShowClipboardOrScanQrDialog(uri))
} else {
store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.QrScan))
}
}
is WalletConnectAction.ShowClipboardOrScanQrDialog -> {
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.ClipboardOrScanQr(
action.wcUri
)
)
)
}
is WalletConnectAction.OpeningSessionTimeout -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.SessionTimeout))
}
is WalletConnectAction.FailureEstablishingSession -> {
if (action.session != null) {
walletConnectManager.disconnect(action.session)
}
}
is WalletConnectAction.UnsupportedCard -> {
store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedCard))
}
is WalletConnectAction.OpenSession -> {
walletConnectManager.connect(
wcUri = action.wcUri,
)
}
is WalletConnectAction.RefuseOpeningSession -> {
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.OpeningSessionRejected
)
)
}
is WalletConnectAction.ScanCard -> {
scanCard(action.session, action.chainId)
}
is WalletConnectAction.ApproveSession -> {
walletConnectManager.approve(action.session)
}
is WalletConnectAction.DisconnectSession -> {
walletConnectManager.disconnect(action.session)
}
is WalletConnectAction.HandleTransactionRequest -> {
walletConnectManager.handleTransactionRequest(
transaction = action.transaction,
session = action.session,
id = action.id,
type = action.type
)
}
is WalletConnectAction.HandlePersonalSignRequest -> {
walletConnectManager.handlePersonalSignRequest(
message = action.message,
session = action.session,
id = action.id
)
}
is WalletConnectAction.RejectRequest -> {
walletConnectManager.rejectRequest(action.session, action.id)
}
is WalletConnectAction.SendTransaction -> {
walletConnectManager.completeTransaction(action.session)
}
is WalletConnectAction.SignMessage -> {
walletConnectManager.sendSignedMessage(action.session)
}
is WalletConnectAction.BinanceTransaction.Trade -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Transfer -> {
val messageData = BnbHelper.createMessageData(action.order)
store.dispatchOnMain(
GlobalAction.ShowDialog(
WalletConnectDialog.BnbTransactionDialog(
data = messageData,
session = action.sessionData.session,
sessionId = action.id,
cardId = action.sessionData.wallet.cardId,
dAppName = action.sessionData.peerMeta.name
)
)
)
}
is WalletConnectAction.BinanceTransaction.Sign -> {
walletConnectManager.signBnb(
action.id, action.data, action.sessionData
)
}
}
}
@ -190,9 +185,9 @@ class WalletConnectMiddleware {
}
val walletManager = getWalletManager(scanResponse, blockchain).guard {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
return
}
val wallet = walletManager.wallet
val derivedKey =
@ -227,7 +222,7 @@ class WalletConnectMiddleware {
): WalletManager? {
val card = scanResponse.card
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
val blockchainToMake = if (blockchain == Blockchain.Ethereum && card.isTestCard) {
Blockchain.EthereumTestnet
} else {
blockchain

View file

@ -5,6 +5,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.ConfirmScreenState
@ -12,12 +13,14 @@ import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_confirm.*
import com.tangem.wallet.databinding.FragmentDetailsConfirmBinding
import org.rekotlin.StoreSubscriber
class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsConfirmBinding by viewBinding(FragmentDetailsConfirmBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
@ -47,38 +50,39 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null) return
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with(binding) {
when (state.confirmScreenState) {
ConfirmScreenState.EraseWallet -> {
toolbar.title = getString(R.string.details_row_title_reset_factory_settings)
tv_warning_description.text = getString(R.string.details_row_title_reset_factory_settings_warning)
btn_confirm.text = getString(R.string.details_row_title_reset_factory_settings)
btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
tvWarningDescription.text = getString(R.string.details_row_title_reset_factory_settings_warning)
btnConfirm.text = getString(R.string.details_row_title_reset_factory_settings)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
)
btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) }
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) }
}
ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode,
ConfirmScreenState.PassCode -> {
toolbar.title = getString(R.string.details_row_title_manage_security)
tv_warning_description.text = getString(R.string.details_security_management_warning)
btn_confirm.text = getString(R.string.common_save_changes)
btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_save), null
toolbar.title = getString(R.string.details_manage_security_title)
tvWarningDescription.text = getString(R.string.details_security_management_warning)
btnConfirm.text = getString(R.string.common_save_changes)
btnConfirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_save), null
)
btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }
btnConfirm.setOnClickListener { store.dispatch(DetailsAction.ManageSecurity.SaveChanges) }
}
}
}
}

View file

@ -6,6 +6,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -18,13 +19,13 @@ import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.android.synthetic.main.fragment_wallet.toolbar
import com.tangem.wallet.databinding.FragmentDetailsBinding
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private var currencySelectionDialog = CurrencySelectionDialog()
private val binding: FragmentDetailsBinding by viewBinding(FragmentDetailsBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -55,15 +56,18 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null) return
if (activity == null || view == null) return
setState(state)
}
private fun setState(state: DetailsState) = with (binding){
if (state.cardInfo != null) {
val cardId = if (state.isTangemTwins) {
@ -71,61 +75,61 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
} else {
state.cardInfo.cardId
}
tv_card_id.text = cardId
tv_issuer.text = state.cardInfo.issuer
tv_signed_hashes.text = getString(
tvCardId.text = cardId
tvIssuer.text = state.cardInfo.issuer
tvSignedHashes.text = getString(
R.string.details_row_subtitle_signed_hashes_format,
state.cardInfo.signedHashes.toString()
)
}
tv_signed_hashes.show(!state.isTangemTwins)
tv_signed_hashes_title.show(!state.isTangemTwins)
tvSignedHashes.show(!state.isTangemTwins)
tvSignedHashesTitle.show(!state.isTangemTwins)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tvDisclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
tv_card_tou.show(state.cardTermsOfUseUrl != null)
tv_card_tou.setOnClickListener {
tvCardTou.show(state.cardTermsOfUseUrl != null)
tvCardTou.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = state.cardTermsOfUseUrl
startActivity(intent)
}
if (state.isTangemTwins) {
tv_reset_to_factory.show()
tv_reset_to_factory.text = getText(R.string.details_row_title_twins_recreate)
tv_reset_to_factory.setOnClickListener {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_twins_recreate)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tv_reset_to_factory.show()
tv_reset_to_factory.text = getText(R.string.details_row_title_reset_factory_settings)
tv_reset_to_factory.setOnClickListener {
tvResetToFactory.show()
tvResetToFactory.text = getText(R.string.details_row_title_reset_factory_settings)
tvResetToFactory.setOnClickListener {
store.dispatch(DetailsAction.ResetToFactory.Check)
store.dispatch(DetailsAction.ResetToFactory.Proceed)
}
}
tv_create_backup.show(state.createBackupAllowed)
tv_create_backup.setOnClickListener {
tvCreateBackup.show(state.createBackupAllowed)
tvCreateBackup.setOnClickListener {
store.dispatch(DetailsAction.CreateBackup)
}
tv_app_currency.text = state.appCurrencyState.fiatCurrencyName
tvAppCurrency.text = state.appCurrencyState.fiatCurrencyName
tv_app_currency_title.setOnClickListener {
tvAppCurrencyTitle.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
tv_send_feedback.setOnClickListener {
tvSendFeedback.setOnClickListener {
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_wallet_connect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
tv_wallet_connect.setOnClickListener {
tvWalletConnect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
tvWalletConnect.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tv_security_title.setOnClickListener {
llManageSecurity.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
}
@ -135,7 +139,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
SecurityOption.AccessCode -> R.string.details_manage_security_access_code
null -> null
}
currentSecurity?.let { tv_security.text = getString(it) }
currentSecurity?.let { tvSecurity.text = getString(it) }
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
@ -147,7 +151,6 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
} else {
currencySelectionDialog.clear()
}
}
}

View file

@ -5,6 +5,7 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -13,14 +14,17 @@ import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_confirm.toolbar
import kotlinx.android.synthetic.main.fragment_details_security.*
import com.tangem.wallet.databinding.FragmentDetailsSecurityBinding
import org.rekotlin.StoreSubscriber
import java.util.*
class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
StoreSubscriber<DetailsState> {
private val binding: FragmentDetailsSecurityBinding by viewBinding(
FragmentDetailsSecurityBinding::bind
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
@ -49,14 +53,14 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
}
private fun setOnClickListeners() {
btn_save_changes.setOnClickListener {
binding.btnSaveChanges.setOnClickListener {
store.state.detailsState.securityScreenState?.selectedOption?.let {
store.dispatch(DetailsAction.ManageSecurity.ConfirmSelection(it))
}
@ -64,7 +68,8 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
}
override fun newState(state: DetailsState) {
if (activity == null) return
if (activity == null || view == null) return
selectSecurityOption(state.securityScreenState?.selectedOption)
for (option in SecurityOption.values()) {
setupOption(
@ -73,18 +78,18 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
?: EnumSet.noneOf(SecurityOption::class.java)
)
}
tv_access_code_unavailable_disclaimer.show(
binding.tvAccessCodeUnavailableDisclaimer.show(
state.scanResponse?.card?.backupStatus == Card.BackupStatus.NoBackup
)
}
private fun selectSecurityOption(securityOption: SecurityOption?) {
radiobutton_long_tap.isChecked =
securityOption == SecurityOption.LongTap && radiobutton_long_tap.isEnabled
radiobutton_passcode.isChecked =
securityOption == SecurityOption.PassCode && radiobutton_passcode.isEnabled
radiobutton_access_code.isChecked =
securityOption == SecurityOption.AccessCode && radiobutton_access_code.isEnabled
private fun selectSecurityOption(securityOption: SecurityOption?) = with(binding) {
radiobuttonLongTap.isChecked =
securityOption == SecurityOption.LongTap && radiobuttonLongTap.isEnabled
radiobuttonPasscode.isChecked =
securityOption == SecurityOption.PassCode && radiobuttonPasscode.isEnabled
radiobuttonAccessCode.isChecked =
securityOption == SecurityOption.AccessCode && radiobuttonAccessCode.isEnabled
}
private fun setupOption(option: SecurityOption, allowedOptions: EnumSet<SecurityOption>) {
@ -95,46 +100,46 @@ class DetailsSecurityFragment : Fragment(R.layout.fragment_details_security),
}
}
private fun enableLongTap(enable: Boolean) {
group_long_tap.show(enable)
private fun enableLongTap(enable: Boolean) = with(binding) {
groupLongTap.show(enable)
val alpha = if (enable) 1f else 0.5f
tv_long_tap_description.alpha = alpha
tv_long_tap_title.alpha = alpha
radiobutton_long_tap.alpha = alpha
radiobutton_long_tap.isEnabled = enable
v_long_tap.isClickable = enable
tvLongTapDescription.alpha = alpha
tvLongTapTitle.alpha = alpha
radiobuttonLongTap.alpha = alpha
radiobuttonLongTap.isEnabled = enable
vLongTap.isClickable = enable
if (enable) {
v_long_tap.setOnClickListener {
vLongTap.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.LongTap))
}
}
}
private fun enablePasscode(enable: Boolean) {
group_passcode.show(enable)
private fun enablePasscode(enable: Boolean) = with(binding) {
groupPasscode.show(enable)
val alpha = if (enable) 1f else 0.5f
tv_passcode_description.alpha = alpha
tv_passcode_title.alpha = alpha
radiobutton_passcode.alpha = alpha
radiobutton_passcode.isEnabled = enable
v_passcode.isClickable = enable
tvPasscodeDescription.alpha = alpha
tvPasscodeTitle.alpha = alpha
radiobuttonPasscode.alpha = alpha
radiobuttonPasscode.isEnabled = enable
vPasscode.isClickable = enable
if (enable) {
v_passcode.setOnClickListener {
vPasscode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.PassCode))
}
}
}
private fun enableAccessCode(enable: Boolean) {
group_access_code.show(enable)
private fun enableAccessCode(enable: Boolean) = with(binding) {
groupAccessCode.show(enable)
val alpha = if (enable) 1f else 0.5f
tv_access_code_description.alpha = alpha
tv_access_code_title.alpha = alpha
radiobutton_access_code.alpha = alpha
radiobutton_access_code.isEnabled = enable
v_access_code.isClickable = enable
tvAccessCodeDescription.alpha = alpha
tvAccessCodeTitle.alpha = alpha
radiobuttonAccessCode.alpha = alpha
radiobuttonAccessCode.isEnabled = enable
vAccessCode.isClickable = enable
if (enable) {
v_access_code.setOnClickListener {
vAccessCode.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.SelectOption(SecurityOption.AccessCode))
}
}

View file

@ -3,14 +3,15 @@ package com.tangem.tap.features.details.ui.walletconnect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.chip.Chip
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_wallet_connect_session.view.*
class WalletConnectSessionsAdapter
: ListAdapter<WalletConnectSession, WalletConnectSessionsAdapter.SessionsViewHolder>(
@ -41,15 +42,14 @@ class WalletConnectSessionsAdapter
RecyclerView.ViewHolder(view) {
fun bind(session: WalletConnectSession) {
view.tv_card_id.text = view.context.getString(
view.findViewById<TextView>(R.id.tv_card_id).text = view.context.getString(
R.string.wallet_connect_card_number, session.wallet.cardId
)
view.tv_d_app_name.text = session.peerMeta.name
view.findViewById<TextView>(R.id.tv_d_app_name).text = session.peerMeta.name
view.btn_disconnect.setOnClickListener {
view.findViewById<Chip>(R.id.btn_disconnect).setOnClickListener {
store.dispatch(WalletConnectAction.DisconnectSession(session.session))
}
}
}
}

View file

@ -1,19 +1,20 @@
package com.tangem.tap.features.details.ui.walletconnect
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet_connect_sessions.*
import com.tangem.wallet.databinding.FragmentWalletConnectSessionsBinding
import org.rekotlin.StoreSubscriber
@ -21,7 +22,9 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_
StoreSubscriber<WalletConnectState> {
private lateinit var walletConnectSessionsAdapter: WalletConnectSessionsAdapter
private var dialog: Dialog? = null
private val binding: FragmentWalletConnectSessionsBinding by viewBinding(
FragmentWalletConnectSessionsBinding::bind
)
override fun onCreate(savedInstanceState: Bundle?) {
@ -53,7 +56,7 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
@ -61,27 +64,31 @@ class WalletConnectSessionsFragment : Fragment(R.layout.fragment_wallet_connect_
}
private fun setOnClickListeners() {
fab_open_session.setOnClickListener {
binding.fabOpenSession.setOnClickListener {
store.dispatch(WalletConnectAction.StartWalletConnect(activity = requireActivity()))
}
}
private fun setupTransactionsRecyclerView() {
private fun setupTransactionsRecyclerView() = with(binding) {
walletConnectSessionsAdapter = WalletConnectSessionsAdapter()
rv_wallet_connect_sessions.layoutManager = LinearLayoutManager(requireContext())
rv_wallet_connect_sessions.adapter = walletConnectSessionsAdapter
rvWalletConnectSessions.layoutManager = LinearLayoutManager(requireContext())
rvWalletConnectSessions.adapter = walletConnectSessionsAdapter
walletConnectSessionsAdapter.submitList(store.state.walletConnectState.sessions)
}
override fun newState(state: WalletConnectState) {
if (activity == null) return
if (activity == null || view == null) return
fab_open_session.show(!state.loading)
pb_wallet_connect.show(state.loading)
binding.fabOpenSession.show(!state.loading)
binding.pbWalletConnect.show(state.loading)
walletConnectSessionsAdapter.submitList(state.sessions)
rv_wallet_connect_sessions.show(state.sessions.isNotEmpty())
tv_no_sessions.show(state.sessions.isEmpty())
tv_no_sessions_title.show(state.sessions.isEmpty())
showUpdatedList(state.sessions)
}
private fun showUpdatedList(sessions: List<WalletConnectSession>) = with(binding) {
walletConnectSessionsAdapter.submitList(sessions)
rvWalletConnectSessions.show(sessions.isNotEmpty())
tvNoSessions.show(sessions.isEmpty())
tvNoSessionsTitle.show(sessions.isEmpty())
}
}

View file

@ -5,18 +5,21 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_disclaimer.*
import com.tangem.wallet.databinding.FragmentDisclaimerBinding
import org.rekotlin.StoreSubscriber
class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
StoreSubscriber<DisclaimerState> {
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
@ -45,22 +48,22 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
binding.toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
setOnClickListeners()
}
private fun setOnClickListeners() {
btn_accept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
binding.btnAccept.setOnClickListener { store.dispatch(DisclaimerAction.AcceptDisclaimer) }
}
override fun newState(state: DisclaimerState) {
if (activity == null) return
if (activity == null || view == null) return
if (state.accepted) {
half_transparent_overlay.hide()
btn_accept.hide()
binding.halfTransparentOverlay.hide()
binding.btnAccept.hide()
}
}
}

View file

@ -11,6 +11,9 @@ import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.TapWorkarounds
import com.tangem.tap.features.feedback.EmailData.Companion.appendBlankLine
import com.tangem.tap.features.feedback.EmailData.Companion.appendDelimiter
import com.tangem.wallet.R
import timber.log.Timber
import java.io.File
import java.io.FileWriter
@ -40,7 +43,8 @@ class FeedbackManager(
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
activity.sendEmail(
email = getSupportEmail(),
subject = emailData.subject, message = emailData.joinTogether(infoHolder, emailData !is ScanFailsEmail),
subject = activity.getString(emailData.subjectResId),
message = emailData.joinTogether(activity, infoHolder),
file = fileLog,
onFail = onFail
)
@ -142,7 +146,7 @@ class AdditionalEmailInfo {
cardFirmwareVersion = card.firmwareVersion.stringValue
cardIssuer = card.issuer.name
signedHashesCount = card.wallets
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
@ -201,43 +205,40 @@ class AdditionalEmailInfo {
}
interface EmailData {
val subject: String
val mainMessage: String
val subjectResId: Int
val mainMessageResId: Int
fun getDataCollectionMessageResId(): Int = R.string.feedback_data_collection_message
fun prepare(infoHolder: AdditionalEmailInfo) {}
fun appendDelimiter(builder: StringBuilder) {
builder.append("----------\n")
}
fun appendBlankLine(builder: StringBuilder) {
builder.append("\n")
}
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(infoHolder: AdditionalEmailInfo, allowToErasePhoneInfo: Boolean): String {
val allowToErasePhoneInfoDisclaimer = if (allowToErasePhoneInfo) {
"Following information is optional. You can erase it if you dont want to share it.\n\n"
} else {
""
}
return "$mainMessage\n\n\n\n" +
allowToErasePhoneInfoDisclaimer +
createOptionalMessage(infoHolder)
fun joinTogether(context: Context, infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
append("\n\n\n\n")
append(context.getString(getDataCollectionMessageResId()))
append(createOptionalMessage(infoHolder))
}.toString()
}
companion object {
internal fun StringBuilder.appendDelimiter() = append("----------\n")
internal fun StringBuilder.appendBlankLine() = append("\n")
}
}
class RateCanBeBetterEmail : EmailData {
override val subject: String = "My suggestions"
override val mainMessage: String = "Tell us what functions you are missing, and we will try to help you."
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 {
val walletInfo = infoHolder.walletsInfo[0]
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -246,11 +247,21 @@ class RateCanBeBetterEmail : EmailData {
}
class ScanFailsEmail : EmailData {
override val subject: String = "Cant scan a card"
override val mainMessage: String = "Please tell us what card do you have?"
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 {
return StringBuilder().apply {
append(context.getString(mainMessageResId))
append("\n\n\n\n")
append(createOptionalMessage(infoHolder))
}.toString()
}
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -259,8 +270,8 @@ class ScanFailsEmail : EmailData {
}
class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
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 {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
@ -268,17 +279,17 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendDelimiter(this)
appendDelimiter()
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendKeyValue("Host", walletInfo.host)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Error", error)
appendDelimiter(this)
appendDelimiter()
appendKeyValue("Source address", walletInfo.address)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
appendBlankLine(this)
appendBlankLine()
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -288,16 +299,16 @@ class SendTransactionFailedEmail(private val error: String) : EmailData {
}
class FeedbackEmail : EmailData {
override val subject: String
override val subjectResId: Int
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessage: String
override val mainMessageResId: Int
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject = "Tangem feedback"
private val tangemMainMessage = "Hi support team,"
private val tangemSubject: Int = R.string.feedback_subject_support_tangem
private val tangemMainMessage: Int = R.string.feedback_preface_support
private val s2cSubject = "Feedback"
private val s2cMainMessage = "Hi support team,"
private val s2cSubject: Int = R.string.feedback_subject_support
private val s2cMainMessage: Int = R.string.feedback_preface_support
private var isS2CCard = false
@ -312,26 +323,26 @@ class FeedbackEmail : EmailData {
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
infoHolder.walletsInfo.forEach {
appendDelimiter(builder)
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Explorer link", it.explorerLink)
}
appendBlankLine(builder)
builder.appendBlankLine()
infoHolder.tokens.forEach { tokens ->
appendDelimiter(builder)
builder.appendDelimiter()
builder.appendKeyValue("Blockchain", tokens.key.fullName)
builder.appendKeyValue("Tokens", tokens.value.map { "${it.name} - ${it.symbol}" }.toString())
}
appendDelimiter(builder)
appendBlankLine(builder)
builder.appendDelimiter()
builder.appendBlankLine()
// appendKeyValue("Outputs count", infoHolder.outputsCount)
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)
builder.appendKeyValue("App version", infoHolder.appVersion)
return builder.toString()
}
}

View file

@ -2,35 +2,25 @@ package com.tangem.tap.features.home
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.ViewStateWidget
import com.tangem.tap.common.transitions.FrontCardEnterTransition
import com.tangem.tap.common.transitions.FrontCardExitTransition
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.Fragment
import com.google.accompanist.appcompattheme.AppCompatTheme
import com.tangem.tap.features.home.compose.StoriesScreen
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_home_top.*
import kotlinx.android.synthetic.main.view_bg_home.*
import org.rekotlin.StoreSubscriber
class HomeFragment : BaseOnboardingFragment<HomeState>() {
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
private lateinit var btnScanCard: ViewStateWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home_top
var homeState: MutableState<HomeState> = mutableStateOf(store.state.homeState)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
store.dispatch(HomeAction.Init)
}
@ -39,50 +29,36 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
store.dispatch(BackupAction.CheckForUnfinishedBackup)
toolbar.hide()
val shareTransition = FragmentShareTransition(
listOf(
ShareElement(imv_front_card, ShareElement.imvFrontCard),
ShareElement(imv_back_card, ShareElement.imvBackCard),
ShareElement(bg_circle_large),
ShareElement(bg_circle_medium),
ShareElement(bg_circle_min),
),
FrontCardEnterTransition(),
FrontCardExitTransition()
)
store.dispatch(HomeAction.SetFragmentShareTransition(shareTransition))
tv_header.setText(R.string.home_welcome_header)
tv_body.setText(R.string.home_welcome_body)
btn_main_action.setText(R.string.home_button_scan)
btn_main_action.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_alternative_action.setText(R.string.home_button_get_new_card)
btn_alternative_action.setOnClickListener { store.dispatch(HomeAction.GoToShop) }
btnScanCard = IndeterminateProgressButtonWidget(btn_main_action, progress)
getView()?.findViewById<ComposeView>(R.id.cv_stories)?.setContent {
AppCompatTheme {
StoriesScreen(
homeState,
onScanButtonClick = { store.dispatch(HomeAction.ReadCard) },
onShopButtonClick = { store.dispatch(HomeAction.GoToShop) }
)
}
}
}
override fun subscribeToStore() {
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
}.select { it.homeState }
}
storeSubscribersList.add(this)
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: HomeState) {
if (activity == null) return
if (activity == null || view == null) return
btnScanCard.changeState(state.btnScanState.progressState)
btnScanCard.mainView.isEnabled = state.btnScanState.enabled
homeState.value = state
}
override fun onDestroyView() {
store.dispatch(HomeAction.SetFragmentShareTransition(null))
super.onDestroyView()
}
}

View file

@ -0,0 +1,45 @@
package com.tangem.tap.features.home.compose
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.ExperimentalUnitApi
@OptIn(ExperimentalUnitApi::class)
@Composable
fun AutoSizeText(
text: String,
textStyle: TextStyle,
modifier: Modifier = Modifier
) {
val scaledTextStyle = remember { mutableStateOf(textStyle) }
val readyToDraw = remember { mutableStateOf(false) }
val onTextLayout: (TextLayoutResult) -> Unit = { result ->
if (result.didOverflowWidth) {
val fontSize = result.size.width / result.multiParagraph.width
scaledTextStyle.value =
// scaledTextStyle.value.copy(fontSize = TextUnit(fontSize, TextUnitType.Sp))
scaledTextStyle.value.copy(fontSize = scaledTextStyle.value.fontSize * 0.9)
} else {
readyToDraw.value = true
}
}
Text(
text = text,
modifier = modifier.drawWithContent {
if (readyToDraw.value) {
drawContent()
}
},
softWrap = false,
onTextLayout = onTextLayout,
style = scaledTextStyle.value,
)
}

View file

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

View file

@ -0,0 +1,68 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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 com.tangem.wallet.R
@Composable
fun HomeButtons(
isDarkBackground: Boolean, modifier: Modifier = Modifier,
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
) {
val darkColorBackground = Color(0xFF26292E)
val darkColorButton = Color(0xFF080C10)
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = modifier
) {
Button(
modifier = Modifier
.weight(1f)
.height(48.dp),
onClick = onScanButtonClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) darkColorBackground else Color.White,
contentColor = if (isDarkBackground) Color.White else darkColorButton
)
) {
Text(
text = stringResource(id = R.string.home_button_scan),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center,
maxLines = 1
)
}
Spacer(modifier = Modifier.size(8.dp))
Button(
modifier = Modifier
.weight(1f)
.height(48.dp),
onClick = onShopButtonClick,
colors = ButtonDefaults.textButtonColors(
backgroundColor = if (isDarkBackground) Color.White else darkColorBackground,
contentColor = if (isDarkBackground) darkColorButton else Color.White
)
) {
Text(
text = stringResource(id = R.string.home_button_order),
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
textAlign = TextAlign.Center,
maxLines = 1
)
}
}
}

View file

@ -0,0 +1,157 @@
package com.tangem.tap.features.home.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
import kotlin.math.max
@Composable
fun StoriesScreen(
homeState: MutableState<HomeState> = mutableStateOf(HomeState()),
onScanButtonClick: () -> Unit,
onShopButtonClick: () -> Unit
) {
val steps = 6
val stepDuration = 8_000
val currentStep = remember { mutableStateOf(1) }
val isDarkBackground = currentStep.value !in 3..5
val goToPreviousScreen = {
currentStep.value = max(1, currentStep.value - 1)
}
val goToNextScreen = {
currentStep.value = if (currentStep.value < steps) currentStep.value + 1 else 1
}
val isPressed = remember { mutableStateOf(false) }
val needsToBePaused =
remember(homeState) { mutableStateOf(homeState.value.btnScanState.progressState == ProgressState.Loading) }
val pause = isPressed.value || needsToBePaused.value
val hideContent = remember { mutableStateOf(true) }
Box(
Modifier
.fillMaxSize()
.background(Color(0xFF090E13))
) {
Row(
Modifier.fillMaxSize()
) {
Box(
Modifier
.weight(1f)
.fillMaxHeight()
.pointerInput(isPressed) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed.value = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) goToPreviousScreen()
isPressed.value = false
},
)
}
)
Box(
Modifier.weight(1f)
.fillMaxHeight()
.pointerInput(isPressed) {
detectTapGestures(
onPress = {
val pressStartTime = System.currentTimeMillis()
isPressed.value = true
this.tryAwaitRelease()
val pressEndTime = System.currentTimeMillis()
val totalPressTime = pressEndTime - pressStartTime
if (totalPressTime < 200) goToNextScreen()
isPressed.value = false
},
)
}
)
}
if (!isDarkBackground) {
Image(
painter = painterResource(id = R.drawable.ic_overlay),
contentDescription = null,
modifier = Modifier
.fillMaxSize(),
contentScale = ContentScale.FillBounds
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
Spacer(modifier = Modifier.size(24.dp))
StoriesProgressBar(
steps = steps,
currentStep = currentStep.value,
// paused = isPressed.value,
stepDuration = stepDuration,
paused = pause,
onStepFinished = goToNextScreen,
)
Image(
painter = painterResource(id = R.drawable.ic_tangem_logo),
contentDescription = null,
contentScale = ContentScale.FillHeight,
modifier = Modifier
.padding(start = 16.dp, top = 10.dp)
.height(17.dp)
.alpha(if (hideContent.value) 0f else 1f)
.align(Alignment.Start),
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()
}
}
HomeButtons(
isDarkBackground = isDarkBackground,
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(16.dp, 0.dp, 16.dp, 37.dp)
.fillMaxWidth(),
onScanButtonClick = onScanButtonClick,
onShopButtonClick = onShopButtonClick
)
}
}
@Preview
@Composable
fun InstagramScreenPreview() {
StoriesScreen(onScanButtonClick = {}, onShopButtonClick = {})
}

View file

@ -0,0 +1,120 @@
package com.tangem.tap.features.home.compose
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 com.tangem.wallet.R
@Composable
fun StoriesGeneralContent(
titleText: String,
subtitleText: String,
imageSource: Int?,
isDarkBackground: Boolean,
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
)
Spacer(modifier = Modifier.size(16.dp))
Text(
text = subtitleText,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
modifier = Modifier
.padding(start = 40.dp, end = 40.dp),
color = Color(0xFFA6AAAD),
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.size(25.dp))
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 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,
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
)
}

View file

@ -0,0 +1,80 @@
package com.tangem.tap.features.home.compose
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@Composable
fun StoriesProgressBar(
steps: Int,
currentStep: Int,
paused: Boolean = false,
stepDuration: Int = 8_000,
onStepFinished: () -> Unit = {},
) {
val progress = remember(currentStep) { Animatable(0f) }
LaunchedEffect(paused, currentStep) {
if (paused) {
progress.stop()
} else {
progress.animateTo(
targetValue = 1f,
animationSpec = tween(
durationMillis = (stepDuration * (1f - progress.value)).toInt(),
easing = LinearEasing
)
)
progress.snapTo(0f)
onStepFinished()
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
// .height()
.padding(start = 9.dp, end = 9.dp, top = 16.dp),
) {
for (index in 1..steps) {
Row(
modifier = Modifier
.height(2.dp)
.weight(1f)
.background(Color.White.copy(alpha = 0.4f))
) {
Box(
modifier = Modifier
.background(Color.White)
.fillMaxHeight().let {
when (index) {
currentStep -> it.fillMaxWidth(progress.value)
in 0..currentStep -> it.fillMaxWidth(1f)
else -> it
}
},
) {}
}
if (index != steps) {
Spacer(modifier = Modifier.width(4.dp))
}
}
}
}
@Preview
@Composable
fun StoriesProgressBarPreview() {
StoriesProgressBar(steps = 3, currentStep = 2, paused = false) { }
}

View file

@ -1,7 +1,6 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import org.rekotlin.Action
sealed class HomeAction : Action {
@ -10,10 +9,9 @@ sealed class HomeAction : Action {
object GoToShop : HomeAction()
// internal
data class ShouldScanCardOnResume(val shouldScanCard: Boolean):HomeAction()
data class ShouldScanCardOnResume(val shouldScanCard: Boolean) : HomeAction()
object Init : HomeAction()
data class SetFragmentShareTransition(val shareTransition: FragmentShareTransition?) : HomeAction()
data class SetTermsOfUseState(val isDisclaimerAccepted: Boolean) : HomeAction()
data class ChangeScanCardButtonState(val state: IndeterminateProgressButton) : HomeAction()

View file

@ -4,7 +4,6 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
@ -39,7 +38,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.GetMoonPayStatus)
store.dispatch(GlobalAction.InitCurrencyExchangeManager)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
}
is HomeAction.ShouldScanCardOnResume -> {
@ -50,7 +49,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.GoToShop -> {
store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.WELCOME.param)
@ -74,14 +73,14 @@ private fun handleReadCard() {
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo, store.state.homeState.shareTransition)
navigateTo(navigateTo)
} else {
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins, store.state.homeState.shareTransition)
navigateTo(AppScreen.OnboardingTwins)
} else {
navigateTo(AppScreen.Wallet, null)
}
@ -99,7 +98,7 @@ private fun changeButtonState(state: ButtonState) {
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition?) {
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition? = null) {
post(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))

View file

@ -15,9 +15,6 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
var state = state.homeState
when (action) {
is HomeAction.SetFragmentShareTransition -> {
state = state.copy(shareTransition = action.shareTransition)
}
is HomeAction.ShouldScanCardOnResume -> {
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
}

View file

@ -1,12 +1,10 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.send.redux.states.ButtonState
import org.rekotlin.StateType
data class HomeState(
val shouldScanCardOnResume: Boolean = false,
val shareTransition: FragmentShareTransition? = null,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
) : StateType

View file

@ -1,6 +1,8 @@
package com.tangem.tap.features.onboarding
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.copyToClipboard
@ -8,11 +10,11 @@ import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchShare
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_onboarding_address_info.*
import kotlinx.android.synthetic.main.layout_pseudo_toolbar.*
import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding
/**
[REDACTED_AUTHOR]
@ -22,30 +24,40 @@ class AddressInfoBottomSheetDialog(
context: Context
) : BottomSheetDialog(context) {
init {
this.setContentView(R.layout.dialog_onboarding_address_info)
var binding: DialogOnboardingAddressInfoBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DialogOnboardingAddressInfoBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
setOnCancelListener { store.dispatchDialogHide() }
setOnCancelListener {
store.dispatchDialogHide()
binding = null
}
}
override fun show() {
super.show()
val data = stateDialog.addressData
showData(data = stateDialog.addressData)
}
imv_close.setOnClickListener {
private fun showData(data: AddressData) = with(binding!!) {
pseudoToolbar.imvClose.setOnClickListener {
dismissWithAnimation = true
cancel()
}
imv_qr_code.setImageBitmap(data.qrCode)
tv_address.text = data.address
btn_fl_copy_address.setOnClickListener {
imvQrCode.setImageBitmap(data.qrCode)
tvAddress.text = data.address
btnFlCopyAddress.setOnClickListener {
context.copyToClipboard(data.address)
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btn_fl_share.setOnClickListener {
btnFlShare.setOnClickListener {
store.dispatchShare(data.shareUrl)
}
tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, stateDialog.currency)
tvReceiveMessage.text = getQRReceiveMessage(tvReceiveMessage.context, stateDialog.currency)
}
}

View file

@ -1,14 +1,12 @@
package com.tangem.tap.features.onboarding.products
import android.os.Bundle
import android.view.View
import com.tangem.tap.common.extensions.inflate
import by.kirich1409.viewbindingdelegate.viewBinding
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.view_confetti.*
import com.tangem.wallet.databinding.FragmentOnboardingMainBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import org.rekotlin.StoreSubscriber
/**
@ -16,26 +14,22 @@ import org.rekotlin.StoreSubscriber
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
protected val binding: FragmentOnboardingMainBinding by viewBinding(FragmentOnboardingMainBinding::bind)
protected val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
override fun configureTransitions() {
enterTransition = HomeToOnboardingTransition()
exitTransition = HomeToOnboardingTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onboarding_top_container.inflate(getOnboardingTopContainerId(), true)
}
abstract fun getOnboardingTopContainerId(): Int
protected fun showConfetti(show: Boolean) {
lav_confetti.show(show)
protected fun showConfetti(show: Boolean) = with(binding.vConfetti) {
lavConfetti.show(show)
if (show) {
lav_confetti.playAnimation()
lavConfetti.playAnimation()
} else {
lav_confetti.cancelAnimation()
lavConfetti.cancelAnimation()
}
}
}

View file

@ -26,11 +26,6 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStat
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
/**
[REDACTED_AUTHOR]
@ -41,8 +36,6 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
@ -52,11 +45,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
imv_front_card.transitionName = ShareElement.imvFrontCard
binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
binding.toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(binding.onboardingTopContainer.onboardingMainContainer)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
@ -72,19 +65,19 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
override fun newState(state: OnboardingNoteState) {
if (activity == null) return
if (activity == null || view == null) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(imv_front_card)
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingNoteStep.None -> setupNoneState(state)
OnboardingNoteStep.None -> setupNoneState()
OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state)
OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state)
OnboardingNoteStep.Done -> setupDoneState(state)
@ -97,77 +90,82 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private fun setBalance(state: OnboardingNoteState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tv_balance_value.text = balanceValue
tv_balance_currency.text = state.walletBalance.currency.currencySymbol
} else {
tv_balance_value.text = ""
tv_balance_currency.text = ""
with (binding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
private fun setupNoneState(state: OnboardingNoteState) {
btn_main_action.isVisible = false
btn_alternative_action.isVisible = false
private fun setupNoneState() = with(binding.onboardingActionContainer) {
btnMainAction.isVisible = false
btnAlternativeAction.isVisible = false
btnRefreshBalanceWidget.show(false)
tv_header.isVisible = false
tv_body.isVisible = false
tvHeader.isVisible = false
tvBody.isVisible = false
btn_main_action.text = ""
btn_alternative_action.text = ""
tv_header.text = ""
tv_body.text = ""
btnMainAction.text = ""
btnAlternativeAction.text = ""
tvHeader.text = ""
tvBody.text = ""
}
private fun setupCreateWalletState(state: OnboardingNoteState) {
btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet)
btn_main_action.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) }
btn_alternative_action.setText(R.string.onboarding_button_what_does_it_mean)
btn_alternative_action.setOnClickListener { }
private fun setupCreateWalletState(state: OnboardingNoteState) =
with(binding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setOnClickListener { store.dispatch(OnboardingNoteAction.CreateWallet) }
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle)
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
btn_alternative_action.isVisible = false // temporary
}
btnAlternativeAction.isVisible = false // temporary
}
private fun setupTopUpWalletState(state: OnboardingNoteState) {
private fun setupTopUpWalletState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) {
if (state.isBuyAllowed) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btn_alternative_action.isVisible = true
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
} else {
btn_main_action.setText(R.string.onboarding_button_receive_crypto)
btn_main_action.setOnClickListener {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
btn_alternative_action.isVisible = false
btnAlternativeAction.isVisible = false
}
tv_header.setText(R.string.onboarding_top_up_header)
tvHeader.setText(R.string.onboarding_top_up_header)
if (state.balanceNonCriticalError == null) {
tv_body.setText(R.string.onboarding_top_up_body)
tvBody.setText(R.string.onboarding_top_up_body)
} else {
state.walletBalance.amountToCreateAccount?.let { amount ->
val tvBodyMessage = getString(
R.string.onboarding_top_up_body_no_account_error,
amount, state.walletBalance.currency.currencySymbol
)
tv_body.text = tvBodyMessage
tvBody.text = tvBodyMessage
}
}
@ -177,38 +175,43 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
binding.onboardingTopContainer.imvCardBackground
.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet)
}
private fun setupDoneState(state: OnboardingNoteState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
private fun setupDoneState(state: OnboardingNoteState) = with(binding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_done_button_continue)
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.setText("")
btn_alternative_action.setOnClickListener { }
btnAlternativeAction.isVisible = false
btnAlternativeAction.setText("")
btnAlternativeAction.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation)
}
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
}
}
}

View file

@ -13,6 +13,7 @@ import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
@ -33,14 +34,16 @@ class OnboardingNoteMiddleware {
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(action, dispatch)
handleNoteAction(state, action, dispatch)
next(action)
}
}
}
private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
if (DemoHelper.tryHandle(appState, action)) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager ?: return

View file

@ -27,7 +27,7 @@ data class OnboardingNoteState(
get() = steps.indexOf(currentStep)
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
}
}

View file

@ -18,18 +18,12 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
@ -39,10 +33,10 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
imv_front_card.transitionName = ShareElement.imvFrontCard
binding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
toolbar.setTitle(R.string.onboarding_title)
binding.toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
@ -57,64 +51,75 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
}
override fun newState(state: OnboardingOtherCardsState) {
if (activity == null) return
if (activity == null || view == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
Picasso.get()
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(imv_front_card)
.load(state.cardArtworkUrl)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvFrontCard)
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState(state)
OnboardingOtherCardsStep.Done -> setupDoneState(state)
OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState()
OnboardingOtherCardsStep.Done -> setupDoneState()
}
showConfetti(state.showConfetti)
}
private fun setupCreateWalletState(state: OnboardingOtherCardsState) {
btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet)
btn_main_action.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) }
btn_alternative_action.setText(R.string.onboarding_button_what_does_it_mean)
btn_alternative_action.setOnClickListener { }
private fun setupCreateWalletState() = with(binding) {
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
with(onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
btnMainAction.setOnClickListener { store.dispatch(OnboardingOtherCardsAction.CreateWallet) }
btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean)
btnAlternativeAction.setOnClickListener { }
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
btnAlternativeAction.isVisible = false // temporary
}
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_circle)
)
updateConstraints(R.layout.lp_onboarding_create_wallet)
btn_alternative_action.isVisible = false // temporary
}
private fun setupDoneState(state: OnboardingOtherCardsState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
private fun setupDoneState() = with(binding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_done_button_continue)
btnMainAction.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingOtherCardsAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.setText("")
btn_alternative_action.setOnClickListener { }
btnAlternativeAction.isVisible = false
btnAlternativeAction.setText("")
btnAlternativeAction.setOnClickListener { }
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
binding.onboardingTopContainer.imvCardBackground?.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
)
updateConstraints(R.layout.lp_onboarding_done)
}
private fun updateConstraints(@LayoutRes layoutId: Int) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
}
}
}

View file

@ -57,7 +57,7 @@ data class TwinCardsState(
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
store.state.globalState.currencyExchangeManager?.buyIsAllowed(walletBalance.currency) ?: false
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
@ -31,12 +32,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_bg_twins_welcome.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
@ -47,12 +43,11 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private val assetReader: AssetReader by lazy {
object : AssetReader {
override fun readAssetAsString(name: String): String = requireContext().readAssetAsString(name)
override fun readAssetAsString(name: String): String =
requireContext().readAssetAsString(name)
}
}
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
@ -74,36 +69,44 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
reconfigureLayoutForTwins()
twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 285f }
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
reconfigureLayoutForTwins(binding.onboardingTopContainer)
toolbar.title = getText(R.string.twins_recreate_toolbar)
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactorForWelcomeState = typedValue.float
twinsWidget = TwinsCardWidget(LeapfrogWidget(binding.onboardingTopContainer.cardsContainer), deviceScaleFactorForWelcomeState) {
285f * deviceScaleFactorForWelcomeState
}
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(imv_twin_front_card)
.load(Artwork.TWIN_CARD_1)
.error(R.drawable.card_placeholder_black)
.placeholder(R.drawable.card_placeholder_black)
?.into(binding.onboardingTopContainer.imvTwinFrontCard)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.error(R.drawable.card_placeholder_white)
.placeholder(R.drawable.card_placeholder_white)
?.into(imv_twin_back_card)
.load(Artwork.TWIN_CARD_2)
.error(R.drawable.card_placeholder_white)
.placeholder(R.drawable.card_placeholder_white)
?.into(binding.onboardingTopContainer.imvTwinBackCard)
}
private fun reconfigureLayoutForTwins() {
imv_front_card.hide()
imv_card_background.hide()
cards_container.show()
private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) =
with(containerBinding) {
imvFrontCard.hide()
imvCardBackground.hide()
cardsContainer.show()
imv_twin_front_card.transitionName = ShareElement.imvFrontCard
imv_twin_back_card.transitionName = ShareElement.imvBackCard
imvTwinFrontCard.transitionName = ShareElement.imvFrontCard
imvTwinBackCard.transitionName = ShareElement.imvBackCard
// if don't this, the bg_circle_... is overflow the app_bar
app_bar.bringToFront()
}
// if don't this, the bg_circle_... is overflow the app_bar
binding.appBar.bringToFront()
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
@ -115,10 +118,10 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
override fun newState(state: TwinCardsState) {
if (activity == null) return
if (activity == null || view == null) return
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
pbBinding.pbState.max = state.steps.size - 1
pbBinding.pbState.progress = state.progress
when (state.currentStep) {
TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state)
@ -139,13 +142,15 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private fun setBalance(state: TwinCardsState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tv_balance_value.text = balanceValue
tv_balance_currency.text = state.walletBalance.currency.currencySymbol
} else {
tv_balance_value.text = ""
tv_balance_currency.text = ""
with(binding.onboardingTopContainer.onboardingTvBalance) {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
tvBalanceValue.text = balanceValue
tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol
} else {
tvBalanceValue.text = ""
tvBalanceCurrency.text = ""
}
}
}
@ -160,203 +165,235 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
setupWelcomeState(state) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) }
}
private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) {
private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) =
with(binding.onboardingActionContainer) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show()
pbBinding.pbState.hide()
tvHeader.setText(R.string.twins_onboarding_subtitle)
tvBody.text = getString(
R.string.twins_onboarding_description_format,
state.cardNumber?.pairIndexNumber()
)
btnMainAction.setText(R.string.common_continue)
btnMainAction.setOnClickListener { mainAction() }
}
private fun setupWarningState(state: TwinCardsState) = with(binding.onboardingActionContainer) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
onboarding_twins_welcome_bg.show()
pb_state.hide()
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
pbBinding.pbState.hide()
chbUnderstand.show()
tv_header.setText(R.string.twins_onboarding_subtitle)
tv_body.text = getString(R.string.twins_onboarding_description_format, state.cardNumber?.pairIndexNumber())
tvHeader.setText(R.string.common_warning)
tvBody.setText(R.string.twins_recreate_warning)
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener { mainAction() }
}
private fun setupWarningState(state: TwinCardsState) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
onboarding_twins_welcome_bg.hide()
pb_state.hide()
chb_understand.show()
tv_header.setText(R.string.common_warning)
tv_body.setText(R.string.twins_recreate_warning)
chb_understand.setOnCheckedChangeListener { buttonView, isChecked ->
chbUnderstand.setOnCheckedChangeListener { buttonView, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btn_main_action.isEnabled = state.userWasUnderstandIfWalletRecreate
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener {
btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate
btnMainAction.setText(R.string.common_continue)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupCreateFirstWalletState(state: TwinCardsState) {
onboarding_twins_welcome_bg.hide()
bg_circle_large.hide()
bg_circle_medium.hide()
bg_circle_min.hide()
chb_understand.hide()
pb_state.show()
private fun setupCreateFirstWalletState(state: TwinCardsState) =
with(binding.onboardingActionContainer) {
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide()
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide()
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide()
binding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide()
chbUnderstand.hide()
pbBinding.pbState.show()
onboarding_main_container.beginDelayedTransition()
binding.onboardingTopContainer.onboardingMainContainer.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> startPostponedEnterTransition()
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) { startPostponedEnterTransition() }
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> startPostponedEnterTransition()
TwinCardNumber.Second -> {
switchToCard(
state.cardNumber,
false
) { startPostponedEnterTransition() }
}
}
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
assetReader
)
)
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) =
with(binding.onboardingActionContainer) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text =
getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchSecondStep(
Message(
getString(
R.string.twins_recreate_title_format,
twinPairIndexNumber
)
),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
)
)
}
}
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
private fun setupCreateThirdWalletState(state: TwinCardsState) =
with(binding.onboardingActionContainer) {
switchToCard(state.cardNumber)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
assetReader
))
val twinIndexNumber = state.cardNumber?.indexNumber()
tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tvBody.setText(R.string.onboarding_twins_interrupt_warning)
btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btnMainAction.setOnClickListener {
store.dispatch(
TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber))
)
)
}
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep(
Message(getString(R.string.twins_recreate_title_format, twinPairIndexNumber)),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
))
}
}
private fun setupCreateThirdWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber)
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber))
))
}
}
private fun setupTopUpWalletState(state: TwinCardsState) {
when (previousStep) {
TwinCardsStep.None -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) {
twinsWidget.toActivate(false) {
private fun setupTopUpWalletState(state: TwinCardsState) =
with(binding.onboardingActionContainer) {
when (previousStep) {
TwinCardsStep.None -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) {
twinsWidget.toActivate(false) {
startPostponedEnterTransition()
}
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) {
twinsWidget.toActivate(false)
startPostponedEnterTransition()
}
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) {
twinsWidget.toActivate(false)
startPostponedEnterTransition()
}
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
}
if (state.isBuyAllowed) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
if (state.isBuyAllowed) {
btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btnAlternativeAction.isVisible = true
btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address)
btnAlternativeAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
} else {
btnMainAction.setText(R.string.onboarding_button_receive_crypto)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
btnAlternativeAction.isVisible = false
}
btn_alternative_action.isVisible = true
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
tvHeader.setText(R.string.onboarding_top_up_header)
tvBody.setText(R.string.onboarding_top_up_body)
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
}
} else {
btn_main_action.setText(R.string.onboarding_button_receive_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
binding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(
requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8)
)
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) =
with(binding.onboardingActionContainer) {
btnMainAction.setText(R.string.onboarding_done_button_continue)
btnMainAction.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btn_alternative_action.isVisible = false
}
btnAlternativeAction.isVisible = false
btnAlternativeAction.isClickable = false
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
tvHeader.setText(R.string.onboarding_done_header)
tvBody.setText(R.string.onboarding_done_body)
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
val layout = when (state.mode) {
CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
with(binding.onboardingTopContainer) {
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboardingMainContainer)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboardingMainContainer, transition)
}
}
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
private fun switchToCard(
cardNumber: TwinCardNumber?,
animate: Boolean = true,
onEnd: VoidCallback = {}
) {
val position = cardNumber?.number ?: return
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {

View file

@ -8,7 +8,6 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.analytics.GetCardSourceParams
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
@ -16,6 +15,7 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL
import com.tangem.tap.features.wallet.redux.Artwork
@ -124,10 +124,15 @@ private fun handleWalletAction(action: Action) {
} else {
null
}
BackupAction.IntroduceBackup(url)
if (walletState.backupState.backupStep == BackupStep.InitBackup ||
walletState.backupState.backupStep == BackupStep.Finished) {
BackupAction.IntroduceBackup(url)
} else {
null
}
}
}
store.dispatch(newAction)
newAction?.let { store.dispatch(it) }
}
OnboardingWalletAction.OnBackPressed -> {
when (walletState.backupState.backupStep) {
@ -164,14 +169,16 @@ class BackupMiddleware {
val backupMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
if (action is BackupAction) handleBackupAction(action)
if (action is BackupAction) handleBackupAction(state, action)
next(action)
}
}
}
}
private fun handleBackupAction(action: BackupAction) {
private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) {
if (DemoHelper.tryHandle(appState, action)) return
val backupState = store.state.onboardingWalletState.backupState
val globalState = store.state.globalState
@ -215,13 +222,11 @@ private fun handleBackupAction(action: BackupAction) {
}
}
is BackupAction.GoToShop -> {
backupState.buyAdditionalCardsUrl?.let { url ->
store.dispatchOpenUrl(url)
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.ONBOARDING.param)
)
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Shop))
store.state.globalState.analyticsHandlers?.triggerEvent(
event = AnalyticsEvent.GET_CARD,
params = mapOf(AnalyticsParam.SOURCE.param to GetCardSourceParams.ONBOARDING.param)
)
}
is BackupAction.FinishAddingBackupCards -> {
if (backupService.addedBackupCardsCount == 1) {
@ -233,9 +238,11 @@ private fun handleBackupAction(action: BackupAction) {
}
is BackupAction.CheckAccessCode -> {
if (action.accessCode.length < 4) {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
))
store.dispatch(
BackupAction.SetAccessCodeError(
AccessCodeError.CodeTooShort
)
)
} else {
store.dispatch(BackupAction.SetAccessCodeError(null))
store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode))
@ -247,9 +254,11 @@ private fun handleBackupAction(action: BackupAction) {
backupService.setAccessCode(action.accessCodeConfirmation)
store.dispatch(BackupAction.PrepareToWritePrimaryCard)
} else {
store.dispatch(BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
))
store.dispatch(
BackupAction.SetAccessCodeError(
AccessCodeError.CodesDoNotMatch
)
)
}
}
is BackupAction.WritePrimaryCard -> {

View file

@ -1,28 +1,30 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import com.tangem.tap.common.extensions.getString
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_backup_info_adapter.view.*
import com.tangem.wallet.databinding.ItemBackupInfoAdapterBinding
class BackupInfoAdapter : RecyclerView.Adapter<BackupInfoViewHolder>() {
private val data = backupInfoSnippets
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder =
BackupInfoViewHolder(LayoutInflater.from(parent.context)
.inflate(R.layout.item_backup_info_adapter, parent, false))
BackupInfoViewHolder(
ItemBackupInfoAdapterBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
)
override fun getItemCount(): Int = data.size
override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) =
holder.itemView.run {
tv_header.text = getString(data[position].header)
tv_body.text = getString(data[position].body)
holder.binding.run {
tvHeader.text = root.getString(data[position].header)
tvBody.text = root.getString(data[position].body)
}
}
@ -51,4 +53,5 @@ private val backupInfoSnippets = listOf(
)
class BackupInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
class BackupInfoViewHolder(val binding: ItemBackupInfoAdapterBinding) :
RecyclerView.ViewHolder(binding.root)

View file

@ -1,18 +1,21 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import android.os.Bundle
import android.util.TypedValue
import android.view.*
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
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
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.FragmentOnBackPressedHandler
@ -21,18 +24,21 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_wallet.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_add_cards.*
import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.*
import kotlinx.android.synthetic.main.view_confetti.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import org.rekotlin.StoreSubscriber
class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>, FragmentOnBackPressedHandler {
private var accessCodeDialog: AccessCodeDialog? = null
private lateinit var cardsWidget: BackupCardsWidget
private lateinit var cardsWidget: WalletCardsWidget
private val binding: FragmentOnboardingWalletBinding by viewBinding(
FragmentOnboardingWalletBinding::bind
)
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(
ViewOnboardingProgressBinding::bind)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -47,19 +53,29 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrog = LeapfrogWidget(fl_cards_container)
cardsWidget = BackupCardsWidget(leapfrog) { 200f }
val typedValue = TypedValue()
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
val deviceScaleFactor = typedValue.float
val leapfrogCalculator = PropertyCalculator(
yTranslationFactor = 25f * deviceScaleFactor,
)
val leapfrog = LeapfrogWidget(binding.flCardsContainer, leapfrogCalculator)
cardsWidget = WalletCardsWidget(leapfrog, deviceScaleFactor) { 200f * deviceScaleFactor }
startPostponedEnterTransition()
view_pager_backup_info.adapter = BackupInfoAdapter()
TabLayoutMediator(tab_layout_backup_info, view_pager_backup_info) { tab, position ->
binding.viewPagerBackupInfo.adapter = BackupInfoAdapter()
TabLayoutMediator(
binding.tabLayoutBackupInfo,
binding.viewPagerBackupInfo
) { tab, position ->
//Some implementation
}.attach()
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
(activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar)
store.dispatch(OnboardingWalletAction.Init)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
store.dispatch(OnboardingWalletAction.LoadArtwork)
@ -81,16 +97,18 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
override fun newState(state: OnboardingWalletState) {
if (activity == null) return
if (activity == null || view == null) return
requireActivity().invalidateOptionsMenu()
loadImageIntoImageView(state.cardArtworkUrl, imv_front_card)
loadImageIntoImageView(state.cardArtworkUrl, imv_first_backup_card)
loadImageIntoImageView(state.cardArtworkUrl, imv_second_backup_card)
with(binding) {
loadImageIntoImageView(state.cardArtworkUrl, imvFrontCard)
loadImageIntoImageView(state.cardArtworkUrl, imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUrl, imvSecondBackupCard)
}
pb_state.max = 6
pb_state.progress = state.getProgressStep()
pbBinding.pbState.max = 6
pbBinding.pbState.progress = state.getProgressStep()
when (state.step) {
OnboardingWalletStep.None -> {
@ -111,25 +129,24 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
?.into(view)
}
private fun setupCreateWalletState() {
btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet)
btn_main_action.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
btn_alternative_action.hide()
private fun setupCreateWalletState() = with(binding) {
layoutButtonsCommon.btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) }
layoutButtonsCommon.btnAlternativeAction.hide()
toolbar.title = getText(R.string.onboarding_getting_started)
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
tvHeader.setText(R.string.onboarding_create_wallet_header)
tvBody.setText(R.string.onboarding_create_wallet_body)
cardsWidget.toFolded()
startPostponedEnterTransition()
cardsWidget.toFolded(false) { startPostponedEnterTransition() }
}
private fun setBackupState(state: BackupState) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state)
BackupStep.ScanOriginCard -> showScanOriginCard(state)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state)
BackupStep.SetAccessCode -> showSetAccessCode()
BackupStep.EnterAccessCode -> showEnterAccessCode(state)
@ -140,65 +157,70 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
}
private fun showBackupIntro(state: BackupState) {
imv_first_backup_card.show()
imv_second_backup_card.show()
private fun showBackupIntro(state: BackupState) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
cardsWidget.toWelcome()
tv_header.hide()
tv_body.hide()
view_pager_backup_info.show()
tab_layout_backup_info.show()
tvHeader.hide()
tvBody.hide()
viewPagerBackupInfo.show()
tabLayoutBackupInfo.show()
with(layoutButtonsCommon) {
btnMainAction.text = getText(R.string.onboarding_button_backup_now)
btnAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnAlternativeAction.show(state.canSkipBackup)
btnMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btnAlternativeAction.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
}
btn_main_action.text = getText(R.string.onboarding_button_backup_now)
btn_alternative_action.text = getText(R.string.onboarding_button_skip_backup)
btn_alternative_action.show(state.canSkipBackup)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.StartBackup) }
btn_alternative_action.setOnClickListener { store.dispatch(BackupAction.DismissBackup) }
startPostponedEnterTransition()
}
private fun showScanOriginCard(state: BackupState) {
private fun showScanOriginCard() = with(binding) {
prepareBackupView()
cardsWidget.toFolded()
tv_header.text = getText(R.string.onboarding_title_scan_origin_card)
tv_body.text = getString(
tvHeader.text = getText(R.string.onboarding_title_scan_origin_card)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_origin_card,
)
btn_main_action.text = getString(R.string.onboarding_button_scan_origin_card)
btn_alternative_action.hide()
btn_main_action.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
with(layoutButtonsCommon) {
btnMainAction.text = getString(R.string.onboarding_button_scan_origin_card)
btnAlternativeAction.hide()
btnMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) }
}
}
private fun prepareBackupView() {
private fun prepareBackupView() = with(binding) {
toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup)
tv_header.show()
tv_body.show()
view_pager_backup_info.hide()
tab_layout_backup_info.hide()
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
imv_first_backup_card.show()
imv_second_backup_card.show()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
}
private fun showAddBackupCards(state: BackupState) {
private fun showAddBackupCards(state: BackupState) = with(binding) {
prepareBackupView()
accessCodeDialog?.dismiss()
accessCodeDialog = null
layout_buttons_add_cards.show()
layout_buttons_common.hide()
btn_add_card.text = getText(R.string.onboarding_button_add_backup_card)
layoutButtonsAddCards.root.show()
layoutButtonsCommon.root.hide()
layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card)
if (state.backupCardsNumber < state.maxBackupCards) {
btn_add_card.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) }
layoutButtonsAddCards.btnAddCard.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) }
} else {
btn_add_card.isEnabled = false
layoutButtonsAddCards.btnAddCard.isEnabled = false
}
when (state.backupCardsNumber) {
@ -207,27 +229,30 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
cardsWidget.getFirstBackupCardView().animate().alpha(0.6f).setDuration(200)
cardsWidget.getSecondBackupCardView().animate().alpha(0.2f).setDuration(200)
}
tv_header.text = getText(R.string.onboarding_title_no_backup_cards)
tv_body.text = getText(R.string.onboarding_subtitle_no_backup_cards)
tvHeader.text = getText(R.string.onboarding_title_no_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards)
}
1 -> {
tv_header.text = getText(R.string.onboarding_title_one_backup_card)
tv_body.text = getText(R.string.onboarding_subtitle_one_backup_card)
tvHeader.text = getText(R.string.onboarding_title_one_backup_card)
tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card)
cardsWidget.toFan(false)
cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400)
cardsWidget.getSecondBackupCardView().alpha = 0.2f
}
2 -> {
tv_header.text = getText(R.string.onboarding_title_two_backup_cards)
tv_body.text = getText(R.string.onboarding_subtitle_two_backup_cards)
tvHeader.text = getText(R.string.onboarding_title_two_backup_cards)
tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards)
cardsWidget.toFan(false)
cardsWidget.getFirstBackupCardView().alpha = 1f
cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400)
}
}
btn_continue.text = getText(R.string.onboarding_button_finalize_backup)
btn_continue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
btn_continue.isEnabled = state.backupCardsNumber != 0
layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup)
layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) }
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
}
private fun showSetAccessCode() {
@ -258,7 +283,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
accessCodeDialog?.showError(state.accessCodeError)
}
private fun showWritePrimaryCard(state: BackupState) {
private fun showWritePrimaryCard(state: BackupState) = with(binding) {
accessCodeDialog?.dismiss()
prepareViewForFinalizeStep()
@ -270,33 +295,33 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tv_header.text = getText(R.string.onboarding_title_prepare_origin)
tv_body.text = getString(
tvHeader.text = getText(R.string.onboarding_title_prepare_origin)
tvBody.text = getString(
R.string.onboarding_subtitle_scan_primary_card_format,
state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }
)
btn_main_action.text = getText(R.string.onboarding_button_backup_origin)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_backup_origin)
layoutButtonsCommon.btnMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) }
}
private fun prepareViewForFinalizeStep() {
layout_buttons_add_cards.hide()
layout_buttons_common.show()
private fun prepareViewForFinalizeStep() = with(binding) {
layoutButtonsAddCards.root.hide()
layoutButtonsCommon.root.show()
imv_first_backup_card.show()
imv_second_backup_card.show()
imvFirstBackupCard.show()
imvSecondBackupCard.show()
toolbar.title = getText(R.string.onboarding_button_finalize_backup)
imv_card_background.hide()
imvCardBackground.hide()
cardsWidget.toLeapfrog()
btn_alternative_action.hide()
layoutButtonsCommon.btnAlternativeAction.hide()
}
private fun showWriteBackupCard(state: BackupState) {
private fun showWriteBackupCard(state: BackupState) = with(binding) {
prepareViewForFinalizeStep()
cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2)
@ -316,46 +341,47 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
}
val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4))
tv_header.text =
tvHeader.text =
getString(R.string.onboarding_title_backup_card_format, cardNumber)
tv_body.text = getString(
tvBody.text = getString(
R.string.onboarding_subtitle_scan_backup_card_format,
cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1])
)
btn_main_action.text = getString(
layoutButtonsCommon.btnMainAction.text = getString(
R.string.onboarding_button_backup_card_format,
cardNumber
)
btn_main_action.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) }
layoutButtonsCommon.btnMainAction.setOnClickListener {
store.dispatch(BackupAction.WriteBackupCard(cardNumber))
}
}
private fun showSuccess() {
tv_header.text = getText(R.string.onboarding_done_header)
private fun showSuccess() = with(binding) {
tvHeader.text = getText(R.string.onboarding_done_header)
tv_header.show()
tv_body.show()
view_pager_backup_info.hide()
tab_layout_backup_info.hide()
tvHeader.show()
tvBody.show()
viewPagerBackupInfo.hide()
tabLayoutBackupInfo.hide()
tv_body.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
btn_main_action.text = getText(R.string.onboarding_button_continue_wallet)
btn_alternative_action.hide()
btn_main_action.setOnClickListener {
lav_confetti.cancelAnimation()
lav_confetti.hide()
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
layoutButtonsCommon.btnMainAction.text = getText(R.string.onboarding_button_continue_wallet)
layoutButtonsCommon.btnAlternativeAction.hide()
layoutButtonsCommon.btnMainAction.setOnClickListener {
vConfetti.lavConfetti.cancelAnimation()
vConfetti.lavConfetti.hide()
store.dispatch(OnboardingWalletAction.FinishOnboarding)
}
cardsWidget.leapfrogWidget.fold {
fl_cards_container?.hide()
imv_card_background?.hide()
lav_confetti?.show()
lav_confetti?.playAnimation()
imv_success?.alpha = 0f
imv_success?.show()
flCardsContainer.hide()
imvCardBackground.hide()
vConfetti.lavConfetti.show()
vConfetti.lavConfetti.playAnimation()
imvSuccess.alpha = 0f
imvSuccess.show()
imv_success?.animate()
imvSuccess.animate()
?.alpha(1f)
?.setDuration(400)
}
@ -379,7 +405,7 @@ class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet),
val shopMenuShouldBeVisible =
(backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards) &&
backupState.buyAdditionalCardsUrl != null
backupState.buyAdditionalCardsUrl != null
menu.getItem(0).isVisible = shopMenuShouldBeVisible
}

View file

@ -10,8 +10,9 @@ import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapView
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapViewState
import com.tangem.tangem_sdk_new.ui.widget.leapfrogWidget.LeapfrogWidget
class BackupCardsWidget(
class WalletCardsWidget(
val leapfrogWidget: LeapfrogWidget,
private val deviceScaleFactor: Float = 1f,
val getTopOfAnchorViewForActivateState: () -> Float,
) {
@ -42,7 +43,7 @@ class BackupCardsWidget(
createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)),
createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP))
)
leapfrogWidget.fold { animator.start() }
leapfrogWidget.fold(animate) { animator.start() }
}
fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) {
@ -98,26 +99,26 @@ class BackupCardsWidget(
private fun createWelcomeProperties(cardType: BackupCardType): CardProperties {
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 440f,
yTranslation = 0f,
rotation = 75f,
elevation = 2f,
scale = .9f,
)
xTranslation = 440f * deviceScaleFactor,
yTranslation = 70f,
rotation = 75f,
elevation = 2f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -100f,
rotation = 100f,
elevation = 1f,
scale = .9f,
)
xTranslation = 10f * deviceScaleFactor,
yTranslation = -30f,
rotation = 100f,
elevation = 1f,
scale = .85f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = -440f,
yTranslation = -100f,
rotation = 70f,
elevation = 0f,
scale = .9f,
)
xTranslation = -440f * deviceScaleFactor,
yTranslation = -30f,
rotation = 70f,
elevation = 0f,
scale = .85f * deviceScaleFactor,
)
}
}
@ -125,24 +126,24 @@ class BackupCardsWidget(
return when (cardType) {
BackupCardType.ORIGIN -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
yTranslation = 30f,
rotation = 5f,
elevation = 2f,
scale = 1f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = -80f,
yTranslation = -50f,
rotation = -5f,
elevation = 1f,
scale = 0.9f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 10f,
yTranslation = -170f,
yTranslation = -120f,
rotation = -20f,
elevation = 0f,
scale = 0.8f,
scale = 0.8f * deviceScaleFactor,
)
}
}
@ -154,21 +155,21 @@ class BackupCardsWidget(
yTranslation = 0f,
rotation = 0f,
elevation = 2f,
scale = 1f,
scale = 1f * deviceScaleFactor,
)
BackupCardType.FIRST_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 1f,
scale = 0.9f,
scale = 0.9f * deviceScaleFactor,
)
BackupCardType.SECOND_BACKUP -> CardProperties(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
elevation = 0f,
scale = 0.8f,
scale = 0.8f * deviceScaleFactor,
)
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.hide
@ -11,64 +12,69 @@ import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.layout_backup_access_code.*
import kotlinx.android.synthetic.main.layout_backup_access_code_info.*
import kotlinx.android.synthetic.main.layout_backup_access_code_submit.*
import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding
class AccessCodeDialog(context: Context) : BottomSheetDialog(context) {
var binding: LayoutBackupAccessCodeBinding? = null
init {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
val dialogView = LayoutInflater.from(context).inflate(R.layout.layout_backup_access_code, null)
setContentView(dialogView)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutBackupAccessCodeBinding
.inflate(LayoutInflater.from(context))
setContentView(binding!!.root)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
override fun setContentView(view: View) {
super.setContentView(view)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) {
super.setOnDismissListener(listener)
binding = null
}
fun showInfoScreen() {
fun showInfoScreen() = with(binding!!) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title)
layout_backup_access_code_info.show()
layout_backup_access_code_submit.hide()
btn_access_code_create.setOnClickListener {
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
layoutBackupAccessCodeInfo.root.show()
layoutBackupAccessCodeSubmit.root.hide()
layoutBackupAccessCodeInfo.btnAccessCodeCreate.setOnClickListener {
store.dispatch(BackupAction.ShowEnterAccessCodeScreen)
}
}
fun showEnterAccessCode() {
layout_backup_access_code_info.hide()
layout_backup_access_code_submit.show()
access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title)
btn_access_code_submit.text = context.getText(R.string.common_continue)
btn_access_code_submit.setOnClickListener {
store.dispatch(BackupAction.CheckAccessCode(et_access_code.text.toString()))
fun showEnterAccessCode() = with(binding!!) {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title)
with(layoutBackupAccessCodeSubmit) {
btnAccessCodeSubmit.text = context.getText(R.string.common_continue)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.CheckAccessCode(etAccessCode.text.toString()))
}
}
}
fun showReenterAccessCode() {
layout_backup_access_code_info.hide()
layout_backup_access_code_submit.show()
access_code_title.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
et_access_code.setText("")
btn_access_code_submit.text = context.getText(R.string.common_submit)
btn_access_code_submit.setOnClickListener {
store.dispatch(BackupAction.SaveAccessCodeConfirmation(et_access_code.text.toString()))
fun showReenterAccessCode() = with(binding!!) {
layoutBackupAccessCodeInfo.root.hide()
layoutBackupAccessCodeSubmit.root.show()
accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title)
with(layoutBackupAccessCodeSubmit) {
etAccessCode.setText("")
btnAccessCodeSubmit.text = context.getText(R.string.common_submit)
btnAccessCodeSubmit.setOnClickListener {
store.dispatch(BackupAction.SaveAccessCodeConfirmation(etAccessCode.text.toString()))
}
}
}
fun showError(error: AccessCodeError?) {
fun showError(error: AccessCodeError?) = with(binding!!.layoutBackupAccessCodeSubmit) {
when (error) {
AccessCodeError.CodeTooShort ->
til_access_code.error = context.getString(R.string.onboarding_access_code_too_short)
tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short)
AccessCodeError.CodesDoNotMatch ->
til_access_code.error = context.getString(R.string.onboarding_access_codes_doesnt_match)
null -> til_access_code.error = null
tilAccessCode.error = context.getString(R.string.onboarding_access_codes_doesnt_match)
null -> tilAccessCode.error = null
}
}
}

View file

@ -6,6 +6,8 @@ import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.extensions.Result
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.ReceiptAction
@ -38,7 +40,11 @@ class RequestFeeMiddleware {
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress!!
val destinationAmount = Amount(typedAmount, sendState.amountState.amountToSendCrypto)
val txSender = walletManager as TransactionSender
val txSender = if (walletManager.isDemoWallet()) {
DemoTransactionSender(walletManager)
} else {
walletManager as TransactionSender
}
scope.launch {
val feeResult = txSender.getFee(destinationAmount, destinationAddress)
withContext(Dispatchers.Main) {
@ -84,15 +90,15 @@ class FeeMock {
suspend fun feeZero(blockchain: Blockchain): List<Amount> = listOf(Amount(BigDecimal.ZERO, blockchain))
suspend fun feeStandard(blockchain: Blockchain): List<Amount> = listOf(
Amount(0.001500.toBigDecimal(), blockchain),
Amount(0.0030.toBigDecimal(), blockchain),
Amount(0.0045001.toBigDecimal(), blockchain)
Amount(0.001500.toBigDecimal(), blockchain),
Amount(0.0030.toBigDecimal(), blockchain),
Amount(0.0045001.toBigDecimal(), blockchain)
)
suspend fun feeStandardBig(blockchain: Blockchain): List<Amount> = listOf(
Amount(1.76.toBigDecimal(), blockchain),
Amount(2.30.toBigDecimal(), blockchain),
Amount(3.45.toBigDecimal(), blockchain)
Amount(1.76.toBigDecimal(), blockchain),
Amount(2.30.toBigDecimal(), blockchain),
Amount(3.45.toBigDecimal(), blockchain)
)
suspend fun feeSingle(blockchain: Blockchain): List<Amount> = listOf(Amount(0.0015.toBigDecimal(), blockchain))

View file

@ -13,6 +13,7 @@ import com.tangem.tap.common.analytics.Analytics
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsParam
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -22,6 +23,8 @@ import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
import com.tangem.tap.features.demo.DemoTransactionSender
import com.tangem.tap.features.demo.isDemoWallet
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.FeeAction.RequestFee
import com.tangem.tap.features.send.redux.states.ButtonState
@ -32,6 +35,7 @@ import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -164,9 +168,12 @@ private fun sendTransaction(
)
)
}
val sendResult = try {
(walletManager as TransactionSender).send(txData, signer)
if (walletManager.isDemoWallet()) {
DemoTransactionSender(walletManager).send(txData, signer)
} else {
(walletManager as TransactionSender).send(txData, signer)
}
} catch (ex: Exception) {
FirebaseCrashlytics.getInstance().recordException(ex)
delay(DELAY_SDK_DIALOG_CLOSE)
@ -238,6 +245,13 @@ private fun sendTransaction(
message.contains("Target account is not created. To create account send 1+ XLM.") -> {
dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated))
}
message.contains(DemoTransactionSender.ID) -> {
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatchDialogShow(AppDialog.SimpleOkDialogRes(
R.string.common_done,
R.string.alert_demo_feature_disabled
) { dispatch(NavigationAction.PopBackTo()) })
}
else -> {
(sendResult.error as? TangemSdkError)?.let { error ->
store.state.globalState.analyticsHandlers?.logCardSdkError(

View file

@ -11,6 +11,7 @@ import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import by.kirich1409.viewbindingdelegate.viewBinding
import com.google.android.material.textfield.TextInputEditText
import com.tangem.Message
import com.tangem.tangem_sdk_new.extensions.dpToPx
@ -40,10 +41,7 @@ import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_send.*
import kotlinx.android.synthetic.main.layout_send_address_payid.*
import kotlinx.android.synthetic.main.layout_send_amount.*
import kotlinx.android.synthetic.main.layout_send_fee.*
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
@ -62,6 +60,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
val binding: FragmentSendBinding by viewBinding(FragmentSendBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
@ -77,7 +77,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
store.dispatch(SendActionUi.CheckIfTransactionDataWasProvided)
}
private fun initSendButtonStates() {
private fun initSendButtonStates() = with(binding) {
btnSend.setOnClickListener {
store.dispatch(SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header))
@ -86,7 +86,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
private fun setupAddressOrPayIdLayout() {
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
@ -113,7 +113,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupTransactionExtrasLayout() {
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
etXlmMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
@ -151,9 +151,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
// Delayed launch is needed in order for the UI to be drawn and to process the sent events.
// If do not use the delay, then etAmount error field is not displayed when
// inserting an incorrect amount by shareUri
imvQrCode.postDelayed({
binding.lSendAddressPayid.imvQrCode.postDelayed({
store.dispatch(PasteAddressPayId(scannedCode))
store.dispatch(TruncateOrRestore(!etAddressOrPayId.isFocused))
store.dispatch(TruncateOrRestore(!binding.lSendAddressPayid.etAddressOrPayId.isFocused))
}, 200)
}
@ -165,7 +165,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
val decimalSeparator = DecimalFormatSymbols.getInstance().decimalSeparator.toString()
store.dispatch(AmountAction.SetDecimalSeparator(decimalSeparator))
tvAmountCurrency.setOnClickListener {
binding.lSendAmount.tvAmountCurrency.setOnClickListener {
store.dispatch(ToggleMainCurrency)
store.dispatch(ReceiptAction.RefreshReceipt)
store.dispatch(SendAction.ChangeSendButtonState(store.state.sendState.getButtonState()))
@ -223,8 +223,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupFeeLayout() {
flExpandCollapse.setOnClickListener {
private fun setupFeeLayout() = with(binding.clNetworkFee) {
flExpandCollapse.flExpandCollapse.setOnClickListener {
store.dispatch(ToggleControlsVisibility)
}
chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL))
@ -240,12 +240,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
}
private fun setupWarningMessages() {
private fun setupWarningMessages() = with(binding) {
warningsAdapter = WarningMessagesAdapter()
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
rv_warning_messages.layoutManager = layoutManager
rv_warning_messages.addItemDecoration(SpacesItemDecoration(rv_warning_messages.dpToPx(16f).toInt()))
rv_warning_messages.adapter = warningsAdapter
rvWarningMessages.layoutManager = layoutManager
rvWarningMessages.addItemDecoration(SpacesItemDecoration(rvWarningMessages.dpToPx(16f).toInt()))
rvWarningMessages.adapter = warningsAdapter
store.dispatch(SendAction.Warnings.Update)
}

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