Updated on 2026-08-14
This commit is contained in:
commit
f94e1a5c19
155 changed files with 5271 additions and 916 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -13,10 +13,7 @@ local.properties
|
|||
.gradle
|
||||
|
||||
# User-specific configurations
|
||||
.idea/caches/
|
||||
.idea/libraries/
|
||||
.idea/*.xml
|
||||
.idea/codeStyles
|
||||
.idea
|
||||
|
||||
# LayoutInspector files
|
||||
/captures
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
26
app/src/main/java/com/tangem/tap/common/Handler.kt
Normal file
26
app/src/main/java/com/tangem/tap/common/Handler.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
app/src/main/java/com/tangem/tap/common/extensions/Toast.kt
Normal file
31
app/src/main/java/com/tangem/tap/common/extensions/Toast.kt
Normal 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) }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
17
app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt
Normal file
17
app/src/main/java/com/tangem/tap/common/redux/StateDialog.kt
Normal 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()
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
8
app/src/main/java/com/tangem/tap/domain/ProductType.kt
Normal file
8
app/src/main/java/com/tangem/tap/domain/ProductType.kt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap.domain
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
enum class ProductType {
|
||||
Note, Wallet, Twin, Other
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
49
app/src/main/java/com/tangem/tap/domain/UrlBitmapLoader.kt
Normal file
49
app/src/main/java/com/tangem/tap/domain/UrlBitmapLoader.kt
Normal 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?) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
152
app/src/main/java/com/tangem/tap/domain/twins/TwinsCardWidget.kt
Normal file
152
app/src/main/java/com/tangem/tap/domain/twins/TwinsCardWidget.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue