Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-25 15:07:28 +00:00
commit f94e1a5c19
155 changed files with 5271 additions and 916 deletions

5
.gitignore vendored
View file

@ -13,10 +13,7 @@ local.properties
.gradle
# User-specific configurations
.idea/caches/
.idea/libraries/
.idea/*.xml
.idea/codeStyles
.idea
# LayoutInspector files
/captures

View file

@ -68,16 +68,17 @@ dependencies {
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
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.0'
implementation 'com.google.android.play:core:1.10.2'
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-38'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-70'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-70'
implementation 'com.tangem:blockchain:develop-42'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-91'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-91'
// WebView
implementation "androidx.browser:browser:1.3.0"
@ -114,17 +115,19 @@ dependencies {
// Network and Json
implementation 'com.squareup.retrofit2:retrofit:2.8.1'
implementation 'com.squareup.retrofit2:converter-moshi:2.6.0'
implementation 'com.squareup.moshi:moshi:1.9.2'
implementation "com.squareup.moshi:moshi-kotlin:1.9.2"
implementation 'com.squareup.moshi:moshi:1.12.0'
implementation "com.squareup.moshi:moshi-kotlin:1.12.0"
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.2'
//
// //Crypto
implementation "com.madgag.spongycastle:core:1.58.0.0"
// animation
implementation "com.airbnb.android:lottie:3.4.0"
testImplementation 'junit:junit:4.13.2'
testImplementation "com.google.truth:truth:1.0.1"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
testImplementation "com.google.truth:truth:1.1.3"
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

@ -1 +1 @@
Subproject commit e80938f7d9ba378a91ac544a9170597e64afa696
Subproject commit 0cf3af21e6f441e6323a1ba127ee7518168797df

View file

@ -17,7 +17,6 @@ import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.CoroutineExceptionHandler
@ -60,8 +59,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.state.globalState.feedbackManager?.updateAcivity(this)
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
@ -70,13 +69,20 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
store.dispatch(WalletConnectAction.RestoreSessions)
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
override fun onResume() {
super.onResume()
notificationsHandler = NotificationsHandler(fragment_container)
if (supportFragmentManager.backStackEntryCount == 0 ||
store.state.globalState.scanNoteResponse == null
) {
store.dispatch(HomeAction.CheckIfFirstLaunch)
val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0
val isScannedBefore = store.state.globalState.scanResponse != null
val isOnboardingServiceActive = store.state.globalState.onboardingManager != null
if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Home))
}
intentHandler.handleIntent(intent)

View file

@ -2,9 +2,12 @@ package com.tangem.tap.common
import android.app.Dialog
import android.content.Context
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@ -37,35 +40,38 @@ class DialogManager : StoreSubscriber<GlobalState> {
val context = context ?: return
if (dialog != null) return
when (state.dialog) {
dialog = when (state.dialog) {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
dialog = SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
)
is WalletConnectDialog.OpeningSessionRejected -> {
dialog = SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
)
}
is WalletConnectDialog.SessionTimeout -> {
dialog = SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
)
}
is WalletConnectDialog.ApproveWcSession ->
dialog = ApproveWcSessionDialog.create(state.dialog.session, context)
ApproveWcSessionDialog.create(state.dialog.session, context)
is WalletConnectDialog.ClipboardOrScanQr ->
dialog = ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->
dialog = TransactionDialog.create(state.dialog.dialogData, context)
TransactionDialog.create(state.dialog.dialogData, context)
is WalletConnectDialog.PersonalSign ->
dialog = PersonalSignDialog.create(state.dialog.data, context)
PersonalSignDialog.create(state.dialog.data, context)
else -> null
}
dialog?.show()
}

View file

@ -0,0 +1,26 @@
package com.tangem.tap.common
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
private val uiHandler = Handler(Looper.getMainLooper())
private val backgroundHandler = Handler(HandlerThread("AppMainHandlerThread").apply { start() }.looper)
fun postUi(ms: Long = 0, func: Runnable) {
if (ms == 0L) uiHandler.post { func.run() } else uiHandler.postDelayed(func, ms)
}
fun postBackground(ms: Long = 0, func: Runnable) {
if (ms == 0L) backgroundHandler.post { func.run() } else backgroundHandler.postDelayed(func, ms)
}
fun post(ms: Long = 0, func: Runnable) {
if (ms == 0L) {
func.run()
} else {
val currentLooper = Looper.myLooper() ?: return
Handler(currentLooper).postDelayed(func, ms)
}
}

View file

@ -1,3 +1,17 @@
package com.tangem.tap.common.entities
open class Button(val enabled: Boolean)
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.wallet.redux.ProgressState
open class Button(val enabled: Boolean)
open class IndeterminateProgressButton(
val state: ButtonState
) : Button(state != ButtonState.DISABLED) {
val progressState: ProgressState
get() = when (state) {
ButtonState.PROGRESS -> ProgressState.Loading
else -> ProgressState.Done
}
}

View file

@ -14,4 +14,18 @@ fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
this.removeAll(toRemove)
return toRemove.isNotEmpty()
}
fun <T> MutableList<T>.replaceBy(item: T, predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
if (toRemove.isEmpty()) return false
val indexes = toRemove.map { indexOf(it) }
this.removeAll(toRemove)
indexes.forEach { this.add(it, item) }
return true
}
fun <T> MutableList<T>.replaceByOrAdd(item: T, predicate: (T) -> Boolean) {
if (!replaceBy(item, predicate)) add(item)
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.common.extensions
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
[REDACTED_AUTHOR]
*/
suspend fun <T> withMainContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.Main, block)
}
suspend fun <T> withIOContext(block: suspend CoroutineScope.() -> T) {
withContext(Dispatchers.IO, block)
}

View file

@ -1,9 +1,12 @@
package com.tangem.tap.common.extensions
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.details.ui.DetailsSecurityFragment
@ -13,17 +16,35 @@ import com.tangem.tap.features.details.ui.walletconnect.QrScanFragment
import com.tangem.tap.features.details.ui.walletconnect.WalletConnectSessionsFragment
import com.tangem.tap.features.disclaimer.ui.DisclaimerFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment
import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment
import com.tangem.tap.features.onboarding.products.wallet.OnboardingWalletFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.tokens.ui.AddTokensFragment
import com.tangem.tap.features.wallet.ui.OnboardingTwinsFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.dialogs.TwinsOnboardingFragment
import com.tangem.wallet.R
fun FragmentActivity.openFragment(screen: AppScreen, addToBackStack: Boolean = true) {
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
fgShareTransition: FragmentShareTransition? = null
) {
val transaction = this.supportFragmentManager.beginTransaction()
transaction.replace(R.id.fragment_container, fragmentFactory(screen), screen.name)
if (addToBackStack && screen != AppScreen.Home) transaction.addToBackStack(null)
val fragment = fragmentFactory(screen)
fgShareTransition?.apply {
fragment.sharedElementEnterTransition = enterTransitionSet
fragment.sharedElementReturnTransition = exitTransitionSet
transaction.setReorderingAllowed(true)
shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.name)
}
}
}
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack && screen != AppScreen.Home) transaction.addToBackStack(null)
transaction.commitAllowingStateLoss()
}
@ -38,9 +59,22 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
return tag?.let { AppScreen.valueOf(tag) }
}
fun FragmentActivity.addOnBackPressedDispatcher(
isEnabled: Boolean = true,
onBackPressed: VoidCallback
): OnBackPressedCallback = (object : OnBackPressedCallback(isEnabled) {
override fun handleOnBackPressed() {
onBackPressed()
}
}).also { this.onBackPressedDispatcher.addCallback(it) }
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
@ -49,7 +83,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.TwinsOnboarding -> TwinsOnboardingFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()

View file

@ -1,6 +1,9 @@
package com.tangem.tap.common.extensions
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -8,8 +11,57 @@ import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.Store
fun Store<AppState>.dispatchOnMain(action: Action) {
fun Store<*>.dispatchOnMain(action: Action) {
scope.launch(Dispatchers.Main) {
store.dispatch(action)
}
}
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowNotification(resId))
}
}
fun Store<*>.dispatchToastNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowToastNotification(resId))
}
}
fun Store<*>.dispatchErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowErrorNotification(error))
}
}
/**
* @param fatal used to indicate errors that should not normally have occurred
*/
fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) {
val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: "
dispatchDebugErrorNotification(TapError.CustomError("$prefix $message"))
}
fun Store<*>.dispatchDebugErrorNotification(error: TapError) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.DebugShowErrorNotification(error))
}
}
fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowDialog(dialog))
}
}
fun Store<*>.dispatchDialogHide() {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.HideDialog)
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.common.extensions
import android.content.Context
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
fun Context.toast(message: String, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, message, length).show()
}
fun Context.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
Toast.makeText(this, this.getString(messageRes), length).show()
}
fun View.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context.toast(message, length)
}
fun View.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context.toast(context.getString(messageRes), length)
}
fun Fragment.toast(message: String, length: Int = Toast.LENGTH_LONG) {
context?.toast(message, length)
}
fun Fragment.toast(@StringRes messageRes: Int, length: Int = Toast.LENGTH_LONG) {
context?.let { it.toast(it.getString(messageRes), length) }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.common.extensions
import androidx.transition.Transition
/**
[REDACTED_AUTHOR]
*/
inline fun Transition.addListener(
crossinline onStart: (animator: Transition) -> Unit = {},
crossinline onEnd: (animator: Transition) -> Unit = {},
crossinline onCancel: (animator: Transition) -> Unit = {},
crossinline onPause: (animator: Transition) -> Unit = {},
crossinline onRepeat: (animator: Transition) -> Unit = {}
): Transition.TransitionListener {
val listener = object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) = onStart(transition)
override fun onTransitionEnd(transition: Transition) = onEnd(transition)
override fun onTransitionCancel(transition: Transition) = onCancel(transition)
override fun onTransitionPause(transition: Transition) = onPause(transition)
override fun onTransitionResume(transition: Transition) = onRepeat(transition)
}
addListener(listener)
return listener
}

View file

@ -5,6 +5,7 @@ import android.content.*
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
@ -16,6 +17,7 @@ import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.google.android.material.card.MaterialCardView
import com.tangem.common.extensions.VoidCallback
fun Fragment.getDrawable(@DrawableRes drawableResId: Int): Drawable? {
return ContextCompat.getDrawable(requireContext(), drawableResId)
@ -76,8 +78,8 @@ fun Context.dpToPixels(dp: Int): Int =
TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), this.resources.displayMetrics
).toInt()
tailrec fun Context?.getActivity(): Activity? = this as? Activity ?:
(this as? ContextWrapper)?.baseContext?.getActivity()
tailrec fun Context?.getActivity(): Activity? = this as? Activity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
fun MaterialCardView.setMargins(
marginLeftDp: Int = 16,
@ -141,4 +143,19 @@ fun Context.shareText(text: String) {
fun Fragment.shareText(text: String) {
requireContext().shareText(text)
}
fun Context.safeStartActivity(
intent: Intent,
options: Bundle? = null,
fallback: ((ActivityNotFoundException) -> Unit)? = null,
finally: VoidCallback? = null
) {
try {
this.startActivity(intent, options)
} catch (ex: ActivityNotFoundException) {
fallback?.invoke(ex)
} finally {
finally?.invoke()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.common.extensions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewParent
import androidx.core.view.forEach
@ -15,11 +16,8 @@ import timber.log.Timber
/**
[REDACTED_AUTHOR]
*/
fun ViewGroup.inflate(viewToInflate: Int, rootView: ViewGroup?, parent: ViewGroup) {
if (rootView == null) {
val inflatedView = LayoutInflater.from(context).inflate(viewToInflate, rootView)
parent.addView(inflatedView)
}
fun ViewGroup.inflate(viewToInflate: Int, attachToRoot: Boolean = false): View {
return LayoutInflater.from(context).inflate(viewToInflate, this, attachToRoot)
}
fun ViewParent?.beginDelayedTransition(transition: Transition = AutoTransition()) {

View file

@ -0,0 +1,59 @@
package com.tangem.tap.common.extensions
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.services.Result
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.amountToCreateAccount
import com.tangem.tap.domain.extensions.isNoAccountError
import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.topup.TradeCryptoHelper
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.store
/**
[REDACTED_AUTHOR]
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
update()
Result.Success(wallet)
} catch (exception: Exception) {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
Result.Failure(TapError.NoInternetConnection)
} else {
val blockchain = wallet.blockchain
val amountToCreateAccount = blockchain.amountToCreateAccount(wallet.getFirstToken())
if (blockchain.isNoAccountError(exception) && amountToCreateAccount != null) {
Result.Failure(TapError.WalletManagerUpdate.NoAccountError(amountToCreateAccount.toString()))
} else {
val message = exception.localizedMessage ?: "Unknown exception during WalletManager update"
Result.Failure(TapError.WalletManagerUpdate.InternalError(message))
}
}
}
fun WalletManager?.getToUpUrl(): String? {
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
)
}
fun WalletManager?.getAddressData(): AddressData? {
val wallet = this?.wallet ?: return null
val addressDataList = wallet.createAddressesData()
return if (addressDataList.isEmpty()) null
else addressDataList[0]
}

View file

@ -3,9 +3,12 @@ package com.tangem.tap.common.images
import android.app.Application
import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso
import com.tangem.wallet.BuildConfig
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import timber.log.Timber
import java.io.File
import java.util.concurrent.TimeUnit
@ -17,12 +20,15 @@ class PicassoHelper {
val picasso = Picasso.Builder(application)
.downloader(OkHttp3Downloader(getOkHttpForPicasso(application)))
.build()
picasso.isLoggingEnabled = BuildConfig.DEBUG
Picasso.setSingletonInstance(picasso)
}
private fun getOkHttpForPicasso(application: Application): OkHttpClient {
val okHttpBuilder = OkHttpClient.Builder()
okHttpBuilder.cache(Cache(File(application.filesDir, "artworks"), Long.MAX_VALUE))
okHttpBuilder.callTimeout(15000, TimeUnit.MILLISECONDS)
okHttpBuilder.addInterceptor { chain ->
val cacheControl = CacheControl.Builder()
.maxStale(KEEP_CACHE_MAX_DAYS, TimeUnit.DAYS)
@ -33,7 +39,25 @@ class PicassoHelper {
.build()
chain.proceed(neverExpireRequest)
}
addDebugInterceptors(okHttpBuilder)
return okHttpBuilder.build()
}
private fun addDebugInterceptors(okHttpBuilder: OkHttpClient.Builder) {
if (!BuildConfig.DEBUG) return
val picassoInterceptor = HttpLoggingInterceptor(PicassoOkHttpLogger()).apply {
level = HttpLoggingInterceptor.Level.BODY
}
okHttpBuilder.addInterceptor(picassoInterceptor)
}
}
}
private class PicassoOkHttpLogger : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
Timber.d(message)
}
}

View file

@ -0,0 +1,147 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.view.animation.LinearInterpolator
/**
[REDACTED_AUTHOR]
*/
//fun View.setForeground(hasForegroundAtEnd: Boolean) {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// foreground = getForegroundDrawable()
// }
//}
//fun View.getForegroundDrawable(): Drawable? {
// return AppCompatResources.getDrawable(context, R.drawable.shape_rectangle_rounded_8_op_20)?.also {
// it.alpha = 60
// }
//}
typealias ProgressListener = (Int) -> Unit
fun createProgressListener(duration: Long, listener: ProgressListener?): Animator {
val valueAnimator = ValueAnimator.ofInt(0, 100)
valueAnimator.duration = duration
valueAnimator.addUpdateListener { listener?.invoke((it.animatedValue as? Int) ?: 0) }
return valueAnimator
}
fun View.unfoldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply { playTogether(translate) }
}
fun View.foldAnimation(animDuration: Long, properties: LeapViewProperties): AnimatorSet {
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translate.duration = animDuration
return AnimatorSet().apply {
playSequentially(translate)
}
}
fun View.leapAnimation(animDuration: Long, properties: LeapViewProperties, overLift: Float): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = (height.toFloat() + overLift) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
translateDown.interpolator = LinearInterpolator()
return AnimatorSet().apply {
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.leapBackAnimation(animDuration: Long, properties: LeapViewProperties, calculator: PropertyCalculator): AnimatorSet {
val halfDuration = animDuration / 2
val upTo = ((height).toFloat() - calculator.yTranslationFactor) * -1
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, upTo)
translateUp.duration = halfDuration
translateUp.interpolator = LinearInterpolator()
val scaleDuration = halfDuration - halfDuration / 5
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val scale = AnimatorSet().apply {
startDelay = scaleDuration
duration = scaleDuration
interpolator = LinearInterpolator()
playTogether(scaleX, scaleY)
}
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd, halfDuration, 100)
val translateDown = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
translateDown.startDelay = halfDuration
translateDown.duration = halfDuration
return AnimatorSet().apply {
playTogether(translateUp, scale, elevation, translateDown)
}
}
fun View.pullUpAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 2
val translateUp = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translateUp, scaleX, scaleY, elevation)
}
}
fun View.pullDownAnimation(leapDuration: Long, properties: LeapViewProperties): AnimatorSet {
val pullDuration = leapDuration / 2
val delayDuration = leapDuration / 4
val translate = ObjectAnimator.ofFloat(this, View.TRANSLATION_Y, properties.yTranslation)
val scaleX = ObjectAnimator.ofFloat(this, View.SCALE_X, properties.scale)
val scaleY = ObjectAnimator.ofFloat(this, View.SCALE_Y, properties.scale)
val elevation = elevationAnimator(properties.elevationStart, properties.elevationEnd)
return AnimatorSet().apply {
startDelay = delayDuration
duration = pullDuration
interpolator = DecelerateInterpolator()
playTogether(translate, scaleX, scaleY, elevation)
}
}
fun View.elevationAnimator(start: Float, end: Float, startDelay: Long? = null, duration: Long? = null): Animator {
val elevationAnimator = ValueAnimator.ofFloat(start, end)
elevationAnimator.addUpdateListener { this.elevation = (it.animatedValue as? Float) ?: start }
startDelay?.let { elevationAnimator.startDelay = it }
duration?.let { elevationAnimator.duration = it }
return elevationAnimator
}

View file

@ -0,0 +1,144 @@
package com.tangem.tap.common.leapfrogWidget
import android.view.View
/**
[REDACTED_AUTHOR]
*/
class LeapView(
val view: View,
val index: Int,
position: Int,
private val maximalPosition: Int,
private val calculator: PropertyCalculator,
) {
var state: LeapViewState
private set
val initialState: LeapViewState
init {
val initialProperties = createInitialProperty(position)
state = LeapViewState(index, position, position, initialProperties)
initialState = state.copy()
}
fun initView() {
applyState(initialState)
}
fun leap(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
0 -> maximalPosition
else -> state.currentPosition - 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == 0 && currentPosition == maximalPosition -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun leapBack(): LeapFrogAnimation {
val previousPosition = state.currentPosition
val currentPosition = when (previousPosition) {
maximalPosition -> 0
else -> state.currentPosition + 1
}
state = state.copy(
currentPosition = currentPosition,
previousPosition = previousPosition,
properties = createLeapAnimationProperty(previousPosition, currentPosition)
)
return when {
previousPosition == maximalPosition && currentPosition == 0 -> LeapFrogAnimation.LEAP
else -> LeapFrogAnimation.PULL
}
}
fun fold() {
state = state.copy(properties = state.properties.toFold()
)
}
fun unfold() {
state = state.copy(properties = state.properties.toUnfold(calculator))
}
fun applyState(state: LeapViewState) {
this.state = state
setProperties(this.state.properties)
}
private fun setProperties(properties: LeapViewProperties) {
view.elevation = properties.elevationEnd
view.translationY = properties.yTranslation
view.scaleX = properties.scale
view.scaleY = properties.scale
// initialProperties.hasForeground
}
private fun createInitialProperty(endPosition: Int): LeapViewProperties {
return LeapViewProperties(
endPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.yTranslation(0),
calculator.hasForeground(endPosition),
)
}
private fun createLeapAnimationProperty(startPosition: Int, endPosition: Int): LeapViewProperties {
return LeapViewProperties(
startPosition,
endPosition,
calculator.scale(endPosition),
calculator.elevation(startPosition, maximalPosition + 1),
calculator.elevation(endPosition, maximalPosition + 1),
calculator.yTranslation(endPosition),
calculator.hasForeground(endPosition),
)
}
private fun LeapViewProperties.toUnfold(calculator: PropertyCalculator): LeapViewProperties {
return this.copy(yTranslation = calculator.yTranslation(this.positionEnd))
}
private fun LeapViewProperties.toFold(): LeapViewProperties {
return this.copy(yTranslation = 0f)
}
override fun toString(): String = "index: $index, position: ${state.currentPosition}, code: ${view.hashCode()}"
}
data class LeapViewState(
val index: Int,
val currentPosition: Int,
val previousPosition: Int,
val properties: LeapViewProperties
)
enum class LeapFrogAnimation {
LEAP, PULL
}
data class LeapViewProperties(
val positionStart: Int,
val positionEnd: Int,
val scale: Float,
val elevationStart: Float,
val elevationEnd: Float,
val yTranslation: Float,
val hasForeground: Boolean,
)

View file

@ -0,0 +1,258 @@
package com.tangem.tap.common.leapfrogWidget
import android.animation.Animator
import android.animation.AnimatorSet
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
[REDACTED_AUTHOR]
*/
class LeapfrogWidget(
private val parentContainer: FrameLayout,
private val calculator: PropertyCalculator = PropertyCalculator(),
) {
private val foldAnimationDuration = 500L
private val leapAnimationDuration = 800L
private val leapBackAnimationDuration = 800L
private val leapViews = mutableListOf<LeapView>()
private var foldUnfoldProgress: Int = 100
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
var isFolded: Boolean = true
private set
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
if (viewIsFullFledged()) {
val maxPosition = parentContainer.childCount - 1
parentContainer.children.forEachIndexed { index, view ->
val initialPosition = maxPosition - index
leapViews.add(LeapView(view, index, initialPosition, maxPosition, calculator))
}
}
}
/**
* Apply initial properties to the views
*/
fun initViews() {
isFolded = true
leapViews.forEach { it.initView() }
}
fun fold(onEnd: () -> Unit = {}) {
val animator = foldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun unfold(onEnd: () -> Unit = {}) {
val animator = unfoldAnimator()
if (animator == null) {
onEnd()
} else {
animator.doOnEnd { onEnd() }
animator.start()
}
}
fun foldAnimator(): Animator? {
if (!canFoldUnfold() || isFolded) return null
return createFoldUnfoldAnimator(true, foldAnimationDuration)
}
fun unfoldAnimator(): Animator? {
if (!canFoldUnfold() || !isFolded) return null
return createFoldUnfoldAnimator(false, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(duration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(duration, it.state.properties))
}
}
animatorsList.add(createProgressListener(duration) {
foldUnfoldProgress = it
if (it == 100) isFolded = isFoldAnimation
})
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
animator.playTogether(animatorsList.toList())
return animator
}
fun leap(onEnd: () -> Unit = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = leapAnimationDuration
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leap()) {
LeapFrogAnimation.LEAP -> {
val overLift = calculator.overLift(leapViews.size)
animatorsList.add(it.view.leapAnimation(duration, it.state.properties, overLift))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullUpAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun leapBack(onEnd: () -> Unit = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = leapBackAnimationDuration
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
LeapFrogAnimation.LEAP -> {
animatorsList.add(it.view.leapBackAnimation(duration, it.state.properties, calculator))
}
LeapFrogAnimation.PULL -> {
animatorsList.add(it.view.pullDownAnimation(duration, it.state.properties))
}
}
}
animatorsList.add(createProgressListener(duration) { leapBackProgress = it })
val animator = AnimatorSet()
animator.playTogether(animatorsList.toList())
animator.doOnEnd { onEnd() }
animator.start()
}
fun getViewsCount(): Int = parentContainer.childCount
fun getViewPositionByIndex(index: Int): Int = leapViews.first { it.index == index }.state.currentPosition
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
isFolded = state.isFolded
state.leapViewStates.forEach { state ->
leapViews.firstOrNull { it.index == state.index }?.applyState(state)
}
}
private fun viewIsFullFledged(): Boolean = parentContainer.childCount > 1
private fun canFoldUnfold(): Boolean {
return when {
!viewIsFullFledged() -> false
foldUnfoldInProgress() -> false
leapInProgress() || leapBackInProgress() -> false
else -> true
}
}
private fun canLeap(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
}
}
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapInProgress() -> false
leapBackProgress < 70 -> false
else -> true
}
}
private fun foldUnfoldInProgress(): Boolean = foldUnfoldProgress != 100
private fun leapInProgress(): Boolean = leapProgress != 100
private fun leapBackInProgress(): Boolean = leapBackProgress != 100
}
data class LeapfrogWidgetState(
val isFolded: Boolean,
val leapViewStates: List<LeapViewState>
)
class PropertyCalculator(
val elevationFactor: Float = 1f,
val decreaseScaleFactor: Float = 0.1f,
val yTranslationFactor: Float = 20f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 0 view it is view on the top of stack, but it is last in parent.children
private var translationValueConverter: ((Float) -> Float)? = null
fun setTranslationConverter(converter: (Float) -> Float) {
translationValueConverter = converter
}
fun scale(position: Int): Float {
return 1f - decreaseScaleFactor * position
}
fun elevation(position: Int, count: Int): Float {
return elevationFactor * (count - 1 - position)
}
fun yTranslation(position: Int): Float {
val yTranslation = yTranslationFactor * position
return convertTranslation(yTranslation)
}
fun hasForeground(position: Int): Boolean {
return position != 0
}
fun overLift(viewCount: Int): Float {
val initialOverLift = when {
yTranslationFactor < 0f -> yTranslationFactor * decreaseOverLiftingFactor * viewCount * -1
else -> 10f
}
val scaleOverLift = when {
decreaseScaleFactor == 0f -> initialOverLift
else -> initialOverLift * decreaseOverLiftingFactor - initialOverLift * decreaseScaleFactor
}
return convertTranslation(scaleOverLift)
}
private fun convertTranslation(value: Float): Float {
return translationValueConverter?.invoke(value) ?: value
}
}

View file

@ -0,0 +1,68 @@
package com.tangem.tap.common.leapfrogWidget
import android.os.Bundle
import android.view.View
import android.widget.FrameLayout
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.test_leapfrog_fragment.*
class TestLeapfrogFragment : Fragment(R.layout.test_leapfrog_fragment) {
private lateinit var twinsCardWidget: TwinsCardWidget
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val leapfrogContainer: FrameLayout = view.findViewById(R.id.leapfrog_views_container)
val leapfrog = LeapfrogWidget(leapfrogContainer)
twinsCardWidget = TwinsCardWidget(leapfrog) { 200f }
btn_twin_welcome.setOnClickListener {
twinsCardWidget.toWelcome()
}
btn_twin_to_leapfrog.setOnClickListener {
twinsCardWidget.toLeapfrog()
}
btn_twin_activate.setOnClickListener {
twinsCardWidget.toActivate()
}
btn_lp_init.setOnClickListener {
leapfrog.initViews()
}
btn_lp_unfold.setOnClickListener {
leapfrog.unfold()
}
btn_lp_fold.setOnClickListener {
leapfrog.fold()
}
btn_lp_leap.setOnClickListener {
leapfrog.leap()
}
btn_lp_leap_back.setOnClickListener {
leapfrog.leapBack()
}
}
override fun onStart() {
super.onStart()
leapfrogWidgetState?.let { twinsCardWidget.leapfrogWidget.applyState(it) }
}
override fun onStop() {
super.onStop()
leapfrogWidgetState = twinsCardWidget.leapfrogWidget.getState()
}
}
private var leapfrogWidgetState: LeapfrogWidgetState? = null

View file

@ -6,6 +6,9 @@ import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer
import com.tangem.tap.features.disclaimer.redux.DisclaimerReducer
import com.tangem.tap.features.home.redux.HomeReducer
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
import com.tangem.tap.features.wallet.redux.reducers.WalletReducer
@ -19,6 +22,9 @@ fun appReducer(action: Action, state: AppState?): AppState {
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
homeState = HomeReducer.reduce(action, state),
onboardingNoteState = OnboardingNoteReducer.reduce(action, state),
onboardingWalletState = OnboardingWalletReducer.reduce(action, state),
onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.GlobalMiddleware
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.global.globalMiddleware
import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
@ -12,7 +12,13 @@ import com.tangem.tap.features.disclaimer.redux.DisclaimerMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
import com.tangem.tap.features.home.redux.HomeMiddleware
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware
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.tokens.redux.TokensMiddleware
import com.tangem.tap.features.tokens.redux.TokensState
@ -25,6 +31,9 @@ data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val homeState: HomeState = HomeState(),
val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(),
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
@ -36,10 +45,14 @@ data class AppState(
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware,
HomeMiddleware().homeMiddleware,
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
sendMiddleware,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,

View file

@ -9,6 +9,7 @@ import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.notificationsHandler
import com.tangem.wallet.BuildConfig
import org.rekotlin.Action
import org.rekotlin.Middleware
import java.lang.ref.WeakReference
@ -63,23 +64,29 @@ fun getMessageString(context: Context, message: Int, args: List<Any>?): String {
val notificationsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.localizedMessage, args)
}
}
handleNotificationAction(action)
next(action)
}
}
}
private fun handleNotificationAction(action: Action) {
if (action is Debug && !BuildConfig.DEBUG) return
when (action) {
is NotificationAction -> notificationsHandler?.showNotification(action.messageResource)
is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource)
is ErrorAction -> {
when (action.error) {
is MultiMessageError -> {
val multiError = action.error as MultiMessageError
notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder)
}
else -> {
val args = (action.error as? ArgError)?.args ?: listOf()
notificationsHandler?.showNotification(action.error.messageResource, args)
}
}
next(action)
}
}
}
@ -94,4 +101,10 @@ interface NotificationAction : Action {
interface ErrorAction : Action {
val error: TapError
}
}
// Processed only in the debug builds
interface Debug
interface DebugNotification : Debug, NotificationAction
interface DebugToastNotification : Debug, ToastNotificationAction
interface DebugErrorAction : Debug, ErrorAction

View file

@ -0,0 +1,17 @@
package com.tangem.tap.common.redux
import com.tangem.tap.features.wallet.redux.AddressData
/**
[REDACTED_AUTHOR]
*/
interface StateDialog
sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val addressData: AddressData,
val onCopyAddress: () -> Unit,
val onExploreAddress: () -> Unit
) : AppDialog()
}

View file

@ -2,10 +2,13 @@ package com.tangem.tap.common.redux.global
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.tap.common.redux.*
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
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
@ -14,13 +17,34 @@ import org.rekotlin.Action
sealed class GlobalAction : Action {
// notifications
data class ShowNotification(override val messageResource: Int) : GlobalAction(), NotificationAction
data class ShowToastNotification(override val messageResource: Int) : GlobalAction(), ToastNotificationAction
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse) : GlobalAction()
object Stop : GlobalAction()
}
data class ScanCard(
val onSuccess: ((ScanResponse) -> Unit)? = null,
val onFailure: ((TangemError) -> Unit)? = null,
val messageResId: Int? = null,
) : GlobalAction()
object ScanFailsCounter {
data class ChooseBehavior(val result: CompletionResult<ScanNoteResponse>) : GlobalAction()
data class ChooseBehavior(val result: CompletionResult<ScanResponse>) : GlobalAction()
object Reset : GlobalAction()
object Increment : GlobalAction()
}
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SaveScanNoteResponse(val scanResponse: ScanResponse) : GlobalAction()
data class SetIfCardVerifiedOnline(val verified: Boolean) : GlobalAction()
@ -45,9 +69,6 @@ sealed class GlobalAction : Action {
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()
object GetMoonPayUserStatus : GlobalAction() {
data class Success(val moonPayUserStatus: MoonPayUserStatus) : GlobalAction()
}

View file

@ -3,20 +3,30 @@ 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.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.home.redux.HomeAction
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.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
class GlobalMiddleware {
companion object {
val handler = globalMiddlewareHandler
}
}
private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
@ -27,8 +37,7 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
if (action.result.error is TangemSdkError.UserCancelled) {
store.dispatch(GlobalAction.ScanFailsCounter.Increment)
if (store.state.globalState.scanCardFailsCounter >= 2) {
store.dispatch(HomeAction.ShowDialog.ScanFails)
store.dispatch(WalletAction.ShowDialog.ScanFails)
store.dispatchDialogShow(AppDialog.ScanFailsDialog)
}
} else {
store.dispatch(GlobalAction.ScanFailsCounter.Reset)
@ -63,7 +72,7 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
}
is GlobalAction.UpdateFeedbackInfo -> {
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
@ -72,12 +81,27 @@ val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
)
}
}
}
}
is GlobalAction.ScanCard -> {
scope.launch {
val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId)
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
withMainContext {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> action.onFailure?.invoke(result.error)
}
}
}
}
}
nextDispatch(action)
}

View file

@ -1,6 +1,8 @@
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.preferencesStorage
import org.rekotlin.Action
fun globalReducer(action: Action, state: AppState): GlobalState {
@ -10,6 +12,14 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingState = OnboardingManager(action.scanResponse, usedCardsPrefStorage)
globalState.copy(onboardingManager = onboardingState)
}
is GlobalAction.Onboarding.Stop -> {
globalState.copy(onboardingManager = null)
}
is GlobalAction.ScanFailsCounter.Increment -> {
globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1)
}
@ -17,7 +27,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(scanCardFailsCounter = 0)
}
is GlobalAction.SaveScanNoteResponse ->
globalState.copy(scanNoteResponse = action.scanNoteResponse)
globalState.copy(scanResponse = action.scanResponse)
is GlobalAction.ChangeAppCurrency -> {
globalState.copy(appCurrency = action.appCurrency)
}
@ -29,17 +39,17 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.UpdateWalletSignedHashes -> {
val wallet = globalState.scanNoteResponse?.card
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanNoteResponse?.card
wallet?.let { globalState.scanNoteResponse.card.updateWallet(wallet) }
val wallet = globalState.scanResponse?.card
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanResponse?.card
wallet?.let { globalState.scanResponse.card.updateWallet(wallet) }
if (card != null) {
globalState.copy(scanNoteResponse = globalState.scanNoteResponse.copy(card = card))
globalState.copy(scanResponse = globalState.scanResponse.copy(card = card))
} else {
globalState
}

View file

@ -1,33 +1,33 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
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.MoonPayUserStatus
import org.rekotlin.StateType
data class GlobalState(
val scanNoteResponse: ScanNoteResponse? = null,
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null
val scanResponse: ScanResponse? = null,
val onboardingManager: OnboardingManager? = null,
val cardVerifiedOnline: Boolean = false,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val configManager: ConfigManager? = null,
val warningManager: WarningMessagesManager? = null,
val feedbackManager: FeedbackManager? = null,
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
) : StateType
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String
interface StateDialog
typealias FiatCurrencyName = String

View file

@ -1,12 +1,17 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.transition.TransitionSet
import org.rekotlin.Action
import java.lang.ref.WeakReference
sealed class NavigationAction : Action {
data class NavigateTo(val screen: AppScreen, val addToBackstack: Boolean = true) :
NavigationAction()
data class NavigateTo(
val screen: AppScreen,
val fragmentShareTransition: FragmentShareTransition? = null,
val addToBackstack: Boolean = true
) : NavigationAction()
data class PopBackTo(val screen: AppScreen? = null) : NavigationAction()
@ -14,4 +19,12 @@ sealed class NavigationAction : Action {
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}
}
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
data class ShareElement(val wView: WeakReference<View>, val name: String)

View file

@ -13,7 +13,11 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
val navState = state()?.navigationState
when (action) {
is NavigationAction.NavigateTo -> {
navState?.activity?.get()?.openFragment(action.screen, action.addToBackstack)
navState?.activity?.get()?.openFragment(
action.screen,
action.addToBackstack,
action.fragmentShareTransition
)
}
is NavigationAction.PopBackTo -> {
if (action.screen == AppScreen.Home) {

View file

@ -5,12 +5,19 @@ import org.rekotlin.StateType
import java.lang.ref.WeakReference
data class NavigationState(
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
val backStack: List<AppScreen> = listOf(AppScreen.Home),
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen {
Home, Wallet, WalletDetails, Send, Details, DetailsConfirm, DetailsSecurity, Disclaimer,
CreateTwinWalletWarning, CreateTwinWallet, TwinsOnboarding, AddTokens, WalletConnectSessions,
Home,
Disclaimer,
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
CreateTwinWalletWarning, CreateTwinWallet,
AddTokens,
WalletConnectSessions,
QrScan
}

View file

@ -5,6 +5,7 @@ import android.view.View
import com.google.android.material.button.MaterialButton
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.wallet.redux.ProgressState
/**
[REDACTED_AUTHOR]
@ -12,7 +13,7 @@ import com.tangem.tap.common.extensions.show
open class IndeterminateProgressButtonWidget(
private val button: MaterialButton,
private val progress: View,
initialState: ProgressState = ProgressState.None
initialState: ProgressState = ProgressState.Done
) : ViewStateWidget {
private var initialButtonText: CharSequence = button.text
@ -23,12 +24,14 @@ open class IndeterminateProgressButtonWidget(
changeState(initialState)
}
override val mainView: View = button
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
when (progressState) {
is ProgressState.None -> switchToNone()
is ProgressState.Progress -> switchToProgress()
ProgressState.Done, ProgressState.Error -> switchToNone()
ProgressState.Loading -> switchToProgress()
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
import android.view.ViewGroup
import android.view.animation.*
import android.widget.ViewSwitcher
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class RefreshBalanceWidget(
private val root: ViewGroup
) : ViewStateWidget {
var isShowing: Boolean? = null
private set
override val mainView: View = root.findViewById(R.id.btn_refresh_balance)
private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) }
private val arrowView = mainView.findViewById<View>(R.id.imv_arrow_refresh)
private var progressViewAnimation: Animation? = null
init {
viewSwitcher.inAnimation = AlphaAnimation(0f, 1f).apply {
duration = 400
interpolator = LinearInterpolator()
}
viewSwitcher.outAnimation = AlphaAnimation(1f, 0f).apply {
duration = 400
interpolator = LinearInterpolator()
}
}
override fun changeState(state: WidgetState) {
val progressState = state as? ProgressState ?: return
val currentStateByView = getState()
if (progressState == currentStateByView) return
animateState(progressState)
viewSwitcher.showNext()
}
private fun animateState(state: ProgressState) {
when (state) {
ProgressState.Done, ProgressState.Error -> {
progressViewAnimation?.cancel()
}
ProgressState.Loading -> {
val animation = RotateAnimation(
0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
)
animation.duration = 700
animation.interpolator = AccelerateInterpolator()
animation.repeatCount = -1
arrowView.startAnimation(animation)
progressViewAnimation = animation
}
}
}
fun show(show: Boolean) {
if (show == isShowing) return
isShowing = show
if (show) {
mainView.startAnimation(ShowAnimation())
} else {
mainView.startAnimation(AlphaAnimation(1f, 0f).apply { fillAfter = true })
}
}
private fun isArrowRefreshActive(): Boolean = viewSwitcher.currentView.id == R.id.fl_arrow_refresh_container
private fun getState(): ProgressState = if (isArrowRefreshActive()) ProgressState.Done else ProgressState.Loading
}
class ShowAnimation : AnimationSet(true) {
init {
addAnimation(ScaleAnimation(
0f, 1f,
0f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f)
)
addAnimation(AlphaAnimation(0f, 1f))
duration = 300
interpolator = AnticipateOvershootInterpolator()
}
}

View file

@ -1,5 +1,7 @@
package com.tangem.tap.common.toggleWidget
import android.view.View
/**
[REDACTED_AUTHOR]
@ -7,10 +9,6 @@ package com.tangem.tap.common.toggleWidget
interface WidgetState
interface ViewStateWidget {
val mainView: View
fun changeState(state: WidgetState)
}
sealed class ProgressState : WidgetState {
object None : ProgressState()
data class Progress(val progress: Int = 0) : ProgressState()
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.common.transitions
import androidx.transition.*
/**
[REDACTED_AUTHOR]
*/
class FrontCardEnterTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeImageTransform()) // изменения внутри ImageView
addTransition(ChangeTransform()) // изменение размеров, углов накона
addTransition(ChangeBounds()) // изменение положения
}
}
class FrontCardExitTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeImageTransform())
addTransition(ChangeTransform())
addTransition(ChangeBounds())
}
}
class HomeToOnboardingTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(Fade())
addTransition(ChangeTransform())
addTransition(ChangeBounds())
}
}
class InternalNoteLayoutTransition : TransitionSet() {
init {
ordering = ORDERING_TOGETHER;
addTransition(ChangeTransform())
addTransition(ChangeBounds())
addTransition(Fade())
}
}

View file

@ -0,0 +1,8 @@
package com.tangem.tap.domain
/**
[REDACTED_AUTHOR]
*/
enum class ProductType {
Note, Wallet, Twin, Other
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.domain
import CreateProductWalletAndRescanTask
import android.content.Context
import com.tangem.Message
import com.tangem.TangemSdk
@ -20,9 +21,9 @@ import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.AnalyticsHandler
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -31,20 +32,34 @@ import kotlin.coroutines.suspendCoroutine
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
suspend fun scanNote(
analyticsHandler: AnalyticsHandler, messageRes: Int? = null,
): CompletionResult<ScanNoteResponse> {
suspend fun scanProduct(
analyticsHandler: AnalyticsHandler,
messageRes: Int? = null,
): CompletionResult<ScanResponse> {
analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null)
val result = runTaskAsyncReturnOnMain(ScanNoteTask(),
initialMessage = Message(
context.getString(messageRes ?: R.string.initial_message_scan_header)
))
if (result is CompletionResult.Failure) {
val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header))
return runTaskAsyncReturnOnMain(ScanProductTask(), null, message)
.also { sendScanFailuresToAnalytics(analyticsHandler, it) }
}
suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult<Card> {
return runTaskAsync(
CreateProductWalletAndRescanTask(scanResponse.productType),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body))
)
}
private fun sendScanFailuresToAnalytics(
analyticsHandler: AnalyticsHandler,
result: CompletionResult<ScanResponse>
) {
if (result is CompletionResult.Failure && result.error is TangemSdkError) {
(result.error as? TangemSdkError)?.let { error ->
analyticsHandler.logCardSdkError(error, FirebaseAnalyticsHandler.ActionToLog.Scan)
}
}
return result
}
suspend fun createWallet(cardId: String?): CompletionResult<Card> {
@ -106,7 +121,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTwinCard()) 4 else null
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTangemTwin()) 4 else null
}
companion object {

View file

@ -1,13 +0,0 @@
package com.tangem.tap.domain
class TapBlockchainConfig {
companion object {
val blockchairApiKey: String = "A___0Shpsu4KagE7oSabrw20DfXAqWlT"
val blockcypherTokens: Set<String> = setOf(
"aa8184b0e0894b88a5688e01b3dc1e82",
"56c4ca23c6484c8f8864c32fde4def8d",
"66a8a37c5e9d4d2c9bb191acfe7f93aa"
)
val infuraProjectId: String = "613a0b14833145968b1f656240c7d245"
}
}

View file

@ -16,12 +16,13 @@ interface MultiMessageError : TapErrors {
}
sealed class TapError(
@StringRes val localizedMessage: Int,
override val args: List<Any>? = null
@StringRes val messageResource: Int,
override val args: List<Any>? = null
) : Throwable(), TapErrors, ArgError {
object UnknownError : TapError(R.string.send_error_unknown)
data class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
open class CustomError(val customMessage: String) : TapError(R.string.common_custom_string, listOf(customMessage))
object ScanCardError : TapError(R.string.scan_card_error)
object PayIdAlreadyCreated : TapError(R.string.wallet_create_payid_error_already_created)
object PayIdCreatingError : TapError(R.string.wallet_create_payid_error_message)
object PayIdEmptyField : TapError(R.string.wallet_create_payid_empty)
@ -38,8 +39,13 @@ sealed class TapError(
object DustChange : TapError(R.string.send_error_dust_change)
data class CreateAccountUnderfunded(override val args: List<Any>) : TapError(R.string.send_error_no_target_account)
sealed class XmlError {
object AssetAccountNotCreated: TapError(R.string.send_error_no_account_xlm)
sealed class XmlError {
object AssetAccountNotCreated : TapError(R.string.send_error_no_account_xlm)
}
sealed class WalletManagerUpdate {
class NoAccountError(amountToCreateAccount: String): CustomError(amountToCreateAccount)
class InternalError(message: String): CustomError(message)
}
data class ValidateTransactionErrors(
@ -60,7 +66,7 @@ fun TapErrors.assembleErrors(): MutableList<Pair<Int, List<Any>?>> {
val idList = mutableListOf<Pair<Int, List<Any>?>>()
when (this) {
is MultiMessageError -> this.errorList.forEach { idList.addAll(it.assembleErrors()) }
is TapError -> idList.add(Pair(this.localizedMessage, this.args))
is TapError -> idList.add(Pair(this.messageResource, this.args))
}
return idList
}

View file

@ -5,6 +5,8 @@ import com.tangem.common.card.Card
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.currenciesRepository
@ -12,10 +14,10 @@ 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.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.WalletAction
@ -38,28 +40,17 @@ class TapWalletManager {
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun loadWalletData(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
handleUpdateWalletResult(result, walletManager)
handleUpdateWalletResult(walletManager.safeUpdate(), walletManager)
}
suspend fun updateWallet(walletManager: WalletManager) {
val result = try {
walletManager.update()
Result.Success(walletManager.wallet)
} catch (exception: Exception) {
Result.Failure(exception)
}
val result = walletManager.safeUpdate()
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(WalletAction.UpdateWallet.Success(result.data))
is Result.Failure ->
store.dispatch(WalletAction.UpdateWallet.Failure(result.error?.localizedMessage))
store.dispatch(WalletAction.UpdateWallet.Failure(result.error.localizedMessage))
}
}
@ -86,7 +77,7 @@ class TapWalletManager {
handleFiatRatesResult(results)
}
suspend fun onCardScanned(data: ScanNoteResponse, addAnalyticsEvent: Boolean = false) {
suspend fun onCardScanned(data: ScanResponse, addAnalyticsEvent: Boolean = false) {
if (addAnalyticsEvent) {
FirebaseAnalyticsHandler.triggerEvent(AnalyticsEvent.CARD_IS_SCANNED, data.card, data.walletData?.blockchain)
}
@ -98,12 +89,9 @@ class TapWalletManager {
store.dispatch(GlobalAction.SaveScanNoteResponse(data))
store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard))
store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed))
if (data.card.isTwinCard()) {
val cardNumber = TwinsHelper.getTwinCardNumber(data.card.cardId)
if (cardNumber != null) {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(
cardNumber, isCreatingTwinCardsAllowed = true
))
if (data.card.isTangemTwin()) {
data.card.getTwinCardNumber()?.let {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(it, true))
}
}
@ -116,7 +104,7 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanNoteResponse) {
private fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.getBlockchain()
if (data.card.isStart2Coin) {
@ -132,7 +120,7 @@ class TapWalletManager {
}
}
suspend fun loadData(data: ScanNoteResponse) {
suspend fun loadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadCardInfo(data.card))
@ -142,7 +130,7 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.wallets.isEmpty() ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
@ -151,7 +139,7 @@ class TapWalletManager {
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != null && primaryWalletManager != null) {
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
@ -228,14 +216,14 @@ class TapWalletManager {
}
}
suspend fun reloadData(data: ScanNoteResponse) {
suspend fun reloadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
when {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
}
data.card.wallets.isEmpty() ||
(data.card.isTwinCard() && data.secondTwinPublicKey == null) -> {
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
@ -260,23 +248,23 @@ class TapWalletManager {
when (result) {
is Result.Success -> store.dispatch(WalletAction.LoadWallet.Success(result.data))
is Result.Failure -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val error = result.error
val blockchain = walletManager.wallet.blockchain
if (error != null && blockchain.isNoAccountError(error)) {
val token = walletManager.wallet.getFirstToken()
val amountToCreateAccount = blockchain.amountToCreateAccount(token)
if (amountToCreateAccount != null) {
store.dispatch(WalletAction.LoadWallet.NoAccount(
walletManager.wallet,
amountToCreateAccount.toString()))
return@withContext
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
TapError.NoInternetConnection -> {
store.dispatch(WalletAction.LoadData.Failure(error))
}
is TapError.WalletManagerUpdate.NoAccountError -> {
store.dispatch(WalletAction.LoadWallet
.NoAccount(walletManager.wallet, error.customMessage))
}
is TapError.WalletManagerUpdate.InternalError -> {
store.dispatch(WalletAction.LoadWallet
.Failure(walletManager.wallet, error.customMessage))
}
else -> {
store.dispatchDebugErrorNotification(error)
}
}
store.dispatch(WalletAction.LoadWallet.Failure(walletManager.wallet, result.error?.localizedMessage))
}
}
}

View file

@ -4,10 +4,10 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.tap.domain.TapWorkarounds.isNote
import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import java.util.*
object TapWorkarounds {
@ -28,26 +28,10 @@ object TapWorkarounds {
return excludedBatch || excludedIssuerName
}
fun Card.isNote(): Boolean {
return notesBatches.contains(batchId)
}
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
fun Card.isTangemWallet(): Boolean = tangemWalletBatches.contains(batchId)
fun Card.isMultiCurrencyWallet(): Boolean {
return multiCurrencyWalletsBatches.contains(batchId)
}
val Card.noteCurrency: Blockchain?
get() {
return when (batchId) {
"AB01" -> Blockchain.Bitcoin
"AB02" -> Blockchain.Ethereum
"AB03" -> Blockchain.CardanoShelley
"AB04" -> Blockchain.Dogecoin
"AB05" -> Blockchain.Binance
"AB06" -> Blockchain.XRP
else -> null
}
}
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
@ -64,21 +48,23 @@ object TapWorkarounds {
"TTM BANK"
)
private val notesBatches = listOf(
"AB01",
"AB02",
"AB03",
"AB04",
"AB05",
"AB06",
)
private val tangemWalletBatches = listOf("AC01")
private val multiCurrencyWalletsBatches = listOf("AC01")
private val tangemNoteBatches = mapOf(
"AB01" to Blockchain.Bitcoin,
"AB02" to Blockchain.Ethereum,
"AB03" to Blockchain.CardanoShelley,
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.Binance,
"AB06" to Blockchain.XRP,
)
}
val DELAY_SDK_DIALOG_CLOSE = 1400L
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTwinCard() && !isStart2Coin && !isNote()
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

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

View file

@ -16,7 +16,7 @@ fun Blockchain.isNoAccountError(exception: Throwable): Boolean {
fun Blockchain.amountToCreateAccount(token: Token? = null): Double? {
return when (this) {
Blockchain.Stellar -> if (token?.symbol == NODL) 1.5 else 1.toDouble()
Blockchain.XRP -> 20.toDouble()
Blockchain.XRP -> 10.toDouble()
else -> null
}
}

View file

@ -2,11 +2,24 @@ package com.tangem.tap.domain.extensions
import com.tangem.common.card.Card
import com.tangem.common.card.CardWallet
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.features.wallet.redux.Artwork
fun Card.getSingleWallet(): CardWallet? {
return wallets.firstOrNull()
}
fun Card.hasWallets(): Boolean = wallets.isNotEmpty()
fun Card.hasNoWallets(): Boolean = wallets.isEmpty()
fun Card.hasSingleWallet(): Boolean = wallets.size == 1
fun Card.hasSignedHashes(): Boolean {
return wallets.any { it.totalSignedHashes ?: 0 > 0 }
}
@ -15,6 +28,46 @@ fun Card.signedHashesCount(): Int {
return wallets.map { it.totalSignedHashes ?: 0 }.sum()
}
suspend fun Card.getOrLoadCardArtworkUrl(cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null): String {
fun ifAnyError(): String {
return when {
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> {
when (getTwinCardNumber()) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
}
}
}
val cardInfoResult = cardInfo ?: OnlineCardVerifier().getCardInfo(cardId, cardPublicKey)
return when (cardInfoResult) {
is Result.Success -> {
val artworkId = cardInfoResult.data.artwork?.id
if (artworkId == null || artworkId.isEmpty()) {
ifAnyError()
} else {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
}
is Result.Failure -> ifAnyError()
}
}
fun Card.getArtworkUrl(artworkId: String?): String? {
return when {
artworkId != null -> {
OnlineCardVerifier.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)
}
cardId.startsWith(Artwork.SERGIO_CARD_ID) -> Artwork.SERGIO_CARD_URL
cardId.startsWith(Artwork.MARTA_CARD_ID) -> Artwork.MARTA_CARD_URL
else -> null
}
}
val Card.remainingSignatures: Int?
get() = this.getSingleWallet()?.remainingSignatures

View file

@ -8,8 +8,8 @@ import com.tangem.common.card.CardWallet
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.isTangemTwin
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
@ -50,20 +50,22 @@ fun WalletManagerFactory.makeWalletManagersForApp(
}
fun WalletManagerFactory.makePrimaryWalletManager(
data: ScanNoteResponse,
data: ScanResponse,
): WalletManager? {
val card = data.card
val blockchain = if (card.isTestCard) {
data.getBlockchain()?.getTestnetVersion()
data.getBlockchain().getTestnetVersion()
} else {
data.getBlockchain()
}
val supportedCurves = blockchain?.getSupportedCurves() ?: return null
val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) }
val wallet = selectWallet(wallets)
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve ?: return null
return if (card.isTwinCard() && data.secondTwinPublicKey != null) {
val curveToUse = wallet.curve
return if (card.isTangemTwin() && data.secondTwinPublicKey != null) {
makeMultisigWalletManager(
cardId = card.cardId,
walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(),

View file

@ -11,6 +11,7 @@ import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
@Deprecated("Use CreateProductWalletAndRescanTask instead")
class CreateWalletAndRescanTask : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {

View file

@ -9,19 +9,16 @@ import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
class CreateWalletsTask(curves: List<EllipticCurve> = emptyList()) : CardSessionRunnable<Card> {
@Deprecated("Use CreateProductWalletsResponse instead")
class CreateWalletsTask(curves: List<EllipticCurve>? = null) : CardSessionRunnable<Card> {
private val curves = if (curves.isEmpty()) {
listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
)
} else {
curves
}
private val curves = curves ?: listOf(
EllipticCurve.Secp256k1,
EllipticCurve.Ed25519,
EllipticCurve.Secp256r1,
)
var index = 0
private var index = 0
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val curve = curves[index]
@ -29,8 +26,8 @@ class CreateWalletsTask(curves: List<EllipticCurve> = emptyList()) : CardSession
}
private fun createWallet(
curve: EllipticCurve, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
curve: EllipticCurve, session: CardSession,
callback: (result: CompletionResult<Card>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
@ -49,12 +46,4 @@ class CreateWalletsTask(curves: List<EllipticCurve> = emptyList()) : CardSession
}
}
}
}
private fun EllipticCurve.toWalletIndex(): Int {
return when (this) {
EllipticCurve.Secp256k1 -> 0
EllipticCurve.Ed25519 -> 1
EllipticCurve.Secp256r1 -> 2
}
}

View file

@ -1,60 +1,32 @@
package com.tangem.tap.domain.tasks
import com.tangem.*
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.WalletData
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.CommandResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isMultiCurrencyWallet
import com.tangem.tap.domain.TapWorkarounds.noteCurrency
import com.tangem.tap.domain.TapWorkarounds.isNote
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.store
import com.tangem.wallet.R
data class ScanNoteResponse(
val card: Card,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
) : CommandResponse {
fun getBlockchain(): Blockchain? {
if (card.noteCurrency != null) return card.noteCurrency
val blockchainName: String = walletData?.blockchain ?: return null
return Blockchain.fromId(blockchainName)
}
fun getPrimaryToken(): Token? {
val cardToken = walletData?.token ?: return null
return Token(
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
Blockchain.fromId(walletData.blockchain)
)
}
}
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteResponse> {
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
@ -69,13 +41,13 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return@run
}
if (card.isTwinCard()) {
if (card.isTangemTwin()) {
dealWithTwinCard(card, session, callback)
} else if (card.firmwareVersion.major >= 4) {
} else if (!card.isTangemNote() && card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
} else {
callback(CompletionResult.Success(
ScanNoteResponse(card, session.environment.walletData)))
ScanResponse(card, ProductType.Other, session.environment.walletData)))
}
}
}
@ -84,28 +56,27 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty()) {
callback(CompletionResult.Success(
ScanNoteResponse(card, walletData)
))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesPresent = card.wallets.map { it.curve }
val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
// val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
val curvesToCreate = card.supportedCurves.subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanNoteResponse(card, walletData)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
CreateWalletsTask(curvesToCreate.toList()).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanNoteResponse(result.data, walletData)))
callback(CompletionResult.Success(ScanResponse(result.data, ProductType.Other, walletData)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
@ -114,34 +85,35 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
private fun dealWithTwinCard(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, publicKey
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanNoteResponse(
card = card,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
ScanResponse(
card = card,
ProductType.Other,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanNoteResponse(card, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
}
@ -149,7 +121,8 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
// Disable new cards on the old version of the app // TODO: remove when cards are supported
if (card.isMultiCurrencyWallet() || card.isNote()) return UpdateAppToUseThisCard()
// Disable new multi-currency HD wallet cards on the old version of the app
// if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
return null
}
@ -159,10 +132,4 @@ class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanNoteRespons
return WalletManagerFactory(blockchainSdkConfig)
}
}
class UpdateAppToUseThisCard : TangemError {
override val code: Int = 50005
override var customMessage: String = code.toString()
override val messageResId: Int = R.string.error_update_app
}
}

View file

@ -0,0 +1,119 @@
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.tasks.product.CreateProductWalletsTask
import com.tangem.tap.domain.tasks.product.ProductCommandProcessor
import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletAndRescanTask(
private val type: ProductType,
) : CardSessionRunnable<Card> {
override fun run(session: CardSession, callback: (result: CompletionResult<Card>) -> Unit) {
val card = session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
CreateProductWalletTask(card, type).run(session) { result ->
when (result) {
is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
private class CreateProductWalletTask(
private val card: Card,
private val type: ProductType,
) : CardSessionRunnable<CreateWalletResponse> {
override fun run(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
val commandProcessor = when (type) {
ProductType.Note -> CreateWalletTangemNote()
ProductType.Twin -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
ProductType.Wallet -> CreateWalletTangemWallet()
else -> CreateWalletOtherCards()
}
commandProcessor.proceed(card, session, callback)
}
}
private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
if (card.supportedCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val curvesSupportedByBlockchain = card.getTangemNoteBlockchain()?.getSupportedCurves()
if (curvesSupportedByBlockchain == null || curvesSupportedByBlockchain.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
val intersectCurves = card.supportedCurves.intersect(curvesSupportedByBlockchain).toList()
if (intersectCurves.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
} else {
val curve = if (intersectCurves.contains(EllipticCurve.Secp256k1)) {
EllipticCurve.Secp256k1
} else {
intersectCurves[0]
}
CreateWalletCommand(curve).run(session, callback)
}
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
//TODO:
}
}
private class CreateWalletOtherCards : ProductCommandProcessor<CreateWalletResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<CreateWalletResponse>) -> Unit
) {
val firmwareVersion = card.firmwareVersion
val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
CreateProductWalletsTask(listOf(card.supportedCurves.first()))
} else {
CreateProductWalletsTask(card.getCurvesForNonCreatedWallets())
}
task.run(session) { result ->
when (result) {
is CompletionResult.Success -> callback(CompletionResult.Success(result.data.createWalletResponses[0]))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,49 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.CommandResponse
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
/**
[REDACTED_AUTHOR]
*/
class CreateProductWalletsResponse(
val createWalletResponses: List<CreateWalletResponse>
) : CommandResponse
class CreateProductWalletsTask(
private val curves: List<EllipticCurve>,
) : CardSessionRunnable<CreateProductWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
override fun run(session: CardSession, callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit) {
val curve = curves[createdWalletsResponses.size]
createWallet(curve, session, callback)
}
private fun createWallet(
curve: EllipticCurve,
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletsResponse>) -> Unit
) {
CreateWalletCommand(curve).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createdWalletsResponses.add(result.data)
if (createdWalletsResponses.size == curves.size) {
callback(CompletionResult.Success(CreateProductWalletsResponse(createdWalletsResponses)))
return@run
}
createWallet(curves[createdWalletsResponses.size], session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.core.CardSession
/**
[REDACTED_AUTHOR]
*/
interface ProductCommandProcessor<T> {
fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<T>) -> Unit,
)
}

View file

@ -0,0 +1,183 @@
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
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.card.WalletData
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.toHexString
import com.tangem.operations.CommandResponse
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.operations.ScanTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain
import com.tangem.tap.domain.TapWorkarounds.isExcluded
import com.tangem.tap.domain.TapWorkarounds.isTangemNote
import com.tangem.tap.domain.TapWorkarounds.isTangemWallet
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.domain.twins.isTangemTwin
data class ScanResponse(
val card: Card,
val productType: ProductType,
val walletData: WalletData?,
val secondTwinPublicKey: String? = null,
) : CommandResponse {
fun getBlockchain(): Blockchain {
if (productType == ProductType.Note) return card.getTangemNoteBlockchain() ?: return Blockchain.Unknown
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
return Blockchain.fromId(blockchainName)
}
fun getPrimaryToken(): Token? {
val cardToken = walletData?.token ?: return null
return Token(
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
Blockchain.fromId(walletData.blockchain)
)
}
}
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwin() -> ScanTwinProcessor()
card.isTangemWallet() -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun getErrorIfExcludedCard(card: Card): TangemError? {
if (card.isExcluded()) return TapSdkError.CardForDifferentApp
return null
}
}
private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Note, session.environment.walletData)))
}
}
private class ScanWalletProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData)))
}
}
private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ReadIssuerDataCommand().run(session) { readDataResult ->
when (readDataResult) {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
val response = ScanResponse(card, ProductType.Twin, walletData, twinPublicKey.toHexString())
callback(CompletionResult.Success(response))
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
}
}
}
}
private class ScanOtherCardsProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(card: Card, session: CardSession, callback: (result: CompletionResult<ScanResponse>) -> Unit) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesToCreate = card.getCurvesForNonCreatedWallets()
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
CreateProductWalletsTask(curvesToCreate).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(PreflightReadMode.FullCardRead, card.cardId).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
val response = ScanResponse(readResult.data, ProductType.Other, walletData)
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
}
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent)
return curvesForNonCreatedWallets.toList()
}

View file

@ -6,16 +6,16 @@ import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.ScanNoteTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.product.ScanResponse
class FinalizeTwinTask(
private val twinPublicKey: ByteArray, private val issuerKeys: KeyPair,
) : CardSessionRunnable<ScanNoteResponse> {
) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanNoteResponse>) -> Unit,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result ->
when (result) {
@ -23,7 +23,7 @@ class FinalizeTwinTask(
PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success ->
ScanNoteTask(readResult.data).run(session, callback)
ScanProductTask(readResult.data).run(session, callback)
is CompletionResult.Failure ->
callback(CompletionResult.Failure(readResult.error))
}

View file

@ -14,19 +14,19 @@ import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context: Context) {
class TwinCardsManager(private val scanResponse: ScanResponse, context: Context) {
private val currentCardId: String = scanNoteResponse.card.cardId
private val currentCardId: String = scanResponse.card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(
context, scanNoteResponse.card.issuer.publicKey.toHexString()
context, scanResponse.card.issuer.publicKey.toHexString()
)
suspend fun createFirstWallet(message: Message): SimpleResult {
@ -43,7 +43,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanNoteResponse.card
card = scanResponse.card
)
}
return SimpleResult.failure(response.error)
@ -65,9 +65,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
preparingMessage = preparingMessage,
creatingWalletMessage = creatingWalletMessage
)
val response = tangemSdkManager.runTaskAsync(
task, null, initialMessage
)
val response = tangemSdkManager.runTaskAsync(task, null, initialMessage)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
@ -78,7 +76,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanNoteResponse.card
card = scanResponse.card
)
}
return SimpleResult.failure(response.error)
@ -87,7 +85,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
}
suspend fun complete(message: Message): Result<ScanNoteResponse> {
suspend fun complete(message: Message): Result<ScanResponse> {
val response = tangemSdkManager.runTaskAsync(
FinalizeTwinTask(secondCardPublicKey!!.hexToBytes(), issuerKeyPair),
currentCardId, message
@ -99,7 +97,7 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WriteIssuerData,
card = scanNoteResponse.card
card = scanResponse.card
)
}
Result.failure(response.error)
@ -109,11 +107,11 @@ class TwinCardsManager(private val scanNoteResponse: ScanNoteResponse, context:
companion object {
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65) return false
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return (cardWalletPublicKey != null &&
CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey))
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
private fun getIssuerKeys(context: Context, publicKey: String): KeyPair {

View file

@ -0,0 +1,152 @@
package com.tangem.tap.domain.twins
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import androidx.core.animation.doOnEnd
import com.tangem.tap.common.leapfrogWidget.LeapView
import com.tangem.tap.common.leapfrogWidget.LeapViewState
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
/**
[REDACTED_AUTHOR]
*/
class TwinsCardWidget(
val leapfrogWidget: LeapfrogWidget,
val getTopOfAnchorViewForActivateState: () -> Float
) {
init {
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
}
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
}
}
fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
animator.playTogether(
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
)
animator.start()
}
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) 400 else 0
doOnEnd { onEnd() }
}
}
private fun createAnimator(cardNumber: TwinCardNumber, properties: TwinsCardProperties): ObjectAnimator {
val view = getLeapViewByCardNumber(cardNumber).view
val animator = ObjectAnimator.ofPropertyValuesHolder(view, *properties.createValuesHolders().toTypedArray())
view.elevation = properties.elevation
return animator
}
private fun createWelcomeProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -180f,
yTranslation = -350f,
rotation = -3f,
elevation = 1f,
scale = 1.1f,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 180f,
yTranslation = 10f,
rotation = -3f,
elevation = 0f,
scale = 1.1f,
)
}
}
}
private fun createLeapfrogProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
return twinProperties.copy(
xTranslation = 0f,
yTranslation = 0f,
rotation = 0f,
scale = 1f,
)
}
private fun createActivateProperties(cardNumber: TwinCardNumber): TwinsCardProperties {
val topOfAnchorView = getTopOfAnchorViewForActivateState()
val twinProperties = TwinsCardProperties.from(getLeapViewByCardNumber(cardNumber).state)
return twinProperties.copy(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.4f,
)
}
private fun getLeapViewByCardNumber(cardNumber: TwinCardNumber): LeapView {
return when (cardNumber) {
TwinCardNumber.First -> leapfrogWidget.getViewByPosition(0)
TwinCardNumber.Second -> leapfrogWidget.getViewByPosition(1)
}
}
}
private data class TwinsCardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,
val rotation: Float = 0f,
val elevation: Float = 0f,
val scale: Float = 1.1f,
) {
fun createValuesHolders(): List<PropertyValuesHolder> {
return listOf(
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation),
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation),
PropertyValuesHolder.ofFloat(View.ROTATION, rotation),
PropertyValuesHolder.ofFloat(View.SCALE_X, scale),
PropertyValuesHolder.ofFloat(View.SCALE_Y, scale),
)
}
companion object {
fun from(leapViewState: LeapViewState): TwinsCardProperties {
val leapViewProperties = leapViewState.properties
return TwinsCardProperties(
yTranslation = leapViewProperties.yTranslation,
elevation = leapViewProperties.elevationEnd,
scale = leapViewProperties.scale,
)
}
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.tap.domain.twins
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.isEven
import com.tangem.tap.domain.twins.TwinsHelper.Companion.getTwinCardNumber
class TwinsHelper {
companion object {
@ -35,22 +33,6 @@ class TwinsHelper {
}
}
fun getTwinsCardId(cardId: String): String? {
val cardIdWithNewSeries = when (getTwinCardNumber(cardId) ?: return null) {
TwinCardNumber.First -> {
val index = if (cardId.startsWith(firstCardSeries[0])) 0 else 1
cardId.replace(firstCardSeries[index], secondCardSeries[index])
}
TwinCardNumber.Second -> {
val index = if (cardId.startsWith(secondCardSeries[0])) 0 else 1
cardId.replace(secondCardSeries[index], firstCardSeries[index])
}
}
val cardIdWithoutChecksum = cardIdWithNewSeries.dropLast(1)
val checkSum = cardIdWithoutChecksum.calculateLuhn()
return cardIdWithoutChecksum + checkSum
}
fun getTwinCardIdForUser(cardId: String): String {
if (cardId.length < 16) return cardId
@ -64,21 +46,6 @@ class TwinsHelper {
}
}
private fun String.calculateLuhn(): Int {
val checksum = this.reversed()
.mapIndexed { index, c ->
val digit = if (c in '0'..'9') c - '0' else c - 'A'
if (!index.isEven()) {
digit
} else {
val newDigit = digit * 2
if (newDigit >= 10) newDigit - 9 else newDigit
}
}.sum()
.rem(10)
return (10 - checksum) % 10
}
enum class TwinCardNumber(val number: Int) {
First(1), Second(2);
@ -88,8 +55,12 @@ enum class TwinCardNumber(val number: Int) {
}
}
fun Card.isTwinCard(): Boolean {
return getTwinCardNumber(cardId) != null
fun Card.isTangemTwin(): Boolean {
return TwinsHelper.getTwinCardNumber(cardId) != null
}
fun Card.getTwinCardNumber(): TwinCardNumber? {
return TwinsHelper.getTwinCardNumber(this.cardId)
}
fun Card.getTwinCardIdForUser(): String {

View file

@ -7,7 +7,7 @@ import com.tangem.common.card.Card
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.termsOfUse.CardTou
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
@ -18,13 +18,13 @@ import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val scanNoteResponse: ScanNoteResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
val card: Card,
val scanResponse: ScanResponse,
val wallets: List<Wallet>,
val isCreatingTwinWalletAllowed: Boolean?,
val cardTou: CardTou,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
) : DetailsAction()
object ShowDisclaimer : DetailsAction()
@ -81,7 +81,7 @@ sealed class DetailsAction : Action {
}
data class LaunchThirdStep(val message: Message) : CreateTwinWalletAction() {
data class Success(val scanNoteResponse: ScanNoteResponse) : CreateTwinWalletAction()
data class Success(val scanResponse: ScanResponse) : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
}

View file

@ -7,11 +7,11 @@ import com.tangem.tap.domain.TapWorkarounds.isStart2Coin
import com.tangem.tap.domain.extensions.isWalletDataSupported
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.getTwinCardNumber
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletReducer
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import org.rekotlin.Action
import java.util.*
@ -46,10 +46,10 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
val twinsState = if (action.card.isTwinCard()) {
val twinsState = if (action.card.isTangemTwin()) {
CreateTwinWalletState(
scanResponse = action.scanNoteResponse,
twinCardNumber = TwinsHelper.getTwinCardNumber(action.card.cardId),
scanResponse = action.scanResponse,
twinCardNumber = action.card.getTwinCardNumber(),
createTwinWallet = null,
showAlert = false,
allowRecreatingWallet = action.isCreatingTwinWalletAllowed
@ -72,12 +72,10 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard =
state.card?.settings?.isPermanentWallet == true
|| state.card?.isWalletDataSupported == true
val notAllowedByAnyWallet = state.card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet || state.card?.isWalletDataSupported == true
val notEmpty = state.wallets.any {
!it.recentTransactions.toPendingTransactions(it.address).isNullOrEmpty()
|| it.amounts.toSendableAmounts().isNotEmpty()
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
@ -191,10 +189,11 @@ private fun handleSecurityAction(
private fun prepareAllowedSecurityOptions(
card: Card?, currentSecurityOption: SecurityOption?
): EnumSet<SecurityOption> {
val prohibitDefaultPin = card?.settings?.isRemovingAccessCodeAllowed != true
val prohibitDefaultPin = card?.settings?.isResettingUserCodesAllowed != true
val allowedSecurityOptions = EnumSet.noneOf(SecurityOption::class.java)
if (card?.isTwinCard() == true) {
if (card?.isTangemTwin() == true) {
allowedSecurityOptions.add(SecurityOption.PassCode)
}
if ((currentSecurityOption == SecurityOption.LongTap) || !prohibitDefaultPin) {

View file

@ -48,7 +48,7 @@ class CreateTwinWalletMiddleware {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanNoteResponse?.let {
store.state.globalState.scanResponse?.let {
twinsManager = TwinCardsManager(it, action.context)
}
scope.launch {
@ -106,7 +106,7 @@ class CreateTwinWalletMiddleware {
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanNoteResponse)
store.state.globalState.tapWalletManager.onCardScanned(action.scanResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))

View file

@ -1,15 +1,15 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardNumber
data class CreateTwinWalletState(
val scanResponse: ScanNoteResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
val scanResponse: ScanResponse?,
val step: CreateTwinWalletStep = CreateTwinWalletStep.FirstStep,
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet?,
val showAlert: Boolean,
val allowRecreatingWallet: Boolean? = null
)
enum class CreateTwinWalletStep { FirstStep, SecondStep, ThirdStep }

View file

@ -1,11 +1,10 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.*
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.redux.AppState
@ -18,9 +17,6 @@ import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.walletconnect.WalletConnectManager
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.wallet.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
class WalletConnectMiddleware {
@ -132,49 +128,35 @@ class WalletConnectMiddleware {
}
private fun handleScanCard(wcUri: String) {
scope.launch {
val result = tangemSdkManager.scanNote(
FirebaseAnalyticsHandler,
R.string.wallet_connect_scan_card_message
)
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
val card = result.data.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@withContext
}
val walletManager = getWalletManager(card)
if (walletManager == null) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@withContext
}
val key = walletManager.wallet.publicKey
store.dispatchOnMain(WalletConnectAction.OpenSession(
wcUri = wcUri,
wallet = WalletForSession(
card.cardId, key.toHexString(),
isTestNet = card.isTestCard
),
))
}
is CompletionResult.Failure ->
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(
null
))
}
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
val card = scanResponse.card
if (!card.isMultiwalletAllowed) {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@ScanCard
}
}
val walletManager = getWalletManager(card).guard {
store.dispatchOnMain(WalletConnectAction.UnsupportedCard)
return@ScanCard
}
val key = walletManager.wallet.publicKey
store.dispatchOnMain(WalletConnectAction.OpenSession(
wcUri = wcUri,
wallet = WalletForSession(
card.cardId, key.toHexString(),
isTestNet = card.isTestCard
),
))
}, {
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(null))
}, R.string.wallet_connect_scan_card_message))
}
private fun getWalletManager(card: Card): WalletManager? {
val factory = store.state.globalState.tapWalletManager.walletManagerFactory
return if (store.state.globalState.scanNoteResponse?.card?.cardId == card.cardId) {
return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) {
store.state.walletState.getWalletManager(Blockchain.Ethereum)
?: factory.makeWalletManagerForApp(card, Blockchain.Ethereum)
?.also { walletManager ->

View file

@ -2,7 +2,7 @@ package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
import com.trustwallet.walletconnect.models.WCPeerMeta

View file

@ -12,7 +12,7 @@ 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.domain.twins.getTwinCardIdForUser
import com.tangem.tap.domain.twins.isTwinCard
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.SecurityOption
@ -68,7 +68,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
val cardId = if (state.card?.isTwinCard() == true) {
val cardId = if (state.card?.isTangemTwin() == true) {
state.card.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
@ -78,8 +78,8 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_signed_hashes.text = state.cardInfo.signedHashes.toString()
}
tv_signed_hashes.show(state.card?.isTwinCard() != true)
tv_signed_hashes_title.show(state.card?.isTwinCard() != true)
tv_signed_hashes.show(state.card?.isTangemTwin() != true)
tv_signed_hashes_title.show(state.card?.isTangemTwin() != true)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }

View file

@ -1,8 +1,9 @@
package com.tangem.tap.features.disclaimer.redux
import com.tangem.tap.common.post
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import org.rekotlin.Middleware
@ -14,16 +15,10 @@ class DisclaimerMiddleware {
when (action) {
is DisclaimerAction.AcceptDisclaimer -> {
preferencesStorage.saveDisclaimerAccepted()
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
store.dispatch(NavigationAction.PopBackTo())
//delayed sending used for better animation
post(550) { store.dispatch(HomeAction.ReadCard) }
}
}
next(action)

View file

@ -58,6 +58,9 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
override fun newState(state: DisclaimerState) {
if (activity == null) return
if (state.accepted) btn_accept.hide()
if (state.accepted) {
half_transparent_overlay.hide()
btn_accept.hide()
}
}
}

View file

@ -1,27 +1,62 @@
package com.tangem.tap.features.home
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.global.StateDialog
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 com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.home.redux.HomeDialog
import com.tangem.tap.features.home.redux.HomeState
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_home.*
import org.rekotlin.StoreSubscriber
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_home.*
import java.lang.ref.WeakReference
class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState> {
class HomeFragment : BaseOnboardingFragment<HomeState>() {
private var dialog: Dialog? = null
private lateinit var btnScanCard: ViewStateWidget
override fun onStart() {
super.onStart()
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
store.dispatch(HomeAction.Init)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.hide()
val shareTransition = FragmentShareTransition(
listOf(ShareElement(WeakReference(imv_front_card), imv_front_card.transitionName)),
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)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.homeState == newState.homeState
@ -29,51 +64,15 @@ class HomeFragment : Fragment(R.layout.fragment_home), StoreSubscriber<HomeState
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun newState(state: HomeState) {
if (activity == null) return
if (state.firstLaunch) {
tv_home_description.text = getText(R.string.home_welcome)
btn_shop.text = getText(R.string.common_no)
btn_yes.text = getText(R.string.home_button_yes)
} else {
tv_home_description.text = getText(R.string.home_welcome_back)
btn_shop.text = getText(R.string.home_button_shop)
btn_yes.text = getText(R.string.home_button_scan)
}
handleDialog(state.dialog)
btnScanCard.changeState(state.btnScanState.progressState)
btnScanCard.mainView.isEnabled = state.btnScanState.enabled
}
private fun handleDialog(stateDialog: StateDialog?) {
when (stateDialog) {
is HomeDialog.ScanFailsDialog -> {
if (dialog == null) dialog = ScanFailsDialog.create(requireContext()).apply { show() }
}
else -> {
dialog?.dismiss()
dialog = null
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
btn_yes?.setOnClickListener { store.dispatch(HomeAction.ReadCard) }
btn_shop?.setOnClickListener { store.dispatch(HomeAction.GoToShop(requireContext())) }
store.dispatch(GlobalAction.GetMoonPayUserStatus)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
exitTransition = inflater.inflateTransition(R.transition.fade)
override fun onDestroyView() {
store.dispatch(HomeAction.SetFragmentShareTransition(null))
super.onDestroyView()
}
}

View file

@ -1,18 +1,20 @@
package com.tangem.tap.features.home.redux
import android.content.Context
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import org.rekotlin.Action
sealed class HomeAction : Action {
// from ui
object ReadCard : HomeAction()
data class GoToShop(val context: Context) : HomeAction()
object CheckIfFirstLaunch : HomeAction() {
data class Result(val firstLaunch: Boolean) : HomeAction()
}
object GoToShop : HomeAction()
object ShowDialog : HomeAction() {
object ScanFails : HomeAction()
}
// internal
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()
object HideDialog : HomeAction()
}

View file

@ -1,80 +1,83 @@
package com.tangem.tap.features.home.redux
import android.content.Intent
import android.net.Uri
import androidx.core.content.ContextCompat.startActivity
import com.tangem.common.CompletionResult
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.post
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
class HomeMiddleware {
val homeMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is HomeAction.CheckIfFirstLaunch -> {
store.dispatch(
HomeAction.CheckIfFirstLaunch.Result(preferencesStorage.getCountOfLaunches() == 1)
)
}
is HomeAction.ReadCard -> {
scope.launch {
val result = tangemSdkManager.scanNote(FirebaseAnalyticsHandler)
withContext(Dispatchers.Main) {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
store.dispatch(GlobalAction.RestoreAppCurrency)
store.state.globalState.tapWalletManager.onCardScanned(result.data)
showDisclaimerOrNavigateToWallet()
}
}
}
}
}
is HomeAction.GoToShop -> {
val uri = Uri.parse(CARD_SHOP_URI)
val intent = Intent(Intent.ACTION_VIEW, uri)
startActivity(action.context, intent, null)
}
}
next(action)
}
}
}
private fun showDisclaimerOrNavigateToWallet() {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
return
}
if (store.state.walletState.twinCardsState != null) {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.TwinsOnboarding))
return
}
}
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
companion object {
private const val CARD_SHOP_URI =
val handler = homeMiddleware
const val CARD_SHOP_URI =
"https://shop.tangem.com/?afmc=1i&utm_campaign=1i&utm_source=leaddyno&utm_medium=affiliate"
}
}
private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.GetMoonPayUserStatus)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
}
is HomeAction.ShouldScanCardOnResume -> {
if (action.shouldScanCard) {
store.dispatch(HomeAction.ShouldScanCardOnResume(false))
postUi(700) { store.dispatch(HomeAction.ReadCard) }
}
}
is HomeAction.ReadCard -> handleReadCard()
is HomeAction.GoToShop -> store.dispatchOpenUrl(HomeMiddleware.CARD_SHOP_URI)
}
next(action)
}
}
}
private fun handleReadCard() {
if (!preferencesStorage.wasDisclaimerAccepted()) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
if (OnboardingHelper.isOnboardingCase(scanResponse)) {
val navigateTo = OnboardingHelper.whereToNavigate(scanResponse)
store.dispatch(GlobalAction.Onboarding.Start(scanResponse))
navigateTo(navigateTo, store.state.homeState.shareTransition)
} else {
scope.launch { store.state.globalState.tapWalletManager.onCardScanned(scanResponse) }
navigateTo(AppScreen.Wallet, null)
}
}, {
changeButtonState(ButtonState.ENABLED)
}))
}
}
private fun changeButtonState(state: ButtonState) {
val btnState = IndeterminateProgressButton(state)
store.dispatch(HomeAction.ChangeScanCardButtonState(btnState))
}
private fun navigateTo(screen: AppScreen, transition: FragmentShareTransition?) {
post(DELAY_SDK_DIALOG_CLOSE) {
changeButtonState(ButtonState.ENABLED)
store.dispatch(NavigationAction.NavigateTo(screen, transition))
}
}

View file

@ -13,18 +13,18 @@ private fun internalReduce(action: Action, state: AppState): HomeState {
if (action !is HomeAction) return state.homeState
var homeState = state.homeState
var state = state.homeState
when (action) {
is HomeAction.CheckIfFirstLaunch.Result -> {
homeState = homeState.copy(firstLaunch = action.firstLaunch)
is HomeAction.SetFragmentShareTransition -> {
state = state.copy(shareTransition = action.shareTransition)
}
is HomeAction.ShowDialog.ScanFails -> {
homeState = homeState.copy(dialog = HomeDialog.ScanFailsDialog)
is HomeAction.ShouldScanCardOnResume -> {
state = state.copy(shouldScanCardOnResume = action.shouldScanCard)
}
is HomeAction.HideDialog -> {
homeState = homeState.copy(dialog = null)
is HomeAction.ChangeScanCardButtonState -> {
state = state.copy(btnScanState = action.state)
}
}
return homeState
return state
}

View file

@ -1,13 +1,12 @@
package com.tangem.tap.features.home.redux
import com.tangem.tap.common.redux.global.StateDialog
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 firstLaunch: Boolean = true,
val dialog: StateDialog? = null
) : StateType
sealed class HomeDialog: StateDialog {
object ScanFailsDialog: HomeDialog()
}
val shouldScanCardOnResume: Boolean = false,
val shareTransition: FragmentShareTransition? = null,
val btnScanState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.ENABLED),
) : StateType

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.onboarding
import android.content.Context
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.redux.AppDialog
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.*
/**
[REDACTED_AUTHOR]
*/
class AddressInfoBottomSheetDialog(
private val stateDialog: AppDialog.AddressInfoDialog,
context: Context
) : BottomSheetDialog(context) {
init {
this.setContentView(R.layout.dialog_onboarding_address_info)
setOnCancelListener { store.dispatchDialogHide() }
}
override fun show() {
super.show()
val data = stateDialog.addressData
imv_close.setOnClickListener {
dismissWithAnimation = true
cancel()
}
imv_qr_code.setImageBitmap(data.qrCode)
tv_address.text = data.address
btn_fl_copy_address.setOnClickListener {
context.copyToClipboard(data.address)
stateDialog.onCopyAddress()
}
btn_fl_explore.setOnClickListener {
stateDialog.onExploreAddress()
}
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.tap.features.onboarding
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.ProductType
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.preferencesStorage
/**
[REDACTED_AUTHOR]
*/
class OnboardingHelper {
companion object {
fun isOnboardingCase(response: ScanResponse): Boolean = if (response.card.hasWallets())
preferencesStorage.usedCardsPrefStorage.activationIsStarted(response.card.cardId) else true
fun whereToNavigate(scanResponse: ScanResponse): AppScreen {
return when (scanResponse.productType) {
ProductType.Note -> AppScreen.OnboardingNote
ProductType.Wallet -> AppScreen.OnboardingWallet
ProductType.Twin -> AppScreen.OnboardingTwins
ProductType.Other -> AppScreen.OnboardingOther
}
}
}
}

View file

@ -0,0 +1,128 @@
package com.tangem.tap.features.onboarding
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.isPositive
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.UrlBitmapLoader
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.persistence.UsedCardsPrefStorage
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
/**
[REDACTED_AUTHOR]
*/
class OnboardingManager(
var scanResponse: ScanResponse,
val usedCardsPrefStorage: UsedCardsPrefStorage,
) {
var cardInfo: Result<CardVerifyAndGetInfo.Response.Item>? = null
private set
suspend fun loadArtwork(): Artwork {
cardInfo = OnlineCardVerifier().getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey)
return loadArtwork(scanResponse.card.getOrLoadCardArtworkUrl(cardInfo))
}
suspend fun loadArtwork(url: String): Artwork {
return withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
UrlBitmapLoader().loadBitmap(url) {
val result = when (it) {
is Result.Success -> Artwork(url, it.data)
is Result.Failure -> Artwork(url, null)
}
continuation.resume(result)
}
}
}
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
return when (val result = walletManager.safeUpdate()) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
if (valueOfAmount == null) {
val customError = TapError.CustomError("Amount is NULL")
OnboardingWalletBalance.criticalError(customError)
} else {
val currency = Currency.Blockchain(walletManager.wallet.blockchain)
val balance = if (valueOfAmount.isZero()) {
OnboardingWalletBalance.done(valueOfAmount, wallet.hasPendingTransactions(), currency)
} else {
OnboardingWalletBalance.done(valueOfAmount, false, currency)
}
balance
}
}
is Result.Failure -> {
val error = (result.error as? TapError) ?: TapError.UnknownError
when (error) {
is TapError.WalletManagerUpdate.NoAccountError -> OnboardingWalletBalance.error(error)
// NoInternetConnection, WalletManagerUpdate.InternalError
else -> OnboardingWalletBalance.criticalError(error)
}
}
}
}
fun activationStarted(cardId: String) {
usedCardsPrefStorage.activationStarted(cardId)
}
fun activationFinished(cardId: String) {
usedCardsPrefStorage.activationFinished(cardId)
}
}
data class OnboardingWalletBalance(
val value: BigDecimal = BigDecimal.ZERO,
val currency: Currency.Blockchain = Currency.Blockchain(Blockchain.Unknown),
val hasIncomingTransaction: Boolean = false,
val state: ProgressState,
val error: TapError? = null,
val criticalError: TapError? = null,
) {
fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction
val amountToCreateAccount: String?
get() = if (error is TapError.WalletManagerUpdate.NoAccountError) error.customMessage else null
companion object {
fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
error = error
)
fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance(
state = ProgressState.Error,
criticalError = error
)
fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance(
value,
state = ProgressState.Loading
)
fun done(value: BigDecimal, hasTransactions: Boolean, currency: Currency.Blockchain): OnboardingWalletBalance =
OnboardingWalletBalance(value, currency, hasTransactions, ProgressState.Done)
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.tap.features.onboarding.products
import android.os.Bundle
import android.view.View
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.transitions.HomeToOnboardingTransition
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.view_confetti.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseOnboardingFragment<T> : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber<T> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
configureTransitions()
}
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)
if (show) {
lav_confetti.playAnimation()
} else {
lav_confetti.cancelAnimation()
}
}
}

View file

@ -0,0 +1,184 @@
package com.tangem.tap.features.onboarding.products.note
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import android.widget.ImageView
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.tangem.blockchain.common.Blockchain
import com.tangem.tangem_sdk_new.extensions.fadeIn
import com.tangem.tangem_sdk_new.extensions.fadeOut
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.postUi
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.*
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_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
/**
[REDACTED_AUTHOR]
*/
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private var currentStep: OnboardingNoteStep = OnboardingNoteStep.None
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.onboarding_title)
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
val resources = AndroidResources(
OnboardingStringResources(
R.string.copy_toast_msg
)
)
store.dispatch(OnboardingNoteAction.LoadCardArtwork)
store.dispatch(OnboardingNoteAction.SetResources(resources))
store.dispatch(OnboardingNoteAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingNoteState == newState.onboardingNoteState
}.select { it.onboardingNoteState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingNoteState) {
if (activity == null) return
state.cardArtwork?.let { imv_front_card.swapToBitmapDrawable(it.artwork) }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
when (state.currentStep) {
OnboardingNoteStep.None -> setupNoneState(state)
OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state)
OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state)
OnboardingNoteStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
currentStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return
val balanceValue = state.walletBalance.value.stripZeroPlainString()
val currency = state.walletBalance.currency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
}
private fun setupNoneState(state: OnboardingNoteState) {
btn_main_action.isVisible = false
btn_alternative_action.isVisible = false
btnRefreshBalanceWidget.show(false)
tv_header.isVisible = false
tv_body.isVisible = false
btn_main_action.text = ""
btn_alternative_action.text = ""
tv_header.text = ""
tv_body.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 { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.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)
}
private fun setupTopUpWalletState(state: OnboardingNoteState) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
}
imv_card_background.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 {
showConfetti(false)
store.dispatch(OnboardingNoteAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.setText("")
btn_alternative_action.setOnClickListener { }
btnRefreshBalanceWidget.mainView.setOnClickListener(null)
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
imv_card_background.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.currentStep == 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)
}
}
fun ImageView.swapToBitmapDrawable(bitmap: Bitmap?, duration: Long = 200) {
if (drawable is BitmapDrawable && (drawable as BitmapDrawable).bitmap == bitmap) return
fadeOut(duration) {
setImageBitmap(bitmap)
fadeIn(duration)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingNoteAction : Action {
// from user, ui
data class SetResources(val resources: AndroidResources) : OnboardingNoteAction()
object LoadCardArtwork : OnboardingNoteAction()
object CreateWallet : OnboardingNoteAction()
object TopUp : OnboardingNoteAction()
object ShowAddressInfoDialog : OnboardingNoteAction()
// from redux
class SetArtworkUrl(val artwork: Artwork) : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()
data class SetStepOfScreen(val step: OnboardingNoteStep) : OnboardingNoteAction()
sealed class Balance {
object Update : OnboardingNoteAction()
data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction()
}
sealed class Confetti {
object Show : OnboardingNoteAction()
object Hide : OnboardingNoteAction()
}
}

View file

@ -0,0 +1,151 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.postUi
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.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
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.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class OnboardingNoteMiddleware {
companion object {
val handler = onboardingNoteMiddleware
}
}
private val onboardingNoteMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleNoteAction(action, dispatch)
next(action)
}
}
}
private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingNoteAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val scanResponse = onboardingManager.scanResponse
val card = onboardingManager.scanResponse.card
val noteState = store.state.onboardingNoteState
when (action) {
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artwork)) }
}
}
is OnboardingNoteAction.DetermineStepOfScreen -> {
val step = when {
!card.hasWallets() -> OnboardingNoteStep.CreateWallet
noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done
else -> OnboardingNoteStep.TopUpWallet
}
store.dispatch((OnboardingNoteAction.SetStepOfScreen(step)))
}
is OnboardingNoteAction.SetStepOfScreen -> {
when (action.step) {
OnboardingNoteStep.TopUpWallet -> {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
OnboardingNoteStep.Done -> {
onboardingManager.activationFinished(card.cardId)
postUi(500) { store.dispatch(OnboardingNoteAction.Confetti.Show) }
}
}
}
is OnboardingNoteAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = scanResponse.copy(card = result.data)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet))
}
is CompletionResult.Failure -> {
// do nothing
}
}
}
}
}
is OnboardingNoteAction.Balance.Update -> {
val walletManager = if (noteState.walletManager != null) {
noteState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(scanResponse).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(OnboardingNoteAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
state = ProgressState.Loading,
error = null,
criticalError = null
)
store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager.updateBalance(walletManager)
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
delay(if (isLoadedBefore) 0 else 300)
withMainContext { store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) }
}
}
is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
}
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
val addressWasCopied = noteState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {
val topUpUrl = noteState.walletManager?.getToUpUrl() ?: return
store.dispatchOpenUrl(topUpUrl)
}
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
class OnboardingNoteReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingNoteState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, appState: AppState): OnboardingNoteState {
var state = appState.onboardingNoteState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingNoteState()
}
is OnboardingNoteAction.SetArtworkUrl -> {
state = state.copy(cardArtwork = action.artwork)
}
is OnboardingNoteAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is OnboardingNoteAction.SetResources -> {
state = state.copy(resources = action.resources)
}
is OnboardingNoteAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingNoteAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingNoteAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
val resources: AndroidResources = AndroidResources(),
// UI
val cardArtwork: Artwork? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}
data class AndroidResources(
val strings: OnboardingStringResources = OnboardingStringResources()
)
data class OnboardingStringResources(
val addressWasCopied: Int = -1
)

View file

@ -0,0 +1,102 @@
package com.tangem.tap.features.onboarding.products.otherCards
import android.os.Bundle
import android.view.View
import android.view.animation.OvershootInterpolator
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.swapToBitmapDrawable
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsAction
import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState
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_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
/**
[REDACTED_AUTHOR]
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)
store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingOtherCardsState == newState.onboardingOtherCardsState
}.select { it.onboardingOtherCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingOtherCardsState) {
if (activity == null) return
if (state.currentStep == OnboardingOtherCardsStep.None) return
state.cardArtwork?.let { imv_front_card.swapToBitmapDrawable(it.artwork) }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
when (state.currentStep) {
OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState(state)
OnboardingOtherCardsStep.Done -> setupDoneState(state)
}
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 { }
tv_header.setText(R.string.onboarding_create_wallet_header)
tv_body.setText(R.string.onboarding_create_wallet_body)
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(R.layout.lp_onboarding_create_wallet)
}
private fun setupDoneState(state: OnboardingOtherCardsState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
showConfetti(false)
store.dispatch(OnboardingOtherCardsAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.setText("")
btn_alternative_action.setOnClickListener { }
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
imv_card_background.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)
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingOtherCardsAction : Action {
// from user, ui
object LoadCardArtwork : OnboardingOtherCardsAction()
object CreateWallet : OnboardingOtherCardsAction()
// from redux
class SetArtworkUrl(val artwork: Artwork) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()
data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction()
sealed class Confetti {
object Show : OnboardingOtherCardsAction()
object Hide : OnboardingOtherCardsAction()
}
}

View file

@ -0,0 +1,91 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
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.domain.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.extensions.hasWallets
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class OnboardingOtherCardsMiddleware {
companion object {
val handler = onboardingOtherCardsMiddleware
}
}
private val onboardingOtherCardsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleOtherCardsAction(action, dispatch)
next(action)
}
}
}
private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
if (action !is OnboardingOtherCardsAction) return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager ?: return
val card = onboardingManager.scanResponse.card
when (action) {
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artwork)) }
}
}
is OnboardingOtherCardsAction.DetermineStepOfScreen -> {
val step = when {
!card.hasWallets() -> OnboardingOtherCardsStep.CreateWallet
else -> OnboardingOtherCardsStep.Done
}
store.dispatch((OnboardingOtherCardsAction.SetStepOfScreen(step)))
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step == OnboardingOtherCardsStep.Done) {
onboardingManager.activationFinished(card.cardId)
postUi(500) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) }
}
}
is OnboardingOtherCardsAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createProductWallet(onboardingManager.scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
val updatedResponse = onboardingManager.scanResponse.copy(card = result.data)
onboardingManager.scanResponse = updatedResponse
onboardingManager.activationStarted(updatedResponse.card.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done))
}
is CompletionResult.Failure -> {
// do nothing
}
}
}
}
}
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(onboardingManager.scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import org.rekotlin.Action
class OnboardingOtherCardsReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingOtherCardsState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, appState: AppState): OnboardingOtherCardsState {
var state = appState.onboardingOtherCardsState
when (action) {
is GlobalAction.Onboarding.Start -> {
state = OnboardingOtherCardsState()
}
is OnboardingOtherCardsAction.SetArtworkUrl -> {
state = state.copy(cardArtwork = action.artwork)
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)
}
}
is OnboardingOtherCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is OnboardingOtherCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -0,0 +1,23 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingOtherCardsState(
// UI
val cardArtwork: Artwork? = null,
val showConfetti: Boolean = false,
val currentStep: OnboardingOtherCardsStep = OnboardingOtherCardsStep.None,
val steps: List<OnboardingOtherCardsStep> = OnboardingOtherCardsStep.values().toList(),
) : StateType {
val progress: Int
get() = steps.indexOf(currentStep)
}
enum class OnboardingOtherCardsStep {
None, CreateWallet, Done
}

View file

@ -0,0 +1,42 @@
package com.tangem.tap.features.onboarding.products.wallet
import android.os.Bundle
import android.view.View
import androidx.transition.TransitionInflater
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.store
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
class OnboardingWalletFragment : BaseStoreFragment(R.layout.fragment_onboarding_wallet), StoreSubscriber<OnboardingWalletState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.fade)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.onboardingWalletState == newState.onboardingWalletState
}.select { it.onboardingWalletState }
}
storeSubscribersList.add(this)
}
override fun newState(state: OnboardingWalletState) {
if (activity == null) return
}
}

View file

@ -0,0 +1,19 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
object TopUp : OnboardingWalletAction()
object Done : OnboardingWalletAction()
object ProceedBackup : OnboardingWalletAction()
sealed class Backup : Action {
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
class OnboardingWalletMiddleware {
companion object {
val handler = onboardingWalletMiddleware
}
}
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
next(action)
}
}
}

View file

@ -0,0 +1,20 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class OnboardingWalletReducer {
companion object {
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
var state = appState.onboardingWalletState
when (action) {
}
return state
}

View file

@ -0,0 +1,14 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class OnboardingWalletState(
val any: String? = null
) : StateType
enum class OnboardingWalletStep {
None, CreateWallet, TopUpWallet, Backup, Done
}

View file

@ -4,12 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
@ -27,18 +24,10 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
)
}
}
})
configureTransitions()
}
protected open fun configureTransitions() {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
@ -51,7 +40,7 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
toolbar?.setNavigationOnClickListener { activity?.onBackPressed() }
}
override fun onStart() {
@ -62,6 +51,7 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}

View file

@ -5,13 +5,13 @@ import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.ToastNotificationAction
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -137,7 +137,7 @@ sealed class SendActionUi : SendScreenActionUi {
sealed class SendAction : SendScreenAction {
data class ChangeSendButtonState(val state: SendButtonState) : SendAction()
data class ChangeSendButtonState(val state: ButtonState) : SendAction()
object SendSuccess : SendAction(), ToastNotificationAction {
override val messageResource: Int = R.string.send_transaction_success
}

View file

@ -21,9 +21,9 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.minimalAmount
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
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendButtonState
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.scope
@ -41,41 +41,43 @@ import java.util.*
/**
[REDACTED_AUTHOR]
*/
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
transactionData.destinationAddress, false
))
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
store.dispatchOnMain(AmountAction.SetAmount(transactionData.amount.toBigDecimal(),
false))
class SendMiddleware {
val sendMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is AddressPayIdActionUi -> AddressPayIdMiddleware().handle(action, appState(), dispatch)
is AmountActionUi -> AmountMiddleware().handle(action, appState(), dispatch)
is RequestFee -> RequestFeeMiddleware().handle(appState(), dispatch)
is SendActionUi.SendAmountToRecipient ->
verifyAndSendTransaction(action, appState(), dispatch)
is PrepareSendScreen -> setIfSendingToPayIdEnabled(appState(), dispatch)
is SendAction.Warnings.Update -> updateWarnings(dispatch)
is SendActionUi.CheckIfTransactionDataWasProvided -> {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
transactionData.destinationAddress, false
))
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
store.dispatchOnMain(AmountAction.SetAmount(transactionData.amount.toBigDecimal(),
false))
}
}
}
nextDispatch(action)
}
nextDispatch(action)
}
}
}
}
private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
val card = appState.globalState.scanNoteResponse?.card ?: return
val card = appState.globalState.scanResponse?.card ?: return
val destinationAddress = sendState.addressPayIdState.destinationWalletAddress ?: return
val typedAmount = sendState.amountState.amountToExtract ?: return
val feeAmount = sendState.feeState.currentFee ?: return
@ -119,7 +121,7 @@ private fun sendTransaction(
externalTransactionData: ExternalTransactionData?,
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(SendButtonState.PROGRESS))
dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS))
var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress)
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
@ -234,7 +236,7 @@ private fun sendTransaction(
}
}
}
dispatch(SendAction.ChangeSendButtonState(SendButtonState.ENABLED))
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.IdStateHolder
@ -41,7 +42,9 @@ class SendScreenReducer {
private class SendReducer : SendInternalReducer {
override fun handle(action: SendScreenAction, sendState: SendState): SendState {
val result = when (action) {
is SendAction.ChangeSendButtonState -> sendState.copy(sendButtonState = action.state)
is SendAction.ChangeSendButtonState -> {
sendState.copy(sendButtonState = IndeterminateProgressButton(action.state))
}
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)

View file

@ -5,8 +5,9 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.redux.global.StateDialog
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
@ -39,7 +40,7 @@ data class SendState(
val feeState: FeeState = FeeState(),
val receiptState: ReceiptState = ReceiptState(),
val sendWarningsList: List<WarningMessage> = listOf(),
val sendButtonState: SendButtonState = SendButtonState.DISABLED,
val sendButtonState: IndeterminateProgressButton = IndeterminateProgressButton(ButtonState.DISABLED),
val dialog: StateDialog? = null,
val externalTransactionData: ExternalTransactionData? = null
) : SendScreenState {
@ -87,7 +88,7 @@ data class SendState(
}
}
fun getButtonState(): SendButtonState = if (isReadyToSend()) SendButtonState.ENABLED else SendButtonState.DISABLED
fun getButtonState(): ButtonState = if (isReadyToSend()) ButtonState.ENABLED else ButtonState.DISABLED
fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal {
val needToExtractFee = amountState.isCoinAmount() && feeState.feeIsIncluded
@ -117,7 +118,7 @@ data class SendState(
}
}
enum class SendButtonState {
enum class ButtonState {
ENABLED, DISABLED, PROGRESS
}

View file

@ -7,6 +7,7 @@ import android.text.method.DigitsKeyListener
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.activity.OnBackPressedCallback
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.recyclerview.widget.LinearLayoutManager
@ -20,6 +21,7 @@ import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.common.extensions.setOnImeActionListener
import com.tangem.tap.common.qrCodeScan.ScanQrCodeActivity
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.snackBar.MaxAmountSnackbar
import com.tangem.tap.common.text.truncateMiddleWith
import com.tangem.tap.common.toggleWidget.*
@ -31,6 +33,7 @@ import com.tangem.tap.features.send.redux.FeeActionUi.*
import com.tangem.tap.features.send.redux.states.FeeType
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.ui.stateSubscribers.SendStateSubscriber
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.ui.adapters.SpacesItemDecoration
import com.tangem.tap.features.wallet.ui.adapters.WarningMessagesAdapter
import com.tangem.tap.mainScope
@ -60,6 +63,23 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private val sendSubscriber = SendStateSubscriber(this)
private lateinit var keyboardObserver: KeyboardObserver
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
val externalTransactionData = store.state.sendState.externalTransactionData
if (externalTransactionData == null) {
store.dispatch(NavigationAction.PopBackTo())
} else {
store.dispatch(
WalletAction.TradeCryptoAction.FinishSelling(externalTransactionData.transactionId)
)
}
}
})
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -10,7 +10,6 @@ import androidx.core.text.bold
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.common.toggleWidget.ProgressState
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.send.BaseStoreFragment
@ -116,20 +115,8 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
}
when (state.sendButtonState) {
SendButtonState.ENABLED -> {
fg.btnSend.isEnabled = true
sendFragment.sendBtn.changeState(ProgressState.None)
}
SendButtonState.DISABLED -> {
fg.btnSend.isEnabled = false
sendFragment.sendBtn.changeState(ProgressState.None)
}
SendButtonState.PROGRESS -> {
fg.btnSend.isEnabled = true
sendFragment.sendBtn.changeState(ProgressState.Progress())
}
}
sendFragment.sendBtn.changeState(state.sendButtonState.progressState)
sendFragment.sendBtn.mainView.isEnabled = state.sendButtonState.enabled
val rv = fg.rv_warning_messages
val adapter = rv.adapter as? WarningMessagesAdapter ?: return
@ -185,7 +172,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
val messageList = multiError.assembleErrors().map { getMessageString(context, it.first, it.second) }
multiError.builder(messageList)
}
else -> context.getString(state.error.localizedMessage)
else -> context.getString(state.error.messageResource)
}
fg.tilAmountToSend.enableError(true, message)
} else {

View file

@ -14,7 +14,7 @@ class TokensMiddleware {
{ action ->
when (action) {
is TokensAction.LoadCurrencies -> {
val card = state()?.globalState?.scanNoteResponse?.card
val card = state()?.globalState?.scanResponse?.card
val isTestcard = card?.isTestCard ?: false
val tokens = currenciesRepository.getPopularTokens(isTestcard)
val blockchains = currenciesRepository.getBlockchains(

View file

@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.models
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionStatus
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.toFormattedString
data class PendingTransaction(
@ -57,4 +58,12 @@ fun TransactionData.toPendingTransactionForToken(token: Token, walletAddress: St
fun List<TransactionData>.toPendingTransactionsForToken(token: Token, walletAddress: String): List<PendingTransaction> {
return this.mapNotNull { it.toPendingTransactionForToken(token, walletAddress) }
}
fun Wallet.getPendingTransactions(): List<PendingTransaction> {
return recentTransactions.toPendingTransactions(address)
}
fun Wallet.hasPendingTransactions(): Boolean {
return recentTransactions.toPendingTransactions(address).isNotEmpty()
}

View file

@ -122,7 +122,6 @@ sealed class WalletAction : Action {
object ShowDialog : WalletAction() {
object QrCode : WalletAction()
object ScanFails : WalletAction()
object SignedHashesMultiWalletDialog : WalletAction()
object ChooseTradeActionDialog : WalletAction()
}

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