Updated on 2026-08-14

This commit is contained in:
Tangem 2021-10-28 11:11:29 +03:00
commit 9dda19d990
108 changed files with 2712 additions and 2263 deletions

184
Gemfile.lock Normal file
View file

@ -0,0 +1,184 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.4)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.519.0)
aws-sdk-core (3.121.3)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.50.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.104.0)
aws-sdk-core (~> 3, >= 3.121.2)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
commander-fastlane (4.4.6)
highline (~> 1.7.2)
declarative (0.0.20)
declarative-option (0.1.0)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (1.0.1)
excon (0.87.0)
faraday (0.17.4)
multipart-post (>= 1.2, < 3)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.2.5)
fastlane (2.146.1)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.2, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander-fastlane (>= 4.4.6, < 5.0.0)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 2.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 0.17)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 0.13.1)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-api-client (>= 0.29.2, < 0.37.0)
google-cloud-storage (>= 1.15.0, < 2.0.0)
highline (>= 1.7.2, < 2.0.0)
json (< 3.0.0)
jwt (~> 2.1.0)
mini_magick (>= 4.9.4, < 5.0.0)
multi_xml (~> 0.5)
multipart-post (~> 2.0.0)
plist (>= 3.1.0, < 4.0.0)
public_suffix (~> 2.0.0)
rubyzip (>= 1.3.0, < 2.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
slack-notifier (>= 2.0.0, < 3.0.0)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
fastlane-plugin-firebase_app_distribution (0.3.1)
gh_inspector (1.1.3)
google-api-client (0.32.1)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.10.0)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.10)
google-cloud-core (1.3.2)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.21.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.26)
google-cloud-core (~> 1.2)
googleauth (>= 0.6.2, < 0.10.0)
mini_mime (~> 1.0)
googleauth (0.9.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
highline (1.7.10)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.4.0)
json (2.6.1)
jwt (2.1.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
os (1.1.1)
plist (3.6.0)
public_suffix (2.0.5)
rake (13.0.6)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
rubyzip (1.3.0)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
slack-notifier (2.4.0)
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unicode-display_width (1.8.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
universal-darwin-18
DEPENDENCIES
fastlane
fastlane-plugin-firebase_app_distribution
BUNDLED WITH
2.2.16

View file

@ -13,6 +13,8 @@ import com.tangem.tap.common.DialogManager
import com.tangem.tap.common.IntentHandler
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.redux.NotificationsHandler
import com.tangem.tap.common.redux.global.AndroidResources
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.TangemSdkManager
@ -66,9 +68,19 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)
tangemSdkManager = TangemSdkManager(tangemSdk, this)
store.dispatch(GlobalAction.SetResources(getAndroidResources()))
store.dispatch(WalletConnectAction.RestoreSessions)
}
private fun getAndroidResources(): AndroidResources {
return AndroidResources(
AndroidResources.RString(
R.string.copy_toast_msg,
R.string.details_notification_erase_wallet_not_possible
)
)
}
private fun systemActions() {
// makes the status bar text dark
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR

View file

@ -7,6 +7,8 @@ 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.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.CreateWalletInterruptDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -43,24 +45,25 @@ class DialogManager : StoreSubscriber<GlobalState> {
dialog = when (state.dialog) {
is AppDialog.ScanFailsDialog -> ScanFailsDialog.create(context)
is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context)
is TwinCardsAction.Wallet.ShowInterruptDialog -> CreateWalletInterruptDialog.create(state.dialog, context)
is WalletConnectDialog.UnsupportedCard ->
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_scanner_error_no_ethereum_wallet,
context = context
)
is WalletConnectDialog.OpeningSessionRejected -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_same_wcuri,
context = context
)
}
is WalletConnectDialog.SessionTimeout -> {
SimpleAlertDialog.create(
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
titleRes = R.string.wallet_connect,
messageRes = R.string.wallet_connect_error_timeout,
context = context
)
}
is WalletConnectDialog.ApproveWcSession ->

View file

@ -10,26 +10,24 @@ 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
import com.tangem.tap.features.details.ui.twins.CreateTwinWalletFragment
import com.tangem.tap.features.details.ui.twins.TwinWalletWarningFragment
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.twins.ui.TwinsCardsFragment
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.wallet.R
fun FragmentActivity.openFragment(
screen: AppScreen,
addToBackstack: Boolean,
fgShareTransition: FragmentShareTransition? = null
screen: AppScreen,
addToBackstack: Boolean,
fgShareTransition: FragmentShareTransition? = null
) {
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = fragmentFactory(screen)
@ -39,7 +37,7 @@ fun FragmentActivity.openFragment(
transaction.setReorderingAllowed(true)
shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.name)
transaction.addSharedElement(view, shareElement.elementName)
}
}
}
@ -60,8 +58,8 @@ fun FragmentActivity.getPreviousScreen(): AppScreen? {
}
fun FragmentActivity.addOnBackPressedDispatcher(
isEnabled: Boolean = true,
onBackPressed: VoidCallback
isEnabled: Boolean = true,
onBackPressed: VoidCallback
): OnBackPressedCallback = (object : OnBackPressedCallback(isEnabled) {
override fun handleOnBackPressed() {
onBackPressed()
@ -70,10 +68,11 @@ fun FragmentActivity.addOnBackPressedDispatcher(
private fun fragmentFactory(screen: AppScreen): Fragment {
return when (screen) {
// AppScreen.Home -> TestLeapfrogFragment()
AppScreen.Home -> HomeFragment()
AppScreen.OnboardingNote -> OnboardingNoteFragment()
AppScreen.OnboardingWallet -> OnboardingWalletFragment()
AppScreen.OnboardingTwins -> OnboardingTwinsFragment()
AppScreen.OnboardingTwins -> TwinsCardsFragment()
AppScreen.OnboardingOther -> OnboardingOtherCardsFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
@ -81,8 +80,6 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
AppScreen.DetailsSecurity -> DetailsSecurityFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.CreateTwinWalletWarning -> TwinWalletWarningFragment()
AppScreen.CreateTwinWallet -> CreateTwinWalletFragment()
AppScreen.AddTokens -> AddTokensFragment()
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectSessionsFragment()

View file

@ -4,6 +4,7 @@ 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.domain.tasks.product.ScanResponse
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
@ -17,6 +18,10 @@ fun Store<*>.dispatchOnMain(action: Action) {
}
}
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}

View file

@ -6,6 +6,7 @@ import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import androidx.core.animation.doOnEnd
import androidx.core.view.children
import com.tangem.common.extensions.VoidCallback
import com.tangem.tangem_sdk_new.extensions.dpToPx
/**
@ -15,9 +16,9 @@ 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 foldAnimationDuration = 300L
private val leapAnimationDuration = 500L
private val leapBackAnimationDuration = 500L
private val leapViews = mutableListOf<LeapView>()
@ -25,9 +26,6 @@ class LeapfrogWidget(
private var leapProgress: Int = 100
private var leapBackProgress: Int = 100
var isFolded: Boolean = true
private set
init {
calculator.setTranslationConverter { parentContainer.dpToPx(it) }
if (viewIsFullFledged()) {
@ -43,12 +41,11 @@ class LeapfrogWidget(
* Apply initial properties to the views
*/
fun initViews() {
isFolded = true
leapViews.forEach { it.initView() }
}
fun fold(onEnd: () -> Unit = {}) {
val animator = foldAnimator()
fun fold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = foldAnimator(animate)
if (animator == null) {
onEnd()
} else {
@ -57,8 +54,8 @@ class LeapfrogWidget(
}
}
fun unfold(onEnd: () -> Unit = {}) {
val animator = unfoldAnimator()
fun unfold(animate: Boolean = true, onEnd: VoidCallback = {}) {
val animator = unfoldAnimator(animate)
if (animator == null) {
onEnd()
} else {
@ -67,33 +64,31 @@ class LeapfrogWidget(
}
}
fun foldAnimator(): Animator? {
if (!canFoldUnfold() || isFolded) return null
fun foldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(true, foldAnimationDuration)
return createFoldUnfoldAnimator(true, animate, foldAnimationDuration)
}
fun unfoldAnimator(): Animator? {
if (!canFoldUnfold() || !isFolded) return null
fun unfoldAnimator(animate: Boolean = true): Animator? {
if (!canFoldUnfold()) return null
return createFoldUnfoldAnimator(false, foldAnimationDuration)
return createFoldUnfoldAnimator(false, animate, foldAnimationDuration)
}
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, duration: Long): Animator {
private fun createFoldUnfoldAnimator(isFoldAnimation: Boolean, animate: Boolean, duration: Long): Animator {
val animatorsList = mutableListOf<Animator>()
val animateDuration = if (animate) duration else 0
leapViews.forEach {
if (isFoldAnimation) {
it.fold()
animatorsList.add(it.view.foldAnimation(duration, it.state.properties))
animatorsList.add(it.view.foldAnimation(animateDuration, it.state.properties))
} else {
it.unfold()
animatorsList.add(it.view.unfoldAnimation(duration, it.state.properties))
animatorsList.add(it.view.unfoldAnimation(animateDuration, it.state.properties))
}
}
animatorsList.add(createProgressListener(duration) {
foldUnfoldProgress = it
if (it == 100) isFolded = isFoldAnimation
})
animatorsList.add(createProgressListener(animateDuration) { foldUnfoldProgress = it })
val animator = AnimatorSet()
animator.interpolator = AccelerateDecelerateInterpolator()
@ -101,13 +96,13 @@ class LeapfrogWidget(
return animator
}
fun leap(onEnd: () -> Unit = {}) {
fun leap(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeap()) {
onEnd()
return
}
val duration = leapAnimationDuration
val duration = if (animate) leapAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leap()) {
@ -128,13 +123,13 @@ class LeapfrogWidget(
animator.start()
}
fun leapBack(onEnd: () -> Unit = {}) {
fun leapBack(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (!canLeapBack()) {
onEnd()
return
}
val duration = leapBackAnimationDuration
val duration = if (animate) leapBackAnimationDuration else 0
val animatorsList = mutableListOf<Animator>()
leapViews.forEach {
when (it.leapBack()) {
@ -160,12 +155,11 @@ class LeapfrogWidget(
fun getViewByPosition(position: Int): LeapView = leapViews.first { it.state.currentPosition == position }
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(isFolded, leapViews.map { it.state })
fun getState(): LeapfrogWidgetState = LeapfrogWidgetState(leapViews.map { it.state })
fun applyState(state: LeapfrogWidgetState) {
isFolded = state.isFolded
state.leapViewStates.forEach { state ->
leapViews.firstOrNull { it.index == state.index }?.applyState(state)
state.leapViewStates.forEach { viewState ->
leapViews.firstOrNull { it.index == viewState.index }?.applyState(viewState)
}
}
@ -183,7 +177,6 @@ class LeapfrogWidget(
private fun canLeap(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapBackInProgress() -> false
leapProgress < 70 -> false
else -> true
@ -193,7 +186,6 @@ class LeapfrogWidget(
private fun canLeapBack(): Boolean {
return when {
!viewIsFullFledged() -> false
isFolded -> false
leapInProgress() -> false
leapBackProgress < 70 -> false
else -> true
@ -206,14 +198,13 @@ class LeapfrogWidget(
}
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 decreaseScaleFactor: Float = 0.15f,
val yTranslationFactor: Float = 35f,
val decreaseOverLiftingFactor: Float = 0.85f,
) {
// 0 view it is view on the top of stack, but it is last in parent.children

View file

@ -8,6 +8,7 @@ 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.twins.redux.TwinCardsReducer
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.tokens.redux.TokensReducer
@ -19,18 +20,19 @@ fun appReducer(action: Action, state: AppState?): AppState {
if (action is AppAction.RestoreState) return action.state
return 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),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
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),
twinCardsState = TwinCardsReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state),
disclaimerState = DisclaimerReducer.reduce(action, state),
tokensState = TokensReducer.reduce(action, state),
walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState)
)
}

View file

@ -16,6 +16,8 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMidd
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.twins.redux.TwinCardsMiddleware
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
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
@ -35,6 +37,7 @@ data class AppState(
val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(),
val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(),
val walletState: WalletState = WalletState(),
val twinCardsState: TwinCardsState = TwinCardsState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState(),
val disclaimerState: DisclaimerState = DisclaimerState(),
@ -45,18 +48,19 @@ data class AppState(
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
logMiddleware, navigationMiddleware, notificationsMiddleware,
GlobalMiddleware.handler,
HomeMiddleware.handler,
OnboardingNoteMiddleware.handler,
OnboardingWalletMiddleware.handler,
OnboardingOtherCardsMiddleware.handler,
WalletMiddleware().walletMiddleware,
TwinCardsMiddleware.handler,
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware
)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.features.wallet.redux.AddressData
/**
@ -11,7 +12,7 @@ sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val addressData: AddressData,
val onCopyAddress: () -> Unit,
val onExploreAddress: () -> Unit
val onCopyAddress: VoidCallback,
val onExploreAddress: VoidCallback
) : AppDialog()
}

View file

@ -23,6 +23,9 @@ sealed class GlobalAction : Action {
data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction
data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction
// android resources
data class SetResources(val resources: AndroidResources) : GlobalAction()
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
object HideDialog : GlobalAction()

View file

@ -47,7 +47,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
preferencesStorage.getAppCurrency()
))
}
is GlobalAction.HideWarningMessage -> {
@ -81,7 +81,7 @@ private val globalMiddlewareHandler: 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)
)
}
}
@ -94,7 +94,7 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
withMainContext {
when (result) {
is CompletionResult.Success -> {
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data.card)
tangemSdkManager.changeDisplayedCardIdNumbersCount(result.data)
action.onSuccess?.invoke(result.data)
}
is CompletionResult.Failure -> action.onFailure?.invoke(result.error)

View file

@ -12,6 +12,9 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
val globalState = state.globalState
return when (action) {
is GlobalAction.SetResources -> {
globalState.copy(resources = action.resources)
}
is GlobalAction.Onboarding.Start -> {
val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage
val onboardingState = OnboardingManager(action.scanResponse, usedCardsPrefStorage)

View file

@ -27,7 +27,17 @@ data class GlobalState(
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
val resources: AndroidResources = AndroidResources(),
) : StateType
data class AndroidResources(
val strings: RString = RString()
) {
data class RString(
val addressWasCopied: Int = -1,
val walletIsNotEmpty: Int = -1
)
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String

View file

@ -0,0 +1,35 @@
package com.tangem.tap.common.redux.navigation
import android.view.View
import androidx.transition.TransitionSet
import java.lang.ref.WeakReference
/**
[REDACTED_AUTHOR]
*/
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
/**
* For ease of use, the name is used as transitionName\name into the FragmentTransaction.addSharedElement
*/
class ShareElement(view: View, name: String? = null) {
val wView: WeakReference<View>
val elementName: String
init {
name?.let { view.transitionName = it }
elementName = view.transitionName
?: throw UnsupportedOperationException("ShareElement require the name")
wView = WeakReference(view)
}
companion object {
val imvFrontCard = "imv_front_card"
val imvBackCard = "imv_back_card"
}
}

View file

@ -1,8 +1,6 @@
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
@ -19,12 +17,4 @@ sealed class NavigationAction : Action {
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}
data class FragmentShareTransition(
val shareElements: List<ShareElement>,
val enterTransitionSet: TransitionSet,
val exitTransitionSet: TransitionSet
)
data class ShareElement(val wView: WeakReference<View>, val name: String)
}

View file

@ -20,8 +20,7 @@ private fun internalReduce(action: Action, state: AppState): NavigationState {
navState.copy(backStack = navState.backStack + navigationAction.screen)
}
is NavigationAction.PopBackTo -> {
val screen =
navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val screen = navigationAction.screen ?: navState.activity?.get()?.getPreviousScreen()
val index = navState.backStack.lastIndexOf(screen) + 1
state.navigationState.copy(backStack = navState.backStack.subList(0, index))
}

View file

@ -5,8 +5,8 @@ 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 {
@ -16,7 +16,6 @@ enum class AppScreen {
Wallet, WalletDetails,
Send,
Details, DetailsConfirm, DetailsSecurity,
CreateTwinWalletWarning, CreateTwinWallet,
AddTokens,
WalletConnectSessions,
QrScan

View file

@ -41,30 +41,31 @@ class RefreshBalanceWidget(
val progressState = state as? ProgressState ?: return
val currentStateByView = getState()
if (progressState == currentStateByView) return
when {
currentStateByView == progressState -> return
currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return
}
animateState(progressState)
viewSwitcher.showNext()
}
private fun animateState(state: ProgressState) {
when (state) {
ProgressState.Done, ProgressState.Error -> {
progressViewAnimation?.cancel()
progressViewAnimation = null
}
ProgressState.Loading -> {
val animation = RotateAnimation(
0f, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
progressViewAnimation = 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
progressViewAnimation?.duration = 700
progressViewAnimation?.interpolator = AccelerateInterpolator()
progressViewAnimation?.repeatCount = -1
progressViewAnimation?.let { arrowView.startAnimation(it) }
}
}
}
@ -88,10 +89,10 @@ class RefreshBalanceWidget(
class ShowAnimation : AnimationSet(true) {
init {
addAnimation(ScaleAnimation(
0f, 1f,
0f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f)
0f, 1f,
0f, 1f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f)
)
addAnimation(AlphaAnimation(0f, 1f))
duration = 300

View file

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

View file

@ -23,7 +23,6 @@ import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
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
@ -45,9 +44,9 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
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))
CreateProductWalletAndRescanTask(scanResponse.productType),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body))
)
}
@ -105,13 +104,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
): CompletionResult<T> =
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
withContext(Dispatchers.Main) {
suspendCoroutine { continuation ->
tangemSdk.startSessionWithRunnable(runnable, cardId, initialMessage) { result ->
continuation.resume(result)
}
}
}
}
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null,
@ -120,8 +119,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
return withContext(Dispatchers.Main) { result }
}
fun changeDisplayedCardIdNumbersCount(card: Card) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (card.isTangemTwin()) 4 else null
fun changeDisplayedCardIdNumbersCount(scanResponse: ScanResponse) {
tangemSdk.config.cardIdDisplayedNumbersCount = if (scanResponse.isTangemTwins()) 4 else null
}
companion object {

View file

@ -16,8 +16,6 @@ import com.tangem.tap.domain.configurable.config.ConfigManager
import com.tangem.tap.domain.extensions.*
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.tokens.CardCurrencies
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
@ -59,8 +57,8 @@ class TapWalletManager {
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
Timber.d(wallet.getTokens().toString())
val currencies = wallet.getTokens()
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
loadFiatRate(fiatCurrency, currencies)
}
@ -89,11 +87,6 @@ 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.isTangemTwin()) {
data.card.getTwinCardNumber()?.let {
store.dispatch(WalletAction.TwinsAction.SetTwinCard(it, true))
}
}
val blockchain = data.getBlockchain()
if (blockchain == Blockchain.Ethereum ||
@ -122,56 +115,47 @@ class TapWalletManager {
suspend fun loadData(data: ScanResponse) {
withContext(Dispatchers.Main) {
store.dispatch(WalletAction.LoadCardInfo(data.card))
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
when {
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
data.card.wallets.isEmpty() ||
(data.card.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
else -> {
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
if (blockchain != Blockchain.Unknown && primaryWalletManager != null) {
val primaryToken = data.getPrimaryToken()
store.dispatch(WalletAction.MultiWallet.SetPrimaryBlockchain(blockchain))
if (primaryToken != null) {
primaryWalletManager.addToken(primaryToken)
store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken))
}
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, primaryWalletManager)
} else {
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain)))
}
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, null)
}
}
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
} else {
if (data.card.isMultiwalletAllowed) {
loadMultiWalletData(data.card, blockchain, null)
}
}
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
}
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens ?: emptySet()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
@ -179,9 +163,9 @@ class TapWalletManager {
if (savedCurrencies == null) {
if (primaryBlockchain != null && primaryWalletManager != null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = setOf(primaryBlockchain), tokens = primaryTokens
)))
CardCurrencies(
blockchains = setOf(primaryBlockchain), tokens = primaryTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(primaryTokens.toList()))
@ -191,7 +175,7 @@ class TapWalletManager {
CardCurrencies(blockchains = blockchains, tokens = emptySet())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList()))
}
@ -200,12 +184,12 @@ class TapWalletManager {
} else {
val blockchains = savedCurrencies.blockchains.toList()
val walletManagers = if (
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
primaryTokens.isNotEmpty() &&
primaryWalletManager != null && primaryBlockchain != null
) {
val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain }
walletManagerFactory.makeWalletManagersForApp(card, blockchainsWithoutPrimary)
.plus(primaryWalletManager)
.plus(primaryWalletManager)
} else {
walletManagerFactory.makeWalletManagersForApp(card, blockchains)
}
@ -218,28 +202,38 @@ class TapWalletManager {
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.isTangemTwin() && data.secondTwinPublicKey == null) -> {
store.dispatch(WalletAction.EmptyWallet)
}
else -> {
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
getActionIfUnknownBlockchainOrEmptyWallet(data)?.let {
store.dispatch(it)
return@withContext
}
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) {
store.dispatch(WalletAction.LoadData.Failure(TapError.NoInternetConnection))
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
store.dispatch(WalletAction.LoadFiatRate())
}
}
private fun getActionIfUnknownBlockchainOrEmptyWallet(data: ScanResponse): WalletAction? {
return when {
// check order is important
data.isTangemTwins() && !data.twinsIsTwinned() -> {
WalletAction.EmptyWallet
}
data.getBlockchain() == Blockchain.Unknown && !data.card.isMultiwalletAllowed -> {
WalletAction.LoadData.Failure(TapError.UnknownBlockchain)
}
data.card.wallets.isEmpty() -> {
WalletAction.EmptyWallet
}
else -> null
}
}

View file

@ -28,10 +28,13 @@ object TapWorkarounds {
return excludedBatch || excludedIssuerName
}
fun Card.isTangemNote(): Boolean = tangemNoteBatches.contains(batchId)
fun Card.isTangemWallet(): Boolean = tangemWalletBatches.contains(batchId)
@Deprecated("Use ScanResponse.isTangemNote")
fun isTangemNote(card: Card): Boolean = tangemNoteBatches.contains(card.batchId)
fun Card.getTangemNoteBlockchain(): Blockchain? = tangemNoteBatches[batchId]
@Deprecated("Use ScanResponse.isTangemWallet")
fun isTangemWallet(card: Card): Boolean = tangemWalletBatches.contains(card.batchId)
fun getTangemNoteBlockchain(card: Card): Blockchain? = tangemNoteBatches[card.batchId]
private const val START_2_COIN_ISSUER = "start2coin"
private const val TEST_CARD_BATCH = "99FF"
@ -55,7 +58,7 @@ object TapWorkarounds {
"AB02" to Blockchain.Ethereum,
"AB03" to Blockchain.CardanoShelley,
"AB04" to Blockchain.Dogecoin,
"AB05" to Blockchain.Binance,
"AB05" to Blockchain.BSC,
"AB06" to Blockchain.XRP,
)
}
@ -64,7 +67,7 @@ val DELAY_SDK_DIALOG_CLOSE = 1400L
val Card.isMultiwalletAllowed: Boolean
get() {
return !isTangemTwin() && !isStart2Coin && !isTangemNote()
return !isTangemTwin() && !isStart2Coin && !isTangemNote(this)
&& (firmwareVersion >= FirmwareVersion.MultiWalletAvailable ||
getSingleWallet()?.curve == EllipticCurve.Secp256k1)
}

View file

@ -14,6 +14,7 @@ data class Config(
val blockchainSdkConfig: BlockchainSdkConfig = BlockchainSdkConfig(),
val isSendingToPayIdEnabled: Boolean = true,
val isTopUpEnabled: Boolean = false,
@Deprecated("Not relevant since version 3.23")
val isCreatingTwinCardsAllowed: Boolean = false
)
@ -50,10 +51,10 @@ class ConfigManager(
fun resetToDefault(name: String) {
when (name) {
isSendingToPayIdEnabled -> config =
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
config.copy(isSendingToPayIdEnabled = defaultConfig.isSendingToPayIdEnabled)
isTopUpEnabled -> config = config.copy(isTopUpEnabled = defaultConfig.isTopUpEnabled)
isCreatingTwinCardsAllowed -> config =
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
config.copy(isCreatingTwinCardsAllowed = defaultConfig.isCreatingTwinCardsAllowed)
}
}

View file

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

View file

@ -9,7 +9,6 @@ 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.product.ScanResponse
import com.tangem.tap.domain.twins.isTangemTwin
fun WalletManagerFactory.makeWalletManagerForApp(
card: Card,
@ -65,7 +64,7 @@ fun WalletManagerFactory.makePrimaryWalletManager(
val publicKey = wallet?.publicKey ?: return null
val curveToUse = wallet.curve
return if (card.isTangemTwin() && data.secondTwinPublicKey != null) {
return if (data.isTangemTwins() && data.secondTwinPublicKey != null) {
makeMultisigWalletManager(
cardId = card.cardId,
walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(),

View file

@ -1,135 +0,0 @@
package com.tangem.tap.domain.tasks
import com.tangem.*
import com.tangem.blockchain.common.BlockchainSdkConfig
import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
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.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.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.isTangemTwin
import com.tangem.tap.store
class ScanNoteTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
override fun run(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Success -> {
val card = this.card ?: result.data
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
if (card.isTangemTwin()) {
dealWithTwinCard(card, session, callback)
} else if (!card.isTangemNote() && card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable) {
createMissingWalletsIfNeeded(card, session, callback)
} else {
callback(CompletionResult.Success(
ScanResponse(card, ProductType.Other, session.environment.walletData)))
}
}
}
}
}
private fun createMissingWalletsIfNeeded(
card: Card, session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val walletData = session.environment.walletData
if (card.wallets.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
val curvesPresent = card.wallets.map { it.curve }
// val curvesToCreate = EllipticCurve.values().subtract(curvesPresent)
val curvesToCreate = card.supportedCurves.subtract(curvesPresent)
if (curvesToCreate.isEmpty()) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, walletData)))
return
}
CreateWalletsTask(curvesToCreate.toList()).run(session) { result ->
when (result) {
is CompletionResult.Success ->
callback(CompletionResult.Success(ScanResponse(result.data, ProductType.Other, walletData)))
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun dealWithTwinCard(
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.Other, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(
readDataResult.data.issuerData, publicKey
)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
callback(CompletionResult.Success(
ScanResponse(
card = card,
ProductType.Other,
walletData = session.environment.walletData,
secondTwinPublicKey = twinPublicKey.toHexString())
))
return@run
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Other, null)))
}
}
}
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
// Disable new multi-currency HD wallet cards on the old version of the app
// if (card.isMultiCurrencyWallet()) return UpdateAppToUseThisCard()
return null
}
private fun getWalletManagerFactory(): WalletManagerFactory {
val blockchainSdkConfig = store.state.globalState.configManager?.config
?.blockchainSdkConfig ?: BlockchainSdkConfig()
return WalletManagerFactory(blockchainSdkConfig)
}
}

View file

@ -45,7 +45,7 @@ private class CreateProductWalletTask(
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.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
ProductType.Wallet -> CreateWalletTangemWallet()
else -> CreateWalletOtherCards()
}
@ -64,7 +64,7 @@ private class CreateWalletTangemNote : ProductCommandProcessor<CreateWalletRespo
return
}
val curvesSupportedByBlockchain = card.getTangemNoteBlockchain()?.getSupportedCurves()
val curvesSupportedByBlockchain = getTangemNoteBlockchain(card)?.getSupportedCurves()
if (curvesSupportedByBlockchain == null || curvesSupportedByBlockchain.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return

View file

@ -19,13 +19,11 @@ 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
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
import com.tangem.tap.domain.twins.TwinsHelper
data class ScanResponse(
val card: Card,
@ -35,7 +33,7 @@ data class ScanResponse(
) : CommandResponse {
fun getBlockchain(): Blockchain {
if (productType == ProductType.Note) return card.getTangemNoteBlockchain() ?: return Blockchain.Unknown
if (productType == ProductType.Note) return getTangemNoteBlockchain(card) ?: return Blockchain.Unknown
val blockchainName: String = walletData?.blockchain ?: return Blockchain.Unknown
return Blockchain.fromId(blockchainName)
}
@ -43,13 +41,20 @@ data class ScanResponse(
fun getPrimaryToken(): Token? {
val cardToken = walletData?.token ?: return null
return Token(
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
Blockchain.fromId(walletData.blockchain)
cardToken.name,
cardToken.symbol,
cardToken.contractAddress,
cardToken.decimals,
Blockchain.fromId(walletData.blockchain)
)
}
fun isTangemNote(): Boolean = productType == ProductType.Note
fun isTangemWallet(): Boolean = productType == ProductType.Wallet
fun isTangemTwins(): Boolean = productType == ProductType.Twins
fun isTangemOtherCards(): Boolean = productType == ProductType.Other
fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null
}
class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse> {
@ -70,9 +75,9 @@ class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse
}
val commandProcessor = when {
card.isTangemNote() -> ScanNoteProcessor()
card.isTangemTwin() -> ScanTwinProcessor()
card.isTangemWallet() -> ScanWalletProcessor()
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session, callback)
@ -89,6 +94,8 @@ class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse
}
}
private fun Card.isTangemTwins(): Boolean = TwinsHelper.getTwinCardNumber(cardId) != null
private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
override fun proceed(
card: Card,
@ -120,21 +127,21 @@ private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
return@run
}
val verified = TwinCardsManager.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
val verified = TwinsHelper.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())
val response = ScanResponse(card, ProductType.Twins, walletData, twinPublicKey.toHexString())
callback(CompletionResult.Success(response))
} else {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
}
}
is CompletionResult.Failure ->
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twin, null)))
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
}
}
}

View file

@ -1,63 +1,52 @@
package com.tangem.tap.domain.twins
import android.content.Context
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
import com.tangem.Message
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.crypto.CryptoUtils
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.readAssetAsString
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.network.createMoshi
import com.tangem.tap.tangemSdkManager
class TwinCardsManager(private val scanResponse: ScanResponse, context: Context) {
class TwinCardsManager(private val card: Card, assetReader: AssetReader) {
private val currentCardId: String = scanResponse.card.cardId
private val currentCardId: String = card.cardId
private var currentCardPublicKey: String? = null
private var secondCardPublicKey: String? = null
private val issuerKeyPair: KeyPair = getIssuerKeys(
context, scanResponse.card.issuer.publicKey.toHexString()
)
private val issuerKeyPair: KeyPair = getIssuerKeys(assetReader, card.issuer.publicKey.toHexString())
suspend fun createFirstWallet(message: Message): SimpleResult {
val response = tangemSdkManager.runTaskAsync(
CreateFirstTwinWalletTask(), currentCardId, message
)
suspend fun createFirstWallet(message: Message): CompletionResult<CreateWalletResponse> {
val response = tangemSdkManager.runTaskAsync(CreateFirstTwinWalletTask(), currentCardId, message)
when (response) {
is CompletionResult.Success -> {
currentCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Success -> currentCardPublicKey = response.data.wallet.publicKey.toHexString()
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
card = card
)
}
return SimpleResult.failure(response.error)
}
}
return response
}
suspend fun createSecondWallet(
initialMessage: Message,
preparingMessage: Message,
creatingWalletMessage: Message,
): SimpleResult {
): CompletionResult<CreateWalletResponse> {
val task = CreateSecondTwinWalletTask(
firstPublicKey = currentCardPublicKey!!,
firstCardId = currentCardId,
@ -69,20 +58,18 @@ class TwinCardsManager(private val scanResponse: ScanResponse, context: Context)
when (response) {
is CompletionResult.Success -> {
secondCardPublicKey = response.data.wallet.publicKey.toHexString()
return SimpleResult.Success
}
is CompletionResult.Failure -> {
(response.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = scanResponse.card
card = card
)
}
return SimpleResult.failure(response.error)
}
}
return response
}
suspend fun complete(message: Message): Result<ScanResponse> {
@ -97,7 +84,7 @@ class TwinCardsManager(private val scanResponse: ScanResponse, context: Context)
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.WriteIssuerData,
card = scanResponse.card
card = card
)
}
Result.failure(response.error)
@ -106,16 +93,8 @@ class TwinCardsManager(private val scanResponse: ScanResponse, context: Context)
}
companion object {
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
private fun getIssuerKeys(context: Context, publicKey: String): KeyPair {
val issuer = getIssuers(context).first { it.publicKey == publicKey }
private fun getIssuerKeys(reader: AssetReader, publicKey: String): KeyPair {
val issuer = getIssuers(reader).first { it.publicKey == publicKey }
return KeyPair(
publicKey = issuer.publicKey.hexToBytes(),
privateKey = issuer.privateKey.hexToBytes()
@ -128,13 +107,17 @@ class TwinCardsManager(private val scanResponse: ScanResponse, context: Context)
)
}
private fun getIssuers(context: Context): List<Issuer> {
val file = context.readAssetAsString("tangem-app-config/issuers")
private fun getIssuers(reader: AssetReader): List<Issuer> {
val file = reader.readAssetAsString("tangem-app-config/issuers")
return getAdapter().fromJson(file)!!
}
}
}
interface AssetReader {
fun readAssetAsString(name: String): String
}
private class Issuer(
val id: String,
val privateKey: String,

View file

@ -5,6 +5,7 @@ import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.view.View
import androidx.core.animation.doOnEnd
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.leapfrogWidget.LeapView
import com.tangem.tap.common.leapfrogWidget.LeapViewState
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
@ -17,47 +18,60 @@ class TwinsCardWidget(
val getTopOfAnchorViewForActivateState: () -> Float
) {
var state: TwinCardsWidgetState? = null
private set
private val animationDuration = 300L
init {
if (leapfrogWidget.getViewsCount() != 2) throw UnsupportedOperationException()
}
fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toWelcome(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Welcome) return
state = TwinCardsWidgetState.Welcome
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createWelcomeProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createWelcomeProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold { animator.start() }
animator.doOnEnd { onEnd() }
leapfrogWidget.fold(animate) { animator.start() }
}
fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toLeapfrog(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Leapfrog) return
state = TwinCardsWidgetState.Leapfrog
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createLeapfrogProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createLeapfrogProperties(TwinCardNumber.Second))
)
leapfrogWidget.fold {
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold()
}
animator.start()
animator.doOnEnd {
leapfrogWidget.initViews()
leapfrogWidget.unfold(animate, onEnd)
}
animator.start()
}
fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) {
val animator = createAnimator(animate, onEnd)
fun toActivate(animate: Boolean = true, onEnd: VoidCallback = {}) {
if (state == TwinCardsWidgetState.Activate) return
state = TwinCardsWidgetState.Activate
val animator = createAnimator(animate)
animator.playTogether(
createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)),
createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second))
)
animator.doOnEnd { onEnd() }
animator.start()
}
private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet {
private fun createAnimator(animate: Boolean): AnimatorSet {
return AnimatorSet().apply {
duration = if (animate) 400 else 0
doOnEnd { onEnd() }
duration = if (animate) animationDuration else 0
}
}
@ -72,20 +86,20 @@ class TwinsCardWidget(
return when (cardNumber) {
TwinCardNumber.First -> {
TwinsCardProperties(
xTranslation = -180f,
yTranslation = -350f,
xTranslation = -120f,
yTranslation = -220f,
rotation = -3f,
elevation = 1f,
scale = 1.1f,
scale = 1f,
)
}
TwinCardNumber.Second -> {
TwinsCardProperties(
xTranslation = 180f,
yTranslation = 10f,
xTranslation = 170f,
yTranslation = 170f,
rotation = -3f,
elevation = 0f,
scale = 1.1f,
scale = 1f,
)
}
}
@ -108,7 +122,7 @@ class TwinsCardWidget(
xTranslation = twinProperties.xTranslation,
yTranslation = twinProperties.yTranslation - topOfAnchorView,
rotation = 0f,
scale = twinProperties.scale - 0.4f,
scale = twinProperties.scale - 0.5f,
)
}
@ -120,6 +134,10 @@ class TwinsCardWidget(
}
}
enum class TwinCardsWidgetState {
Welcome, Leapfrog, Activate
}
private data class TwinsCardProperties(
val xTranslation: Float = 0f,
val yTranslation: Float = 0f,

View file

@ -1,11 +1,20 @@
package com.tangem.tap.domain.twins
import com.tangem.common.card.Card
import com.tangem.crypto.CryptoUtils
class TwinsHelper {
companion object {
const val TWIN_FILE_NAME = "TwinPublicKey"
fun verifyTwinPublicKey(issuerData: ByteArray, cardWalletPublicKey: ByteArray?): Boolean {
if (issuerData.size < 65 || cardWalletPublicKey == null) return false
val publicKey = issuerData.sliceArray(0 until 65)
val signedKey = issuerData.sliceArray(65 until issuerData.size)
return CryptoUtils.verify(cardWalletPublicKey, publicKey, signedKey)
}
fun getTwinCardNumber(cardId: String): TwinCardNumber? {
return when {
firstCardSeries.map { cardId.startsWith(it) }.contains(true) -> {
@ -55,6 +64,7 @@ enum class TwinCardNumber(val number: Int) {
}
}
@Deprecated("Use ScanResponse.isTangemTwin")
fun Card.isTangemTwin(): Boolean {
return TwinsHelper.getTwinCardNumber(cardId) != null
}

View file

@ -1,26 +1,26 @@
package com.tangem.tap.features.send
package com.tangem.tap.features
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.snackbar.Snackbar
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_wallet.*
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
abstract fun subscribeToStore()
abstract class BaseFragment(layoutId: Int) : Fragment(layoutId), FragmentOnBackPressedHandler {
protected lateinit var mainView: View
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -38,28 +38,29 @@ abstract class BaseStoreFragment(layoutId: Int) : Fragment(layoutId) {
return mainView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar?.setNavigationOnClickListener { activity?.onBackPressed() }
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
fun showRetrySnackbar(message: String, action: () -> Unit) {
fun showRetrySnackbar(message: String, action: VoidCallback) {
val snackbar = Snackbar.make(mainView, message, Snackbar.LENGTH_INDEFINITE)
snackbar.setAction(getString(R.string.common_retry)) {
snackbar.dismiss()
action()
}.show()
}
}
interface FragmentOnBackPressedHandler {
fun handleOnBackPressed()
}
@SuppressLint("FragmentBackPressedCallback")
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
handler.handleOnBackPressed()
}
})
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener { activity?.onBackPressed() }
}

View file

@ -0,0 +1,24 @@
package com.tangem.tap.features
import com.tangem.tap.store
import org.rekotlin.StoreSubscriber
/**
[REDACTED_AUTHOR]
*/
abstract class BaseStoreFragment(layoutId: Int) : BaseFragment(layoutId) {
abstract fun subscribeToStore()
protected val storeSubscribersList = mutableListOf<StoreSubscriber<*>>()
override fun onStart() {
super.onStart()
subscribeToStore()
}
override fun onStop() {
storeSubscribersList.forEach { store.unsubscribe(it) }
storeSubscribersList.clear()
super.onStop()
}
}

View file

@ -1,16 +1,12 @@
package com.tangem.tap.features.details.redux
import android.content.Context
import com.tangem.Message
import com.tangem.blockchain.common.Wallet
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.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
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
@ -18,17 +14,15 @@ import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
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()
data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction()
sealed class EraseWallet : DetailsAction() {
object Check : EraseWallet()
@ -48,44 +42,6 @@ sealed class DetailsAction : Action {
object Success : EraseWallet()
}
sealed class CreateTwinWalletAction : DetailsAction() {
data class ShowWarning(
val twinCardNumber: TwinCardNumber?,
val createTwinWallet: CreateTwinWallet = CreateTwinWallet.RecreateWallet
) : CreateTwinWalletAction()
object NotEmpty : CreateTwinWalletAction(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
object ShowAlert : CreateTwinWalletAction()
object HideAlert : CreateTwinWalletAction()
object Proceed: CreateTwinWalletAction()
object Cancel : CreateTwinWalletAction() {
object Confirm : CreateTwinWalletAction()
}
data class LaunchFirstStep(
val message: Message, val context: Context
) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : CreateTwinWalletAction() {
object Success : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
data class LaunchThirdStep(val message: Message) : CreateTwinWalletAction() {
data class Success(val scanResponse: ScanResponse) : CreateTwinWalletAction()
object Failure : CreateTwinWalletAction()
}
}
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()

View file

@ -4,13 +4,16 @@ 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.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
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.features.details.redux.twins.CreateTwinWalletMiddleware
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.preferencesStorage
@ -26,7 +29,6 @@ class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val appCurrencyMiddleware = AppCurrencyMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val twinWalletMiddleware = CreateTwinWalletMiddleware()
val detailsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
@ -35,11 +37,25 @@ class DetailsMiddleware {
is DetailsAction.EraseWallet -> eraseWalletMiddleware.handle(action)
is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.CreateTwinWalletAction -> twinWalletMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet = store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
} else {
if (wallet.hasSendableAmountsOrPendingTransactions()) {
val walletIsNotEmpty = store.state.globalState.resources.strings.walletIsNotEmpty
store.dispatchNotification(walletIsNotEmpty)
} else {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
}
}
}
next(action)
}
@ -84,7 +100,7 @@ class DetailsMiddleware {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.EraseWallet.Confirm -> {
val card = store.state.detailsState.card ?: return
val card = store.state.detailsState.card ?: return
scope.launch {
val result = tangemSdkManager.eraseWallet(card)
withContext(Dispatchers.Main) {

View file

@ -7,10 +7,7 @@ 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.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.hasPendingTransactions
import org.rekotlin.Action
import java.util.*
@ -38,34 +35,17 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
is DetailsAction.ManageSecurity -> {
handleSecurityAction(action, detailsState)
}
is DetailsAction.CreateTwinWalletAction -> {
CreateTwinWalletReducer.handle(action, detailsState)
}
else -> detailsState
}
}
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
val twinsState = if (action.card.isTangemTwin()) {
CreateTwinWalletState(
scanResponse = action.scanResponse,
twinCardNumber = action.card.getTwinCardNumber(),
createTwinWallet = null,
showAlert = false,
allowRecreatingWallet = action.isCreatingTwinWalletAllowed
)
} else {
null
}
return DetailsState(
card = action.card, wallets = action.wallets,
cardInfo = action.card.toCardInfo(),
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
),
createTwinWalletState = twinsState,
cardTermsOfUseUrl = action.cardTou.getUrl(action.card)
card = action.scanResponse.card,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card)
)
}
@ -99,7 +79,7 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS
}
private fun handleAppCurrencyAction(
action: DetailsAction.AppCurrencyAction, state: DetailsState
action: DetailsAction.AppCurrencyAction, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.AppCurrencyAction.SetCurrencies -> {
@ -113,9 +93,9 @@ private fun handleAppCurrencyAction(
}
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
state.copy(
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName, showAppCurrencyDialog = false
)
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName, showAppCurrencyDialog = false
)
)
}
else -> state
@ -123,7 +103,7 @@ private fun handleAppCurrencyAction(
}
private fun handleSecurityAction(
action: DetailsAction.ManageSecurity, state: DetailsState
action: DetailsAction.ManageSecurity, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.ManageSecurity.SetCurrentOption -> {
@ -143,8 +123,8 @@ private fun handleSecurityAction(
is DetailsAction.ManageSecurity.OpenSecurity -> {
if (state.card?.isStart2Coin == true) {
return state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = EnumSet.of(SecurityOption.LongTap),
selectedOption = state.securityScreenState.currentOption
allowedOptions = EnumSet.of(SecurityOption.LongTap),
selectedOption = state.securityScreenState.currentOption
))
}
@ -152,13 +132,13 @@ private fun handleSecurityAction(
state.card, state.securityScreenState?.currentOption
)
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
allowedOptions = allowedSecurityOptions,
selectedOption = state.securityScreenState.currentOption
))
}
is DetailsAction.ManageSecurity.SelectOption -> {
state.copy(securityScreenState = state.securityScreenState?.copy(
selectedOption = action.option
selectedOption = action.option
))
}
is DetailsAction.ManageSecurity.ConfirmSelection -> {
@ -213,4 +193,5 @@ private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()
return CardInfo(cardId, issuer, signedHashes)}
return CardInfo(cardId, issuer, signedHashes)
}

View file

@ -6,41 +6,52 @@ import com.tangem.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import org.rekotlin.StateType
import java.util.*
import kotlin.properties.ReadOnlyProperty
data class DetailsState(
val card: Card? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val createTwinWalletState: CreateTwinWalletState? = null,
val cardTermsOfUseUrl: Uri? = null,
) : StateType
val card: Card? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
val securityScreenState: SecurityScreenState? = null,
val cardTermsOfUseUrl: Uri? = null,
) : StateType {
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { thisRef, property ->
store.state.twinCardsState
}
val isTangemTwins: Boolean
get() = store.state.globalState.scanResponse?.isTangemTwins() == true
}
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int,
val cardId: String,
val issuer: String,
val signedHashes: Int,
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
enum class ConfirmScreenState { EraseWallet, LongTap, AccessCode, PassCode }
data class SecurityScreenState(
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
val currentOption: SecurityOption = SecurityOption.LongTap,
val selectedOption: SecurityOption = currentOption,
val allowedOptions: EnumSet<SecurityOption> = EnumSet.allOf(SecurityOption::class.java),
val buttonProceed: Button = Button(true),
)
enum class SecurityOption { LongTap, PassCode, AccessCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
)

View file

@ -1,119 +0,0 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class CreateTwinWalletMiddleware {
var twinsManager: TwinCardsManager? = null
fun handle(action: DetailsAction.CreateTwinWalletAction) {
when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
val wallet = store.state.detailsState.wallets.firstOrNull()
if (wallet == null) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
return
}
val notEmpty = wallet.recentTransactions.isNotEmpty() || wallet.amounts.toSendableAmounts().isNotEmpty()
if (notEmpty) {
store.dispatch(DetailsAction.CreateTwinWalletAction.NotEmpty)
} else {
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWalletWarning))
}
}
is DetailsAction.CreateTwinWalletAction.Proceed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.CreateTwinWallet))
is DetailsAction.CreateTwinWalletAction.Cancel -> {
val step = store.state.detailsState.createTwinWalletState?.step
if (step != null && step != CreateTwinWalletStep.FirstStep
) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowAlert)
} else {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo())
}
}
is DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> {
twinsManager = null
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> {
store.state.globalState.scanResponse?.let {
twinsManager = TwinCardsManager(it, action.context)
}
scope.launch {
val result = twinsManager?.createFirstWallet(action.message)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure)
}
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep ->
scope.launch {
val result = twinsManager?.createSecondWallet(action.initialMessage,
action.preparingMessage, action.creatingWalletMessage)
withContext(Dispatchers.Main) {
when (result) {
SimpleResult.Success ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success)
is SimpleResult.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure)
}
}
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success -> {
}
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
scope.launch {
val result = twinsManager?.complete(action.message)
withContext(Dispatchers.Main) {
when (result) {
is Result.Success ->
store.dispatch(
DetailsAction.CreateTwinWalletAction
.LaunchThirdStep.Success(result.data)
)
is Result.Failure ->
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure)
}
}
}
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
scope.launch {
store.state.globalState.tapWalletManager.onCardScanned(action.scanResponse)
}
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
}
}
}
}

View file

@ -1,68 +0,0 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
class CreateTwinWalletReducer {
companion object {
fun handle(
action: DetailsAction.CreateTwinWalletAction, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.CreateTwinWalletAction.ShowWarning -> {
state.copy(createTwinWalletState = CreateTwinWalletState(
scanResponse = null,
twinCardNumber = action.twinCardNumber
?: state.createTwinWalletState?.twinCardNumber,
createTwinWallet = action.createTwinWallet,
showAlert = false,
allowRecreatingWallet = state.createTwinWalletState?.allowRecreatingWallet
))
}
DetailsAction.CreateTwinWalletAction.NotEmpty -> state
DetailsAction.CreateTwinWalletAction.ShowAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = true
))
}
DetailsAction.CreateTwinWalletAction.HideAlert -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
showAlert = false
))
}
is DetailsAction.CreateTwinWalletAction.Proceed -> {
state
}
DetailsAction.CreateTwinWalletAction.Cancel -> state
DetailsAction.CreateTwinWalletAction.Cancel.Confirm -> state
is DetailsAction.CreateTwinWalletAction.LaunchFirstStep -> state
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Success -> {
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.SecondStep
))
}
DetailsAction.CreateTwinWalletAction.LaunchFirstStep.Failure -> state
is DetailsAction.CreateTwinWalletAction.LaunchSecondStep -> state
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Success ->
state.copy(createTwinWalletState = state.createTwinWalletState?.copy(
step = CreateTwinWalletStep.ThirdStep
))
DetailsAction.CreateTwinWalletAction.LaunchSecondStep.Failure -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep -> {
state
}
is DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Success -> {
state
}
DetailsAction.CreateTwinWalletAction.LaunchThirdStep.Failure -> {
state
}
}
}
}
}

View file

@ -1,17 +0,0 @@
package com.tangem.tap.features.details.redux.twins
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardNumber
data class CreateTwinWalletState(
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 }
enum class CreateTwinWallet { CreateWallet, RecreateWallet }

View file

@ -6,17 +6,14 @@ import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
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.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
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.feedback.FeedbackEmail
import com.tangem.tap.store
import com.tangem.wallet.R
@ -68,8 +65,8 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
val cardId = if (state.card?.isTangemTwin() == true) {
state.card.getTwinCardIdForUser()
val cardId = if (state.isTangemTwins) {
state.card?.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
@ -78,8 +75,8 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
tv_signed_hashes.text = state.cardInfo.signedHashes.toString()
}
tv_signed_hashes.show(state.card?.isTangemTwin() != true)
tv_signed_hashes_title.show(state.card?.isTangemTwin() != true)
tv_signed_hashes.show(!state.isTangemTwins)
tv_signed_hashes_title.show(!state.isTangemTwins)
tv_disclaimer.setOnClickListener { store.dispatch(DetailsAction.ShowDisclaimer) }
@ -90,17 +87,11 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
startActivity(intent)
}
if (state.createTwinWalletState != null) {
if (state.createTwinWalletState.allowRecreatingWallet == true) {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
null, CreateTwinWallet.RecreateWallet
))
}
} else {
tv_erase_wallet.hide()
if (state.isTangemTwins) {
tv_erase_wallet.show()
tv_erase_wallet.text = getText(R.string.details_row_title_twins_recreate)
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.ReCreateTwinsWallet(state.twinCardsState.cardNumber!!))
}
} else {
tv_erase_wallet.show()
@ -140,9 +131,9 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,
requireContext()
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,
requireContext()
)
} else {
currencySelectionDialog.clear()

View file

@ -1,174 +0,0 @@
package com.tangem.tap.features.details.ui.twins
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.core.content.ContextCompat.getColor
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.details.redux.twins.CreateTwinWalletStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import org.rekotlin.StoreSubscriber
class CreateTwinWalletFragment : Fragment(R.layout.fragment_details_twin_cards),
StoreSubscriber<DetailsState> {
private var dialog: Dialog? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_card_2)
}
override fun newState(state: DetailsState) {
if (activity == null) return
toolbar.title = when (state.createTwinWalletState?.createTwinWallet) {
CreateTwinWallet.CreateWallet -> getText(R.string.wallet_button_create_wallet)
CreateTwinWallet.RecreateWallet, null -> getText(R.string.details_twins_recreate_toolbar)
}
val selectedColor = getColor(requireContext(), R.color.colorSecondary)
val defaultColor = getColor(requireContext(), R.color.blue_pale)
val twinCardNumber = state.createTwinWalletState?.twinCardNumber ?: TwinCardNumber.First
val cardNumber = when (state.createTwinWalletState?.step) {
CreateTwinWalletStep.FirstStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "1")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(defaultColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchFirstStep(
Message(getString(
R.string.details_twins_recreate_title_format,
twinCardNumberString
)),
requireContext()
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format,
twinCardNumber.number.toString())
twinCardNumberString
}
CreateTwinWalletStep.SecondStep -> {
val twinCardNumberString = twinCardNumber.pairNumber().number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "2")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(defaultColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchSecondStep(
Message(getString(R.string.details_twins_recreate_title_format, twinCardNumberString)),
Message(getString(R.string.details_twins_recreate_title_preparing)),
Message(getString(R.string.details_twins_recreate_title_creating_wallet)),
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, "2")
twinCardNumberString
}
CreateTwinWalletStep.ThirdStep -> {
val twinCardNumberString = twinCardNumber.number.toString()
tv_step_number.text =
getString(R.string.details_twins_recreate_step_format, "3")
v_step_1.setBackgroundColor(selectedColor)
v_step_2.setBackgroundColor(selectedColor)
v_step_3.setBackgroundColor(selectedColor)
btn_tap.setOnClickListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.LaunchThirdStep(
Message(getString(
R.string.details_twins_recreate_title_format, twinCardNumberString)
)
))
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, twinCardNumberString)
twinCardNumberString
}
else -> null
}
btn_tap.text = getString(R.string.details_twins_recreate_button_format, cardNumber)
tv_twin_title.text = getString(R.string.details_twins_recreate_title_format, cardNumber)
if (state.createTwinWalletState?.showAlert == true) {
if (dialog == null) {
dialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(R.string.details_twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ ->
store.dispatch(DetailsAction.CreateTwinWalletAction.Cancel.Confirm)
}
.setNegativeButton(R.string.common_cancel) { _, _ ->
dialog?.cancel()
}
.setOnCancelListener {
store.dispatch(DetailsAction.CreateTwinWalletAction.HideAlert)
}
.create()
dialog?.show()
}
} else {
dialog?.cancel()
dialog = null
}
}
}

View file

@ -1,60 +0,0 @@
package com.tangem.tap.features.details.ui.twins
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards_warning.*
import kotlinx.android.synthetic.main.layout_twin_cards_orange.*
class TwinWalletWarningFragment : Fragment(R.layout.fragment_details_twin_cards_warning) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val createTwinWallet = store.state.detailsState.createTwinWalletState?.createTwinWallet
if (createTwinWallet == CreateTwinWallet.CreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_subtitle)
} else if (createTwinWallet == CreateTwinWallet.RecreateWallet) {
tv_twin_cards_description.text = getText(R.string.details_twins_recreate_warning)
}
btn_cancel.setOnClickListener { store.dispatch(NavigationAction.PopBackTo()) }
btn_start.setOnClickListener {
store.dispatch(
DetailsAction.CreateTwinWalletAction.Proceed)
}
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
}

View file

@ -16,15 +16,14 @@ import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
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_home.*
import java.lang.ref.WeakReference
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_home_top.*
class HomeFragment : BaseOnboardingFragment<HomeState>() {
private lateinit var btnScanCard: ViewStateWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_home_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -38,7 +37,10 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
toolbar.hide()
val shareTransition = FragmentShareTransition(
listOf(ShareElement(WeakReference(imv_front_card), imv_front_card.transitionName)),
listOf(
ShareElement(imv_front_card, ShareElement.imvFrontCard),
ShareElement(imv_back_card, ShareElement.imvBackCard)
),
FrontCardEnterTransition(),
FrontCardExitTransition()
)
@ -62,6 +64,7 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
oldState.homeState == newState.homeState
}.select { it.homeState }
}
storeSubscribersList.add(this)
}
override fun newState(state: HomeState) {

View file

@ -2,6 +2,8 @@ package com.tangem.tap.features.home.redux
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.post
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -11,6 +13,8 @@ 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.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.send.redux.states.ButtonState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
@ -56,13 +60,24 @@ private fun handleReadCard() {
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
store.dispatch(TwinCardsAction.IfTwinsPrepareState(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)
scope.launch {
store.onCardScanned(scanResponse)
withMainContext {
if (scanResponse.twinsIsTwinned() && !preferencesStorage.wasTwinsOnboardingShown()) {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly))
navigateTo(AppScreen.OnboardingTwins, store.state.homeState.shareTransition)
} else {
navigateTo(AppScreen.Wallet, null)
}
}
}
}
}, {
changeButtonState(ButtonState.ENABLED)

View file

@ -12,14 +12,28 @@ import com.tangem.tap.preferencesStorage
class OnboardingHelper {
companion object {
fun isOnboardingCase(response: ScanResponse): Boolean = if (response.card.hasWallets())
preferencesStorage.usedCardsPrefStorage.activationIsStarted(response.card.cardId) else true
fun isOnboardingCase(response: ScanResponse): Boolean {
val cardInfoStorage = preferencesStorage.usedCardsPrefStorage
return when {
response.productType == ProductType.Twins -> {
if (!response.twinsIsTwinned()) {
true
} else {
cardInfoStorage.activationIsStarted(response.card.cardId)
}
}
response.card.hasWallets() -> {
cardInfoStorage.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.Twins -> AppScreen.OnboardingTwins
ProductType.Other -> AppScreen.OnboardingOther
}
}

View file

@ -55,7 +55,7 @@ class OnboardingManager(
}
suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance {
return when (val result = walletManager.safeUpdate()) {
val balance = when (val result = walletManager.safeUpdate()) {
is Result.Success -> {
val wallet = walletManager.wallet
val valueOfAmount = wallet.amounts[AmountType.Coin]?.value
@ -63,11 +63,10 @@ class OnboardingManager(
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)
OnboardingWalletBalance.done(valueOfAmount, wallet.hasPendingTransactions())
} else {
OnboardingWalletBalance.done(valueOfAmount, false, currency)
OnboardingWalletBalance.done(valueOfAmount, false)
}
balance
}
@ -81,6 +80,8 @@ class OnboardingManager(
}
}
}
return balance.copy(currency = Currency.Blockchain(walletManager.wallet.blockchain))
}
fun activationStarted(cardId: String) {
@ -122,7 +123,10 @@ data class OnboardingWalletBalance(
state = ProgressState.Loading
)
fun done(value: BigDecimal, hasTransactions: Boolean, currency: Currency.Blockchain): OnboardingWalletBalance =
OnboardingWalletBalance(value, currency, hasTransactions, ProgressState.Done)
fun done(value: BigDecimal, hasTransactions: Boolean): OnboardingWalletBalance = OnboardingWalletBalance(
value,
hasIncomingTransaction = hasTransactions,
state = ProgressState.Done
)
}
}

View file

@ -5,7 +5,7 @@ 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.tap.features.BaseStoreFragment
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.view_confetti.*
@ -16,11 +16,6 @@ import org.rekotlin.StoreSubscriber
*/
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()

View file

@ -15,16 +15,19 @@ 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.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.note.redux.*
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_bottom_action_views.*
import kotlinx.android.synthetic.main.layout_onboarding_note.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
@ -33,24 +36,28 @@ import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
*/
class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private var currentStep: OnboardingNoteStep = OnboardingNoteStep.None
private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
imv_front_card.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
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)
}
@ -78,7 +85,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
setBalance(state)
showConfetti(state.showConfetti)
currentStep = state.currentStep
previousStep = state.currentStep
}
private fun setBalance(state: OnboardingNoteState) {
@ -129,14 +136,22 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
tv_header.setText(R.string.onboarding_top_up_header)
tv_body.setText(R.string.onboarding_top_up_body)
if (state.balanceNonCriticalError == null) {
tv_body.setText(R.string.onboarding_top_up_body)
} else {
state.walletBalance.amountToCreateAccount?.let { amount ->
val tvBodyMessage = getString(
R.string.onboarding_top_up_body_no_account_error,
amount, state.walletBalance.currency.currencySymbol
)
tv_body.text = tvBodyMessage
}
}
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(OnboardingNoteAction.Balance.Update)
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
@ -163,7 +178,7 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
}
private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) {
if (this.currentStep == currentStep) return
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.Action
@ -10,14 +11,13 @@ import org.rekotlin.Action
*/
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()
class SetArtwork(val artwork: Artwork) : OnboardingNoteAction()
data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction()
object DetermineStepOfScreen : OnboardingNoteAction()
object Done : OnboardingNoteAction()
@ -27,6 +27,7 @@ sealed class OnboardingNoteAction : Action {
sealed class Balance {
object Update : OnboardingNoteAction()
data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction()
data class SetNonCriticalError(val error: TapError?) : OnboardingNoteAction()
}
sealed class Confetti {

View file

@ -12,6 +12,7 @@ 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.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.scope
import com.tangem.tap.store
@ -50,7 +51,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
is OnboardingNoteAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artwork)) }
withMainContext { store.dispatch(OnboardingNoteAction.SetArtwork(artwork)) }
}
}
is OnboardingNoteAction.DetermineStepOfScreen -> {
@ -107,6 +108,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = noteState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Loading,
error = null,
criticalError = null
@ -117,9 +119,11 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
val loadedBalance = onboardingManager.updateBalance(walletManager)
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
delay(if (isLoadedBefore) 0 else 300)
withMainContext { store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) }
withMainContext {
store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance))
store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error))
}
}
}
is OnboardingNoteAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
@ -128,7 +132,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
val addressWasCopied = noteState.resources.strings.addressWasCopied
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
@ -143,7 +147,7 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
OnboardingNoteAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(scanResponse)
store.onCardScanned(scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -17,18 +17,18 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingNoteSt
is GlobalAction.Onboarding.Start -> {
state = OnboardingNoteState()
}
is OnboardingNoteAction.SetArtworkUrl -> {
is OnboardingNoteAction.SetArtwork -> {
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.Balance.SetNonCriticalError -> {
state = state.copy(balanceNonCriticalError = action.error)
}
is OnboardingNoteAction.SetStepOfScreen -> {
if (action.step != state.currentStep && state.steps.contains(action.step)) {
state = state.copy(currentStep = action.step)

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.features.wallet.redux.Artwork
import org.rekotlin.StateType
@ -10,10 +11,10 @@ import org.rekotlin.StateType
*/
data class OnboardingNoteState(
val walletManager: WalletManager? = null,
val resources: AndroidResources = AndroidResources(),
// UI
val cardArtwork: Artwork? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val balanceNonCriticalError: TapError? = null,
val currentStep: OnboardingNoteStep = OnboardingNoteStep.None,
val steps: List<OnboardingNoteStep> = OnboardingNoteStep.values().toList(),
val showConfetti: Boolean = false,
@ -25,12 +26,4 @@ data class OnboardingNoteState(
enum class OnboardingNoteStep {
None, CreateWallet, TopUpWallet, Done
}
data class AndroidResources(
val strings: OnboardingStringResources = OnboardingStringResources()
)
data class OnboardingStringResources(
val addressWasCopied: Int = -1
)
}

View file

@ -8,7 +8,9 @@ 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.redux.navigation.ShareElement
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.addBackPressHandler
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
@ -17,8 +19,8 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt
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.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
/**
@ -26,10 +28,19 @@ import kotlinx.android.synthetic.main.view_onboarding_progress.*
*/
class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCardsState>() {
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_note
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
imv_front_card.transitionName = ShareElement.imvFrontCard
startPostponedEnterTransition()
toolbar.setTitle(R.string.onboarding_title)
store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork)

View file

@ -12,7 +12,7 @@ sealed class OnboardingOtherCardsAction : Action {
object CreateWallet : OnboardingOtherCardsAction()
// from redux
class SetArtworkUrl(val artwork: Artwork) : OnboardingOtherCardsAction()
class SetArtwork(val artwork: Artwork) : OnboardingOtherCardsAction()
object DetermineStepOfScreen : OnboardingOtherCardsAction()
object Done : OnboardingOtherCardsAction()

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.common.CompletionResult
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.extensions.withMainContext
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppState
@ -44,7 +45,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
is OnboardingOtherCardsAction.LoadCardArtwork -> {
scope.launch {
val artwork = onboardingManager.loadArtwork()
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artwork)) }
withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtwork(artwork)) }
}
}
is OnboardingOtherCardsAction.DetermineStepOfScreen -> {
@ -83,7 +84,7 @@ private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) {
OnboardingOtherCardsAction.Done -> {
store.dispatch(GlobalAction.Onboarding.Stop)
scope.launch {
globalState.tapWalletManager.onCardScanned(onboardingManager.scanResponse)
store.onCardScanned(onboardingManager.scanResponse)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}

View file

@ -17,7 +17,7 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingOtherC
is GlobalAction.Onboarding.Start -> {
state = OnboardingOtherCardsState()
}
is OnboardingOtherCardsAction.SetArtworkUrl -> {
is OnboardingOtherCardsAction.SetArtwork -> {
state = state.copy(cardArtwork = action.artwork)
}
is OnboardingOtherCardsAction.SetStepOfScreen -> {

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.Action
/**
[REDACTED_AUTHOR]
*/
sealed class TwinCardsAction : Action {
data class IfTwinsPrepareState(val scanResponse: ScanResponse) : TwinCardsAction()
object Init : TwinCardsAction()
data class SetMode(val mode: CreateTwinWalletMode) : TwinCardsAction()
data class SetStepOfScreen(val step: TwinCardsStep) : TwinCardsAction()
data class SetUserUnderstand(val isUnderstand: Boolean) : TwinCardsAction()
sealed class CardsManager {
data class Set(val manager: TwinCardsManager) : TwinCardsAction()
object Release : TwinCardsAction()
}
sealed class Wallet : TwinCardsAction() {
data class HandleOnBackPressed(
// this is necessary for the correct animation of the return of cards to the Home screen
val shouldResetTwinCardsWidget: (should: Boolean, popAction: VoidCallback) -> Unit
) : TwinCardsAction()
data class ShowInterruptDialog(val onOk: VoidCallback) : TwinCardsAction(), StateDialog
data class LaunchFirstStep(val initialMessage: Message, val reader: AssetReader) : TwinCardsAction()
data class LaunchSecondStep(
val initialMessage: Message,
val preparingMessage: Message,
val creatingWalletMessage: Message,
) : TwinCardsAction()
data class LaunchThirdStep(val message: Message) : TwinCardsAction()
}
// for the onboarding
data class SetPairCardId(val cardId: String) : TwinCardsAction()
object TopUp : TwinCardsAction()
object ShowAddressInfoDialog : TwinCardsAction()
data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction()
object Done : TwinCardsAction()
sealed class Balance {
object Update : TwinCardsAction()
data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction()
}
sealed class Confetti {
object Show : TwinCardsAction()
object Hide : TwinCardsAction()
}
}

View file

@ -0,0 +1,259 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.extensions.Result
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.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class TwinCardsMiddleware {
companion object {
val handler = twinsWalletMiddleware
}
}
private val twinsWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handle(action, dispatch)
next(action)
}
}
}
private fun handle(action: Action, dispatch: DispatchFunction) {
val action = action as? TwinCardsAction ?: return
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingManager
val twinCardsState = store.state.twinCardsState
fun getScanResponse(): ScanResponse {
return when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse
CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse
} ?: throw NullPointerException("ScanResponse can't be NULL")
}
fun updateScanResponse(response: ScanResponse) {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response
CreateTwinWalletMode.RecreateWallet -> store.dispatch(GlobalAction.SaveScanNoteResponse(response))
}
}
fun startCardActivation(cardId: String) {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationStarted(cardId)
}
}
fun finishCardActivation() {
if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) {
onboardingManager?.activationFinished(getScanResponse().card.cardId)
twinCardsState.pairCardId?.let { onboardingManager?.activationFinished(it) }
}
}
when (action) {
is TwinCardsAction.Init -> {
if (twinCardsState.currentStep == TwinCardsStep.WelcomeOnly) return
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
if (preferencesStorage.wasTwinsOnboardingShown()) {
val step = when {
!getScanResponse().twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet
twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done
else -> TwinCardsStep.TopUpWallet
}
store.dispatch((TwinCardsAction.SetStepOfScreen(step)))
} else {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome))
}
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning))
}
}
}
is TwinCardsAction.Wallet.HandleOnBackPressed -> {
val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet
&& twinCardsState.currentStep != TwinCardsStep.TopUpWallet
&& twinCardsState.currentStep != TwinCardsStep.Done
if (twinCardsState.showAlert) {
val onInterruptPrompt = {
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
val stateDialog = TwinCardsAction.Wallet.ShowInterruptDialog(onInterruptPrompt)
store.dispatchDialogShow(stateDialog)
} else {
store.dispatch(TwinCardsAction.CardsManager.Release)
action.shouldResetTwinCardsWidget(shouldReturnCardBack) {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
is TwinCardsAction.SetStepOfScreen -> {
when (action.step) {
TwinCardsStep.WelcomeOnly -> preferencesStorage.saveTwinsOnboardingShown()
TwinCardsStep.Welcome -> preferencesStorage.saveTwinsOnboardingShown()
TwinCardsStep.TopUpWallet -> store.dispatch(TwinCardsAction.Balance.Update)
TwinCardsStep.Done -> {
finishCardActivation()
postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) }
}
}
}
is TwinCardsAction.Wallet.LaunchFirstStep -> {
val manager = TwinCardsManager(getScanResponse().card, action.reader)
store.dispatch(TwinCardsAction.CardsManager.Set(manager))
scope.launch {
when (val result = manager.createFirstWallet(action.initialMessage)) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet))
}
}
is CompletionResult.Failure -> {
}
}
}
}
is TwinCardsAction.Wallet.LaunchSecondStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
val result = manager.createSecondWallet(
action.initialMessage,
action.preparingMessage,
action.creatingWalletMessage
)
when (result) {
is CompletionResult.Success -> {
startCardActivation(result.data.cardId)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext {
store.dispatch(TwinCardsAction.SetPairCardId(result.data.cardId))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet))
}
}
is CompletionResult.Failure -> {
}
}
}
}
is TwinCardsAction.Wallet.LaunchThirdStep -> {
val manager = twinCardsState.twinCardsManager ?: return
scope.launch {
when (val result = manager.complete(action.message)) {
is Result.Success -> {
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) }
}
is Result.Failure -> {
}
}
}
}
is TwinCardsAction.Balance.Update -> {
val walletManager = if (twinCardsState.walletManager != null) {
twinCardsState.walletManager
} else {
val wmFactory = globalState.tapWalletManager.walletManagerFactory
val walletManager = wmFactory.makePrimaryWalletManager(getScanResponse()).guard {
val message = "Loading cancelled. Cause: wallet manager didn't created"
val customError = TapError.CustomError(message)
store.dispatchErrorNotification(customError)
return
}
dispatch(TwinCardsAction.SetWalletManager(walletManager))
walletManager
}
val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading
val balanceIsLoading = twinCardsState.walletBalance.copy(
currency = Currency.Blockchain(walletManager.wallet.blockchain),
state = ProgressState.Loading,
error = null,
criticalError = null
)
store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading))
scope.launch {
val loadedBalance = onboardingManager?.updateBalance(walletManager) ?: return@launch
loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) }
delay(if (isLoadedBefore) 0 else 300)
withMainContext { store.dispatch(TwinCardsAction.Balance.Set(loadedBalance)) }
}
}
is TwinCardsAction.Balance.Set -> {
if (action.balance.balanceIsToppedUp()) {
scope.launch {
withMainContext { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) }
}
}
}
is TwinCardsAction.ShowAddressInfoDialog -> {
val addressData = twinCardsState.walletManager?.getAddressData() ?: return
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {
val topUpUrl = twinCardsState.walletManager?.getToUpUrl() ?: return
store.dispatchOpenUrl(topUpUrl)
}
TwinCardsAction.Done -> {
scope.launch {
val scanResponse = getScanResponse()
store.onCardScanned(scanResponse)
withMainContext {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatch(GlobalAction.Onboarding.Stop)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(NavigationAction.PopBackTo())
}
}
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.twins.getTwinCardNumber
import org.rekotlin.Action
class TwinCardsReducer {
companion object {
fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): TwinCardsState {
if (action !is TwinCardsAction) return state.twinCardsState
var state = state.twinCardsState
when (action) {
is TwinCardsAction.IfTwinsPrepareState -> {
state = if (action.scanResponse.isTangemTwins()) {
val cardNumber = action.scanResponse.card.getTwinCardNumber() ?: return state
TwinCardsState(
cardNumber = cardNumber,
mode = CreateTwinWalletMode.CreateWallet,
)
} else {
TwinCardsState()
}
}
is TwinCardsAction.SetMode -> {
state = state.copy(mode = action.mode)
}
is TwinCardsAction.SetStepOfScreen -> {
state = state.copy(currentStep = action.step)
}
is TwinCardsAction.SetUserUnderstand -> {
state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand)
}
is TwinCardsAction.CardsManager.Set -> {
state = state.copy(twinCardsManager = action.manager)
}
TwinCardsAction.CardsManager.Release -> {
state = state.copy(twinCardsManager = null)
}
is TwinCardsAction.SetPairCardId -> {
state = state.copy(pairCardId = action.cardId)
}
is TwinCardsAction.SetWalletManager -> {
state = state.copy(walletManager = action.walletManager)
}
is TwinCardsAction.Balance.Set -> {
state = state.copy(walletBalance = action.balance)
}
is TwinCardsAction.Confetti.Show -> {
state = state.copy(showConfetti = true)
}
is TwinCardsAction.Confetti.Hide -> {
state = state.copy(showConfetti = false)
}
}
return state
}

View file

@ -0,0 +1,61 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import org.rekotlin.StateType
/**
[REDACTED_AUTHOR]
*/
data class TwinCardsState(
// if cardNumber = null -> the scanned card is not Twin
val cardNumber: TwinCardNumber? = null,
val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet,
val currentStep: TwinCardsStep = TwinCardsStep.Welcome,
val userWasUnderstandIfWalletRecreate: Boolean = false,
// available only when creating or recreating wallets
val twinCardsManager: TwinCardsManager? = null,
// for the onboarding
val pairCardId: String? = null, // available after create\recreate wallets
val walletManager: WalletManager? = null,
val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(),
val showConfetti: Boolean = false,
) : StateType {
val steps: List<TwinCardsStep>
get() = when (mode) {
CreateTwinWalletMode.CreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.TopUpWallet,
TwinCardsStep.Done
)
CreateTwinWalletMode.RecreateWallet -> listOf(
TwinCardsStep.None,
TwinCardsStep.CreateFirstWallet,
TwinCardsStep.CreateSecondWallet,
TwinCardsStep.CreateThirdWallet,
TwinCardsStep.Done
)
}
val progress: Int
get() = steps.indexOf(currentStep)
val showAlert: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
}
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }
enum class TwinCardsStep {
None, WelcomeOnly, Welcome, Warning, CreateFirstWallet, CreateSecondWallet, CreateThirdWallet,
// for the onboarding
TopUpWallet, Done
}

View file

@ -0,0 +1,368 @@
package com.tangem.tap.features.onboarding.products.twins.ui
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.TransitionInflater
import androidx.transition.TransitionManager
import com.squareup.picasso.Picasso
import com.tangem.Message
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.domain.twins.AssetReader
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinsCardWidget
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_bg_twins_welcome.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
import org.rekotlin.Action
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
private var previousStep = TwinCardsStep.None
private lateinit var twinsWidget: TwinsCardWidget
private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget
private val assetReader: AssetReader by lazy {
object : AssetReader {
override fun readAssetAsString(name: String): String = requireContext().readAssetAsString(name)
}
}
override fun getOnboardingTopContainerId(): Int = R.layout.layout_onboarding_container_top
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
postponeEnterTransition()
store.dispatch(TwinCardsAction.Init)
}
override fun configureTransitions() {
when (store.state.twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> super.configureTransitions()
CreateTwinWalletMode.RecreateWallet -> {
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
addBackPressHandler(this)
reconfigureLayoutForTwins()
twinsWidget = TwinsCardWidget(LeapfrogWidget(cards_container)) { 250f }
btnRefreshBalanceWidget = RefreshBalanceWidget(onboarding_main_container)
toolbar.title = getText(R.string.twins_recreate_toolbar)
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder_black)
?.into(imv_twin_front_card)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder_white)
?.into(imv_twin_back_card)
}
private fun reconfigureLayoutForTwins() {
imv_front_card.hide()
imv_card_background.hide()
cards_container.show()
imv_twin_front_card.transitionName = ShareElement.imvFrontCard
imv_twin_back_card.transitionName = ShareElement.imvBackCard
}
override fun subscribeToStore() {
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.twinCardsState == newState.twinCardsState
}.select { it.twinCardsState }
}
storeSubscribersList.add(this)
}
override fun newState(state: TwinCardsState) {
if (activity == null) return
// toolbar.title = when (state.mode) {
// CreateTwinWalletMode.CreateWallet -> getText(R.string.wallet_button_create_wallet)
// CreateTwinWalletMode.RecreateWallet -> getText(R.string.twins_recreate_toolbar)
// }
pb_state.max = state.steps.size - 1
pb_state.progress = state.progress
when (state.currentStep) {
TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state)
TwinCardsStep.Welcome -> setupWelcomeState(state)
TwinCardsStep.Warning -> setupWarningState(state)
TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state)
TwinCardsStep.CreateSecondWallet -> setupCreateSecondWalletState(state)
TwinCardsStep.CreateThirdWallet -> setupCreateThirdWalletState(state)
TwinCardsStep.TopUpWallet -> setupTopUpWalletState(state)
TwinCardsStep.Done -> setupDoneState(state)
}
setBalance(state)
showConfetti(state.showConfetti)
previousStep = state.currentStep
}
private fun setBalance(state: TwinCardsState) {
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 setupWelcomeOnlyState(state: TwinCardsState) {
setupWelcomeState(state, NavigationAction.NavigateTo(AppScreen.Wallet))
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state, TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
private fun setupWelcomeState(state: TwinCardsState, mainAction: Action) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
pb_state.hide()
tv_header.setText(R.string.twins_onboarding_subtitle)
tv_body.text = getString(R.string.twins_onboarding_description_format, state.cardNumber?.pairIndexNumber())
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener { store.dispatch(mainAction) }
}
private fun setupWarningState(state: TwinCardsState) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
pb_state.hide()
chb_understand.show()
tv_header.setText(R.string.common_warning)
tv_body.setText(R.string.twins_recreate_warning)
chb_understand.setOnCheckedChangeListener { buttonView, isChecked ->
store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked))
}
btn_main_action.isEnabled = state.userWasUnderstandIfWalletRecreate
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
}
}
private fun setupCreateFirstWalletState(state: TwinCardsState) {
bg_circle_large.hide()
bg_circle_medium.hide()
bg_circle_min.hide()
chb_understand.hide()
pb_state.show()
onboarding_main_container.beginDelayedTransition()
when (previousStep) {
TwinCardsStep.None -> {
twinsWidget.toLeapfrog(false) {
when (state.cardNumber) {
TwinCardNumber.First -> startPostponedEnterTransition()
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) { startPostponedEnterTransition() }
}
}
}
}
TwinCardsStep.Welcome, TwinCardsStep.Warning -> {
twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) })
}
}
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchFirstStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)),
assetReader
))
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
}
private fun setupCreateSecondWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber?.pairNumber())
val twinPairIndexNumber = state.cardNumber?.pairIndexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinPairIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchSecondStep(
Message(getString(R.string.twins_recreate_title_format, twinPairIndexNumber)),
Message(getString(R.string.twins_recreate_title_preparing)),
Message(getString(R.string.twins_recreate_title_creating_wallet)),
))
}
}
private fun setupCreateThirdWalletState(state: TwinCardsState) {
switchToCard(state.cardNumber)
val twinIndexNumber = state.cardNumber?.indexNumber()
tv_header.text = getString(R.string.twins_recreate_title_format, twinIndexNumber)
tv_body.setText(R.string.onboarding_twins_interrupt_warning)
btn_main_action.text = getString(R.string.twins_recreate_button_format, twinIndexNumber)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Wallet.LaunchThirdStep(
Message(getString(R.string.twins_recreate_title_format, twinIndexNumber))
))
}
}
private fun setupTopUpWalletState(state: TwinCardsState) {
when (previousStep) {
TwinCardsStep.None -> {
when (state.cardNumber) {
TwinCardNumber.First -> {
twinsWidget.leapfrogWidget.unfold(false) {
twinsWidget.toActivate(false) {
startPostponedEnterTransition()
}
}
}
TwinCardNumber.Second -> {
switchToCard(state.cardNumber, false) {
twinsWidget.toActivate(false)
startPostponedEnterTransition()
}
}
}
}
TwinCardsStep.CreateThirdWallet -> {
twinsWidget.toActivate()
}
}
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(TwinCardsAction.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(TwinCardsAction.Balance.Update)
}
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}
private fun setupDoneState(state: TwinCardsState) {
btn_main_action.setText(R.string.onboarding_done_button_continue)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.Confetti.Hide)
store.dispatch(TwinCardsAction.Done)
}
btn_alternative_action.isVisible = false
btn_alternative_action.isClickable = false
tv_header.setText(R.string.onboarding_done_header)
tv_body.setText(R.string.onboarding_done_body)
val layout = when (state.mode) {
CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins
CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done
}
updateConstraints(state.currentStep, layout)
}
private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) {
if (this.previousStep == currentStep) return
val constraintSet = ConstraintSet()
constraintSet.clone(requireContext(), layoutId)
constraintSet.applyTo(onboarding_main_container)
val transition = InternalNoteLayoutTransition()
transition.interpolator = OvershootInterpolator()
TransitionManager.beginDelayedTransition(onboarding_main_container, transition)
}
private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) {
val position = cardNumber?.number ?: return
if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) {
twinsWidget.leapfrogWidget.leap(animate, onEnd)
} else {
onEnd()
}
}
override fun handleOnBackPressed() {
store.dispatch(TwinCardsAction.Wallet.HandleOnBackPressed { should, popAction ->
store.dispatch(TwinCardsAction.Confetti.Hide)
showConfetti(false)
if (should) switchToCard(TwinCardNumber.First, true, popAction)
else popAction()
})
}
}
private fun TwinCardNumber?.indexNumber(): String {
return this?.number?.toString() ?: ""
}
private fun TwinCardNumber?.pairIndexNumber(): String {
return this?.pairNumber()?.number?.toString() ?: ""
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.onboarding.products.twins.ui.dialog
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.store
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
class CreateWalletInterruptDialog {
companion object {
fun create(state: TwinCardsAction.Wallet.ShowInterruptDialog, context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context)
.setMessage(R.string.twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ -> state.onOk() }
.setNegativeButton(R.string.common_cancel) { _, _ -> }
.setOnDismissListener { store.dispatchDialogHide() }
.create()
}
}
}

View file

@ -3,8 +3,8 @@ 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.BaseStoreFragment
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

View file

@ -7,7 +7,6 @@ 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
@ -25,7 +24,8 @@ 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.*
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi.*
import com.tangem.tap.features.send.redux.AmountActionUi.*
@ -63,25 +63,9 @@ 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)
addBackPressHandler(this)
etAmountToSend = view.findViewById(R.id.etAmountToSend)
@ -97,7 +81,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun initSendButtonStates() {
btnSend.setOnClickListener {
store.dispatch(SendActionUi.SendAmountToRecipient(
Message(getString(R.string.initial_message_sign_header))
Message(getString(R.string.initial_message_sign_header))
))
}
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
@ -124,8 +108,8 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
imvQrCode.setOnClickListener {
startActivityForResult(
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
Intent(requireContext(), ScanQrCodeActivity::class.java),
ScanQrCodeActivity.SCAN_QR_REQUEST_CODE
)
}
}
@ -281,6 +265,17 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sp.edit().putString("mainCurrency", type.name).apply()
}
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 onDestroy() {
keyboardObserver.unregisterListener()
store.dispatch(ReleaseSendState)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.send.ui.stateSubscribers
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import org.rekotlin.StateType
import org.rekotlin.StoreSubscriber
import java.lang.ref.WeakReference

View file

@ -12,7 +12,7 @@ import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
import com.tangem.tap.domain.MultiMessageError
import com.tangem.tap.domain.assembleErrors
import com.tangem.tap.features.send.BaseStoreFragment
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction.Error
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.SendAction
@ -188,15 +188,15 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
val balanceText = fg.getString(R.string.send_balance_subtitle_format,
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
fg.tvBalance.update(balanceText)
fg.tilAmountToSend.isEnabled = state.inputIsEnabled
val imageRes = if (state.inputIsEnabled) R.drawable.ic_arrows_up_down else 0
fg.tvAmountCurrency.setCompoundDrawablesWithIntrinsicBounds(0, 0, imageRes, 0)
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
val textColor = if (state.inputIsEnabled) R.color.blue else R.color.textGray
fg.tvAmountCurrency.setTextColor(fg.getColor(textColor))
}
@ -245,7 +245,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
val willSent = getString(R.string.send_total_subtitle_format,
receipt.willSentCrypto, receipt.symbols.crypto)
receipt.willSentCrypto, receipt.symbols.crypto)
totalLayout.tvWillBeSentValue.update(willSent)
}
@ -277,9 +277,9 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalLayout.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
val willSent = getString(
R.string.send_total_subtitle_asset_format,
receipt.symbols.token ?: "", receipt.willSentToken,
receipt.symbols.crypto, receipt.willSentFeeCoin
R.string.send_total_subtitle_asset_format,
receipt.symbols.token ?: "", receipt.willSentToken,
receipt.symbols.crypto, receipt.willSentFeeCoin
)
totalLayout.tvWillBeSentValue.update(willSent)
}

View file

@ -18,7 +18,6 @@ import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_add_tokens.*
import kotlinx.android.synthetic.main.fragment_details_twin_cards.toolbar
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.*
import org.rekotlin.StoreSubscriber

View file

@ -5,12 +5,13 @@ 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
import com.tangem.tap.domain.extensions.toSendableAmounts
data class PendingTransaction(
val address: String?,
val amount: String?,
val currency: String,
val type: PendingTransactionType
val address: String?,
val amount: String?,
val currency: String,
val type: PendingTransactionType
)
enum class PendingTransactionType { Incoming, Outgoing, Unknown }
@ -36,10 +37,10 @@ fun TransactionData.toPendingTransaction(walletAddress: String): PendingTransact
}
return PendingTransaction(
if (address == "unknown") null else address,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
if (address == "unknown") null else address,
this.amount.value?.toFormattedString(amount.decimals),
this.amount.currencySymbol,
type
)
}
@ -65,5 +66,9 @@ fun Wallet.getPendingTransactions(): List<PendingTransaction> {
}
fun Wallet.hasPendingTransactions(): Boolean {
return recentTransactions.toPendingTransactions(address).isNotEmpty()
return getPendingTransactions().isNotEmpty()
}
fun Wallet.hasSendableAmountsOrPendingTransactions(): Boolean {
return hasPendingTransactions() || amounts.toSendableAmounts().isNotEmpty()
}

View file

@ -9,7 +9,6 @@ import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -147,12 +146,4 @@ sealed class WalletAction : Action {
data class ChangeSelectedAddress(val type: AddressType) : WalletAction()
sealed class TwinsAction : WalletAction() {
object ShowOnboarding : TwinsAction()
object SetOnboardingShown : TwinsAction()
data class SetTwinCard(
val number: TwinCardNumber,
val isCreatingTwinCardsAllowed: Boolean,
) : TwinsAction()
}
}

View file

@ -12,39 +12,49 @@ import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
import kotlin.properties.ReadOnlyProperty
data class WalletState(
val state: ProgressState = ProgressState.Done,
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val walletDialog: StateDialog? = null,
val twinCardsState: TwinCardsState? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedWallet: Currency? = null,
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val tradeCryptoAllowed: TradeCryptoAvailability = TradeCryptoAvailability()
val state: ProgressState = ProgressState.Done,
val error: ErrorType? = null,
val cardImage: Artwork? = null,
val hashesCountVerified: Boolean? = null,
val walletDialog: StateDialog? = null,
val mainWarningsList: List<WarningMessage> = mutableListOf(),
val wallets: List<WalletData> = emptyList(),
val walletManagers: List<WalletManager> = emptyList(),
val isMultiwalletAllowed: Boolean = false,
val cardCurrency: CryptoCurrencyName? = null,
val selectedWallet: Currency? = null,
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val tradeCryptoAllowed: TradeCryptoAvailability = TradeCryptoAvailability()
) : StateType {
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { thisRef, property ->
store.state.twinCardsState
}
val isTangemTwins: Boolean
get() = store.state.globalState.scanResponse?.isTangemTwins() == true
val primaryWallet = if (wallets.isNotEmpty()) wallets[0] else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != com.tangem.tap.features.wallet.ui.BalanceStatus.UnknownBlockchain
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val blockchains: List<Blockchain>
get() = walletManagers.map { it.wallet.blockchain }
@ -57,16 +67,16 @@ data class WalletState(
return walletManagers.find { it.wallet.blockchain == token.blockchain }
}
fun getWalletManager(currency: Currency?) : WalletManager? {
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return walletManagers.find { it.wallet.blockchain == currency.blockchain }
}
fun getWalletManager(blockchain: Blockchain) : WalletManager? {
fun getWalletManager(blockchain: Blockchain): WalletManager? {
return walletManagers.find { it.wallet.blockchain == blockchain }
}
fun getWalletData(currency: Currency?) : WalletData? {
fun getWalletData(currency: Currency?): WalletData? {
if (currency == null) return null
return wallets.find { it.currency == currency }
}
@ -93,7 +103,7 @@ data class WalletState(
?: return true
if (walletData.currency is Currency.Blockchain &&
walletManager.cardTokens.isNotEmpty()
walletManager.cardTokens.isNotEmpty()
) {
return false
}
@ -105,9 +115,9 @@ data class WalletState(
wallet.amounts.toSendableAmounts().isEmpty()
} else if (walletData.currency is Currency.Token) (
return wallet.recentTransactions.toPendingTransactionsForToken(
walletData.currency.token, wallet.address).isEmpty()
walletData.currency.token, wallet.address).isEmpty()
&& wallet.amounts[AmountType.Token(token = walletData.currency.token)]
?.isAboveZero() != true
?.isAboveZero() != true
)
}
return false
@ -137,9 +147,9 @@ data class WalletState(
}
}
sealed class WalletDialog: StateDialog {
sealed class WalletDialog : StateDialog {
data class QrDialog(
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
@ -147,7 +157,7 @@ sealed class WalletDialog: StateDialog {
object ChooseTradeActionDialog : WalletDialog()
}
enum class ProgressState: WidgetState { Loading, Done, Error }
enum class ProgressState : WidgetState { Loading, Done, Error }
enum class ErrorType { NoInternetConnection }
@ -157,22 +167,22 @@ sealed class WalletMainButton(enabled: Boolean) : Button(enabled) {
}
data class WalletAddresses(
val selectedAddress: AddressData,
val list: List<AddressData>
val selectedAddress: AddressData,
val list: List<AddressData>
)
data class AddressData(
val address: String,
val type: AddressType,
val shareUrl: String,
val exploreUrl: String,
val address: String,
val type: AddressType,
val shareUrl: String,
val exploreUrl: String,
) {
val qrCode: Bitmap by lazy { shareUrl.toQrCode() }
}
data class Artwork(
val artworkId: String,
val artwork: Bitmap? = null
val artworkId: String,
val artwork: Bitmap? = null
) {
companion object {
const val DEFAULT_IMG_URL = "https://app.tangem.com/cards/card_default.png"
@ -198,12 +208,6 @@ data class TradeCryptoAvailability(
val availableToSell: Set<String> = TradeCryptoHelper.AVAILABLE_TO_SELL,
)
data class TwinCardsState(
val cardNumber: TwinCardNumber?,
val showTwinOnboarding: Boolean,
val isCreatingTwinCardsAllowed: Boolean
)
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.wallet.redux.middlewares
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
class TwinsMiddleware {
fun handle(action: WalletAction.TwinsAction) {
when (action) {
is WalletAction.TwinsAction.SetTwinCard -> {
val showOnboarding = !preferencesStorage.wasTwinsOnboardingShown()
if (showOnboarding) store.dispatch(WalletAction.TwinsAction.ShowOnboarding)
}
WalletAction.TwinsAction.SetOnboardingShown -> {
preferencesStorage.saveTwinsOnboardingShown()
}
}
}
}

View file

@ -11,209 +11,185 @@ import com.tangem.common.services.Result
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.extensions.*
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.extensions.toSendableAmounts
import com.tangem.tap.domain.twins.TwinsHelper
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.twins.CreateTwinWallet
import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
class WalletMiddleware {
private val tradeCryptoMiddleware = TradeCryptoMiddleware()
private val twinsMiddleware = TwinsMiddleware()
private val warningsMiddleware = WarningsMiddleware()
private val multiWalletMiddleware = MultiWalletMiddleware()
val walletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
val globalState = state()?.globalState
val walletState = state()?.walletState
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(action)
is WalletAction.TwinsAction -> twinsMiddleware.handle(action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet ->
multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
walletState?.walletManagers?.map { walletManager ->
globalState?.tapWalletManager?.loadWalletData(walletManager)
}
} else {
val walletManager = walletState?.getWalletManager(action.blockchain)
walletManager?.let { globalState?.tapWalletManager?.loadWalletData(it) }
}
}
}
is WalletAction.LoadWallet.Success -> {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState?.getWalletData(action.wallet.blockchain) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(action.wallet.blockchain))
store.dispatch(WalletAction.LoadWallet.Success(action.wallet))
}
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
scope.launch {
when {
action.wallet != null -> {
globalState?.tapWalletManager?.loadFiatRate(
globalState.appCurrency, action.wallet
)
}
action.currency != null -> {
globalState?.tapWalletManager?.loadFiatRate(
globalState.appCurrency, action.currency
)
}
else -> {
globalState?.tapWalletManager?.loadFiatRate(
fiatCurrency = globalState.appCurrency,
currencies = walletState?.wallets?.mapNotNull { it.currency }
?: emptyList()
)
}
}
}
}
is WalletAction.CreateWallet -> {
if (walletState?.twinCardsState != null) {
store.dispatch(DetailsAction.CreateTwinWalletAction.ShowWarning(
globalState?.scanResponse?.card?.cardId?.let {
TwinsHelper.getTwinCardNumber(it)
},
CreateTwinWallet.CreateWallet
))
} else {
scope.launch {
val result = tangemSdkManager.createWallet(
globalState?.scanResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse =
globalState?.scanResponse?.copy(card = result.data)
scanNoteResponse?.let {
globalState.tapWalletManager.onCardScanned(scanNoteResponse)
}
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = store.state.detailsState.card
)
}
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (action.blockchain != null) {
scope.launch {
val walletManager = walletState?.getWalletManager(action.blockchain)
walletManager?.let { globalState?.tapWalletManager?.updateWallet(it) }
}
} else {
scope.launch {
if (walletState?.state == ProgressState.Done) {
walletState.walletManagers.map { walletManager ->
globalState?.tapWalletManager?.updateWallet(walletManager)
}
}
}
}
handleAction(action, dispatch)
next(action)
}
}
}
private fun handleAction(action: Action, dispatch: DispatchFunction) {
val globalState = store.state.globalState
val walletState = store.state.walletState
when (action) {
is WalletAction.TradeCryptoAction -> tradeCryptoMiddleware.handle(action)
is WalletAction.Warnings -> warningsMiddleware.handle(action, globalState)
is WalletAction.MultiWallet -> multiWalletMiddleware.handle(action, walletState, globalState)
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
walletState.walletManagers.map { walletManager ->
globalState.tapWalletManager.loadWalletData(walletManager)
}
} else {
val walletManager = walletState.getWalletManager(action.blockchain)
walletManager?.let { globalState.tapWalletManager.loadWalletData(it) }
}
is WalletAction.Scan -> {
store.dispatch(HomeAction.ShouldScanCardOnResume(true))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
is WalletAction.LoadWallet.Success -> {
val coinAmount = action.wallet.amounts[AmountType.Coin]?.value
if (coinAmount != null && !coinAmount.isZero()) {
if (walletState.getWalletData(action.wallet.blockchain) == null) {
store.dispatch(WalletAction.MultiWallet.AddBlockchain(action.wallet.blockchain))
store.dispatch(WalletAction.LoadWallet.Success(action.wallet))
}
is WalletAction.LoadCardInfo -> {
scope.launch {
val response = OnlineCardVerifier().getCardInfo(
action.card.cardId, action.card.cardPublicKey
}
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
warningsMiddleware.tryToShowAppRatingWarning(action.wallet)
}
is WalletAction.LoadFiatRate -> {
scope.launch {
when {
action.wallet != null -> {
globalState.tapWalletManager.loadFiatRate(
globalState.appCurrency, action.wallet
)
when (response) {
is Result.Success -> {
store.dispatchOnMain(
WalletAction.SetArtworkId(response.data.artwork?.id)
)
store.dispatchOnMain(
WalletAction.LoadArtwork(action.card, response.data.artwork?.id)
)
store.dispatchOnMain(
GlobalAction.SetIfCardVerifiedOnline(response.data.passed)
)
}
is Result.Failure -> {
store.dispatchOnMain(
GlobalAction.SetIfCardVerifiedOnline(false)
)
}
}
store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded)
}
}
is WalletAction.LoadData -> {
scope.launch {
val scanNoteResponse = globalState?.scanResponse
?: return@launch
if (!walletState?.wallets.isNullOrEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
action.currency != null -> {
globalState.tapWalletManager.loadFiatRate(
globalState.appCurrency, action.currency
)
}
}
is NetworkStateChanged -> {
globalState?.scanResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
scope.launch {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
}
}
is WalletAction.CopyAddress -> {
action.context.copyToClipboard(action.address)
store.dispatch(WalletAction.CopyAddress.Success)
}
is WalletAction.ShareAddress -> {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(action.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount, state()?.walletState)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
else -> {
globalState.tapWalletManager.loadFiatRate(
fiatCurrency = globalState.appCurrency,
currencies = walletState.wallets.mapNotNull { it.currency }
)
}
}
}
next(action)
}
is WalletAction.CreateWallet -> {
scope.launch {
val result = tangemSdkManager.createWallet(
globalState.scanResponse?.card?.cardId
)
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse = globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = store.state.detailsState.card
)
}
}
}
}
}
is WalletAction.UpdateWallet -> {
if (action.blockchain != null) {
scope.launch {
val walletManager = walletState.getWalletManager(action.blockchain)
walletManager?.let { globalState.tapWalletManager.updateWallet(it) }
}
} else {
scope.launch {
if (walletState.state == ProgressState.Done) {
walletState.walletManagers.map { walletManager ->
globalState.tapWalletManager.updateWallet(walletManager)
}
}
}
}
}
is WalletAction.Scan -> {
store.dispatch(HomeAction.ShouldScanCardOnResume(true))
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
is WalletAction.LoadCardInfo -> {
scope.launch {
val response = OnlineCardVerifier().getCardInfo(
action.card.cardId, action.card.cardPublicKey
)
when (response) {
is Result.Success -> {
val actionList = listOf(
WalletAction.SetArtworkId(response.data.artwork?.id),
WalletAction.LoadArtwork(action.card, response.data.artwork?.id),
GlobalAction.SetIfCardVerifiedOnline(response.data.passed)
)
actionList.forEach { store.dispatch(it) }
}
is Result.Failure -> {
store.dispatchOnMain(GlobalAction.SetIfCardVerifiedOnline(false))
}
}
store.dispatchOnMain(WalletAction.Warnings.CheckIfNeeded)
}
}
is WalletAction.LoadData -> {
scope.launch {
val scanNoteResponse = globalState.scanResponse ?: return@launch
if (!walletState.wallets.isEmpty()) {
globalState.tapWalletManager.reloadData(scanNoteResponse)
} else {
globalState.tapWalletManager.loadData(scanNoteResponse)
}
}
}
is NetworkStateChanged -> {
globalState.scanResponse?.let { scanNoteResponse ->
store.dispatch(WalletAction.Warnings.CheckHashesCount.CheckHashesCountOnline)
scope.launch { globalState.tapWalletManager.loadData(scanNoteResponse) }
}
}
is WalletAction.CopyAddress -> {
action.context.copyToClipboard(action.address)
store.dispatch(WalletAction.CopyAddress.Success)
}
is WalletAction.ShareAddress -> {
action.context.shareText(action.address)
}
is WalletAction.ExploreAddress -> {
val uri = Uri.parse(action.exploreUrl)
val intent = Intent(Intent.ACTION_VIEW, uri)
ContextCompat.startActivity(action.context, intent, null)
}
is WalletAction.Send -> {
val newAction = prepareSendAction(action.amount, store.state.walletState)
store.dispatch(newAction)
if (newAction is PrepareSendScreen) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
}
}
@ -236,12 +212,12 @@ class WalletMiddleware {
when (currency) {
is Currency.Blockchain -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.blockchain.currency }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.Send.ChooseCurrency(amounts)
PrepareSendScreen(amountToSend, selectedWalletData.fiatRate, walletManager)
}
is Currency.Token -> {
val amountToSend = amounts?.find { it.currencySymbol == currency.token.symbol }
?: return WalletAction.Send.ChooseCurrency(amounts)
?: return WalletAction.Send.ChooseCurrency(amounts)
prepareSendActionForToken(amountToSend, state, selectedWalletData, wallet, walletManager)
}
}
@ -257,8 +233,8 @@ class WalletMiddleware {
}
private fun prepareSendActionForToken(
amount: Amount, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?,
walletManager: WalletManager?
amount: Amount, state: WalletState?, selectedWalletData: WalletData?, wallet: Wallet?,
walletManager: WalletManager?
): PrepareSendScreen {
val coinRate = state?.getWalletData(wallet?.blockchain)?.fiatRate
val tokenRate = if (state?.isMultiwalletAllowed == true) {
@ -267,7 +243,7 @@ class WalletMiddleware {
selectedWalletData?.currencyData?.token?.fiatRate
}
return PrepareSendScreen(
wallet?.amounts?.get(AmountType.Coin), coinRate, walletManager,
amount, tokenRate)
wallet?.amounts?.get(AmountType.Coin), coinRate, walletManager,
amount, tokenRate)
}
}

View file

@ -17,7 +17,7 @@ import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.tap.domain.extensions.hasSignedHashes
import com.tangem.tap.domain.extensions.remainingSignatures
import com.tangem.tap.domain.isMultiwalletAllowed
import com.tangem.tap.domain.twins.isTangemTwin
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.preferencesStorage
@ -51,12 +51,12 @@ class WarningsMiddleware {
}
is WalletAction.Warnings.CheckRemainingSignatures -> {
if (action.remainingSignatures != null &&
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
action.remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
store.state.globalState.warningManager
?.removeWarnings(
messageRes = R.string.warning_low_signatures_format
)
?.removeWarnings(
messageRes = R.string.warning_low_signatures_format
)
addWarningMessage(
warning =
WarningMessagesManager.remainingSignaturesNotEnough(action.remainingSignatures),
@ -81,7 +81,8 @@ class WarningsMiddleware {
}
private fun showCardWarningsIfNeeded(globalState: GlobalState?) {
globalState?.scanResponse?.card?.let { card ->
globalState?.scanResponse?.let { scanResponse ->
val card = scanResponse.card
globalState.warningManager?.removeWarnings(WarningMessage.Origin.Local)
if (card.isTestCard) {
addWarningMessage(WarningMessagesManager.testCardWarning(), autoUpdate = true)
@ -92,7 +93,7 @@ class WarningsMiddleware {
if (card.firmwareVersion.type != FirmwareVersion.FirmwareType.Release) {
addWarningMessage(WarningMessagesManager.devCardWarning())
} else if (!preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) {
checkIfWarningNeeded(card)?.let { warning -> addWarningMessage(warning) }
checkIfWarningNeeded(scanResponse)?.let { warning -> addWarningMessage(warning) }
}
if (card.firmwareVersion.type == FirmwareVersion.FirmwareType.Release) {
if (!globalState.cardVerifiedOnline) {
@ -106,7 +107,7 @@ class WarningsMiddleware {
private fun showWarningLowRemainingSignaturesIfNeeded(card: Card) {
val remainingSignatures = card.remainingSignatures
if (remainingSignatures != null &&
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
remainingSignatures <= WarningMessagesManager.REMAINING_SIGNATURES_WARNING
) {
addWarningMessage(
WarningMessagesManager.remainingSignaturesNotEnough(
@ -117,12 +118,12 @@ class WarningsMiddleware {
}
private fun checkIfWarningNeeded(
card: Card,
scanResponse: ScanResponse,
): WarningMessage? {
if (card.isTangemTwin()) return null
if (scanResponse.isTangemTwins()) return null
if (card.isMultiwalletAllowed) {
return if (card.hasSignedHashes()) {
if (scanResponse.card.isMultiwalletAllowed) {
return if (scanResponse.card.hasSignedHashes()) {
WarningMessagesManager.signedHashesMultiWalletWarning()
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
@ -133,7 +134,7 @@ class WarningsMiddleware {
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator
return if (validator == null) {
if (card.hasSignedHashes()) {
if (scanResponse.card.hasSignedHashes()) {
WarningMessagesManager.alreadySignedHashesWarning()
} else {
store.dispatch(WalletAction.Warnings.CheckHashesCount.SaveCardId)
@ -149,10 +150,11 @@ class WarningsMiddleware {
if (store.state.walletState.hashesCountVerified != false) return
if (!NetworkConnectivity.getInstance().isOnlineOrConnecting()) return
val card = store.state.globalState.scanResponse?.card
val scanResponse = store.state.globalState.scanResponse
val card = scanResponse?.card
if (card == null || preferencesStorage.usedCardsPrefStorage.wasScanned(card.cardId)) return
if (card.isTangemTwin() || card.isMultiwalletAllowed) return
if (scanResponse.isTangemTwins() || card.isMultiwalletAllowed) return
val validator = store.state.walletState.walletManagers.firstOrNull()
as? SignatureCountValidator

View file

@ -1,35 +0,0 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.tap.features.wallet.redux.TwinCardsState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
class TwinsReducer {
fun reduce(action: WalletAction.TwinsAction, state: WalletState): WalletState {
return when (action) {
is WalletAction.TwinsAction.SetTwinCard -> {
state.copy(
twinCardsState = TwinCardsState(
cardNumber = action.number,
showTwinOnboarding = state.twinCardsState?.showTwinOnboarding
?: false,
isCreatingTwinCardsAllowed = action.isCreatingTwinCardsAllowed
)
)
}
is WalletAction.TwinsAction.ShowOnboarding -> {
state.copy(
twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = true)
?: TwinCardsState(cardNumber = null,
showTwinOnboarding = true,
isCreatingTwinCardsAllowed = false)
)
}
is WalletAction.TwinsAction.SetOnboardingShown -> {
state.copy(
twinCardsState = state.twinCardsState?.copy(showTwinOnboarding = false)
)
}
}
}
}

View file

@ -25,7 +25,6 @@ class WalletReducer {
private fun internalReduce(action: Action, state: AppState): WalletState {
val multiWalletReducer = MultiWalletReducer()
val twinsReducer = TwinsReducer()
val onWalletLoadedReducer = OnWalletLoadedReducer()
if (action !is WalletAction) return state.walletState
@ -34,21 +33,17 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
when (action) {
is WalletAction.Warnings -> newState = handleCheckSignedHashesActions(action, newState)
is WalletAction.TwinsAction -> newState = twinsReducer.reduce(action, newState)
is WalletAction.MultiWallet -> newState = multiWalletReducer.reduce(action, newState)
is WalletAction.ResetState -> newState = WalletState()
is WalletAction.SetIfTestnetCard -> newState = newState.copy(isTestnet = action.isTestnet)
is WalletAction.EmptyWallet -> {
val creatingWalletAllowed = !(newState.twinCardsState != null &&
newState.twinCardsState?.isCreatingTwinCardsAllowed != true)
newState = newState.copy(
state = ProgressState.Done,
wallets = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(creatingWalletAllowed),
mainButton = WalletMainButton.CreateWalletButton(true),
)
)
)
@ -215,7 +210,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (newState.twinCardsState?.cardNumber) {
?: when (state.twinCardsState.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL

View file

@ -1,62 +0,0 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.squareup.picasso.Picasso
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_twin_cards.*
import kotlinx.android.synthetic.main.layout_twin_cards.*
class OnboardingTwinsFragment : Fragment(R.layout.fragment_twin_cards) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
store.dispatch(WalletAction.TwinsAction.SetOnboardingShown)
val secondTwinNumber =
store.state.walletState.twinCardsState?.cardNumber?.pairNumber()?.number ?: ""
val text = getString(R.string.twins_onboarding_description_format, secondTwinNumber)
tv_twin_cards_description_1.text = text
setOnClickListeners()
Picasso.get()
.load(Artwork.TWIN_CARD_1)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_1)
Picasso.get()
.load(Artwork.TWIN_CARD_2)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
?.into(iv_twin_card_2)
}
private fun setOnClickListeners() {
btn_continue.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
}
}

View file

@ -20,9 +20,7 @@ import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_twin_cards.*
import kotlinx.android.synthetic.main.fragment_wallet_details.*
import kotlinx.android.synthetic.main.fragment_wallet_details.toolbar
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import kotlinx.android.synthetic.main.item_popular_token.view.*
import kotlinx.android.synthetic.main.layout_balance_error.*
@ -98,12 +96,12 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
tv_currency_title.text = selectedWallet.currencyData.currency
val currency = selectedWallet.currency
if (currency is Currency.Token) {
tv_currency_subtitle.text = currency.blockchain.tokenDisplayName()
tv_currency_subtitle.show()
if (currency is Currency.Token) {
tv_currency_subtitle.text = currency.blockchain.tokenDisplayName()
tv_currency_subtitle.show()
} else {
tv_currency_subtitle.hide()
}
tv_currency_subtitle.hide()
}
showPendingTransactionsIfPresent(selectedWallet.pendingTransactions)
@ -129,9 +127,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
srl_wallet_details.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
allowToBuy = selectedWallet.tradeCryptoState.buyingAllowed,
allowToSell = selectedWallet.tradeCryptoState.sellingAllowed,
blockchain = selectedWallet.currency?.blockchain
allowToBuy = selectedWallet.tradeCryptoState.buyingAllowed,
allowToSell = selectedWallet.tradeCryptoState.sellingAllowed,
blockchain = selectedWallet.currency?.blockchain
))
}
}
@ -180,8 +178,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
tv_address.text = state.walletAddresses.selectedAddress.address
tv_explore?.setOnClickListener {
store.dispatch(WalletAction.ExploreAddress(
state.walletAddresses.selectedAddress.exploreUrl,
requireContext()))
state.walletAddresses.selectedAddress.exploreUrl,
requireContext()))
}
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
}
@ -192,8 +190,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
if (state.error == ErrorType.NoInternetConnection) {
srl_wallet_details?.isRefreshing = false
(activity as? SnackbarHandler)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
) { store.dispatch(WalletAction.LoadData) }
}
} else {
@ -249,8 +247,8 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
tv_error_title.text = getText(R.string.wallet_error_no_account)
tv_error_descriptions.text =
getString(
R.string.wallet_error_no_account_subtitle_format,
data.amountToCreateAccount, data.currencySymbol
R.string.wallet_error_no_account_subtitle_format,
data.amountToCreateAccount, data.currencySymbol
)
}
}

View file

@ -138,8 +138,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
if (state.error == ErrorType.NoInternetConnection) {
srl_wallet_details?.isRefreshing = false
(activity as? MainActivity)?.showSnackbar(
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
text = R.string.wallet_notification_no_internet,
buttonTitle = R.string.common_retry
) { store.dispatch(WalletAction.LoadData) }
}
} else {
@ -150,8 +150,8 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
private fun setupCardImage(cardImage: Artwork?) {
Picasso.get()
.load(cardImage?.artworkId)
.placeholder(R.drawable.card_placeholder)
?.error(R.drawable.card_placeholder)
.placeholder(R.drawable.card_placeholder_black)
?.error(R.drawable.card_placeholder_black)
?.into(iv_card)
}
@ -161,11 +161,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
store.dispatch(GlobalAction.UpdateFeedbackInfo(store.state.walletState.walletManagers))
store.state.globalState.scanResponse?.let { scanNoteResponse ->
store.dispatch(DetailsAction.PrepareScreen(
scanNoteResponse.card, scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
store.state.globalState.configManager?.config?.isCreatingTwinCardsAllowed,
CardTou(),
store.state.globalState.appCurrency
scanNoteResponse,
store.state.walletState.walletManagers.map { it.wallet },
CardTou(),
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true

View file

@ -7,9 +7,8 @@ import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.BalanceWidget
@ -71,7 +70,7 @@ class SingleWalletView : WalletView {
state.primaryWallet ?: return
setupTwinCards(state.twinCardsState, fragment)
setupButtons(state.primaryWallet, state.twinCardsState != null, fragment)
setupButtons(state.primaryWallet, state.isTangemTwins, fragment)
setupAddressCard(state.primaryWallet, fragment)
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
setupBalance(state, state.primaryWallet)
@ -87,12 +86,12 @@ class SingleWalletView : WalletView {
private fun setupBalance(state: WalletState, primaryWallet: WalletData) {
fragment?.apply {
this.l_balance.show()
BalanceWidget(this, primaryWallet.currencyData, state.twinCardsState != null).setup()
BalanceWidget(this, primaryWallet.currencyData, state.isTangemTwins).setup()
}
}
private fun setupTwinCards(
twinCardsState: TwinCardsState?, fragment: WalletFragment
twinCardsState: TwinCardsState?, fragment: WalletFragment
) = with(fragment) {
twinCardsState?.cardNumber?.let { cardNumber ->
tv_twin_card_number.show()
@ -106,11 +105,6 @@ class SingleWalletView : WalletView {
if (twinCardsState?.cardNumber == null) {
tv_twin_card_number.hide()
}
if (twinCardsState?.showTwinOnboarding == true) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
}
private fun setupButtons(

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="12dp" />
<size
android:width="328dp"
android:height="174dp" />
<gradient
android:endColor="#FFFFFF"
android:startColor="#FFFFFF" />
</shape>
</item>
<item
android:drawable="@drawable/ic_tangem_pale"
android:gravity="center">
<size
android:width="85dp"
android:height="23dp" />
</item>
</layer-list>

View file

@ -0,0 +1,126 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="249dp"
android:height="150dp"
android:viewportWidth="249"
android:viewportHeight="150">
<path
android:fillAlpha="0.03"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z" />
<path
android:fillAlpha="0.5"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="7.404067"
android:centerY="8.65384"
android:gradientRadius="267.36932"
android:type="radial">
<item
android:color="#FFFFFFFF"
android:offset="0" />
<item
android:color="#19FFFFFF"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#1B1D1C"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z" />
<path
android:fillColor="#00000000"
android:pathData="M10,0.5L238,0.5A9.499,9.501 134.998,0 1,247.501 10L247.517,140A9.499,9.501 134.998,0 1,238.019 149.5L10.019,149.5A9.499,9.501 134.998,0 1,0.517 140L0.501,10A9.499,9.501 134.998,0 1,10 0.5z"
android:strokeWidth="1">
<aapt:attr name="android:strokeColor">
<gradient
android:endX="246.96196"
android:endY="144.774"
android:startX="2.844399"
android:startY="3.38345"
android:type="linear">
<item
android:color="#FF1A1A1A"
android:offset="0" />
<item
android:color="#FF565656"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#A5A5A5"
android:pathData="M28.359,121.942L26.389,120.734L26.472,123H25.41L25.498,120.734L23.505,121.95L23.002,121.003L25.044,119.992L23.009,118.974L23.511,118.058L25.497,119.25L25.415,117H26.468L26.389,119.258L28.365,118.058L28.884,118.974L26.826,120L28.885,121.011L28.359,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M38.855,121.942L36.885,120.734L36.967,123H35.906L35.994,120.734L34.001,121.95L33.498,121.003L35.54,119.992L33.505,118.974L34.007,118.058L35.993,119.25L35.911,117H36.964L36.884,119.258L38.861,118.058L39.38,118.974L37.322,120L39.381,121.011L38.855,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M49.351,121.942L47.381,120.734L47.463,123H46.402L46.49,120.734L44.497,121.95L43.994,121.003L46.035,119.992L44.001,118.974L44.503,118.058L46.489,119.25L46.407,117H47.46L47.38,119.258L49.357,118.058L49.876,118.974L47.818,120L49.877,121.011L49.351,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M59.847,121.942L57.877,120.734L57.959,123H56.897L56.985,120.734L54.993,121.95L54.49,121.003L56.531,119.992L54.497,118.974L54.999,118.058L56.985,119.25L56.902,117H57.956L57.876,119.258L59.853,118.058L60.372,118.974L58.314,120L60.373,121.011L59.847,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M76.235,121.942L74.266,120.734L74.348,123H73.286L73.374,120.734L71.381,121.95L70.879,121.003L72.92,119.992L70.885,118.974L71.387,118.058L73.373,119.25L73.291,117H74.345L74.265,119.258L76.242,118.058L76.761,118.974L74.703,120L76.762,121.011L76.235,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M86.731,121.942L84.761,120.734L84.844,123H83.782L83.87,120.734L81.877,121.95L81.374,121.003L83.416,119.992L81.381,118.974L81.883,118.058L83.869,119.25L83.787,117H84.841L84.761,119.258L86.737,118.058L87.257,118.974L85.199,120L87.258,121.011L86.731,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M97.227,121.942L95.257,120.734L95.34,123H94.278L94.366,120.734L92.373,121.95L91.87,121.003L93.912,119.992L91.877,118.974L92.379,118.058L94.365,119.25L94.283,117H95.337L95.257,119.258L97.233,118.058L97.752,118.974L95.695,120L97.753,121.011L97.227,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M107.723,121.942L105.753,120.734L105.835,123H104.774L104.862,120.734L102.869,121.95L102.366,121.003L104.408,119.992L102.373,118.974L102.875,118.058L104.861,119.25L104.779,117H105.832L105.752,119.258L107.729,118.058L108.248,118.974L106.19,120L108.249,121.011L107.723,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M124.112,121.942L122.142,120.734L122.224,123H121.163L121.251,120.734L119.258,121.95L118.755,121.003L120.796,119.992L118.762,118.974L119.264,118.058L121.25,119.25L121.168,117H122.221L122.141,119.258L124.118,118.058L124.637,118.974L122.579,120L124.638,121.011L124.112,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M134.608,121.942L132.638,120.734L132.72,123H131.659L131.746,120.734L129.754,121.95L129.251,121.003L131.292,119.992L129.258,118.974L129.76,118.058L131.746,119.25L131.664,117H132.717L132.637,119.258L134.614,118.058L135.133,118.974L133.075,120L135.134,121.011L134.608,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M145.104,121.942L143.134,120.734L143.216,123H142.154L142.242,120.734L140.249,121.95L139.747,121.003L141.788,119.992L139.754,118.974L140.256,118.058L142.242,119.25L142.159,117H143.213L143.133,119.258L145.11,118.058L145.629,118.974L143.571,120L145.63,121.011L145.104,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M155.599,121.942L153.63,120.734L153.712,123H152.65L152.738,120.734L150.745,121.95L150.242,121.003L152.284,119.992L150.249,118.974L150.751,118.058L152.737,119.25L152.655,117H153.709L153.629,119.258L155.606,118.058L156.125,118.974L154.067,120L156.126,121.011L155.599,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M171.988,121.942L170.018,120.734L170.101,123H169.039L169.127,120.734L167.134,121.95L166.631,121.003L168.673,119.992L166.638,118.974L167.14,118.058L169.126,119.25L169.044,117H170.098L170.018,119.258L171.994,118.058L172.513,118.974L170.456,120L172.515,121.011L171.988,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M182.484,121.942L180.514,120.734L180.597,123H179.535L179.623,120.734L177.63,121.95L177.127,121.003L179.169,119.992L177.134,118.974L177.636,118.058L179.622,119.25L179.54,117H180.593L180.514,119.258L182.49,118.058L183.009,118.974L180.952,120L183.01,121.011L182.484,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M192.98,121.942L191.01,120.734L191.092,123H190.031L190.119,120.734L188.126,121.95L187.623,121.003L189.665,119.992L187.63,118.974L188.132,118.058L190.118,119.25L190.036,117H191.089L191.009,119.258L192.986,118.058L193.505,118.974L191.447,120L193.506,121.011L192.98,121.942Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M203.476,121.942L201.506,120.734L201.588,123H200.527L200.615,120.734L198.622,121.95L198.119,121.003L200.16,119.992L198.126,118.974L198.628,118.058L200.614,119.25L200.532,117H201.585L201.505,119.258L203.482,118.058L204.001,118.974L201.943,120L204.002,121.011L203.476,121.942Z" />
<path
android:fillColor="#ffffff"
android:pathData="M26.483,30.4L26.483,36.64L29.382,36.64C29.494,36.639 29.602,36.596 29.681,36.522C29.761,36.447 29.806,36.346 29.807,36.241L29.807,30.4L26.483,30.4Z" />
<path
android:fillColor="#ffffff"
android:pathData="M20,36.241C20.001,36.346 20.046,36.447 20.126,36.522C20.205,36.596 20.313,36.639 20.425,36.64L23.324,36.64L23.324,30.4L20,30.4L20,36.241Z" />
<path
android:fillColor="#ffffff"
android:pathData="M29.807,27.2L29.807,24.409C29.807,24.301 29.763,24.197 29.685,24.12C29.606,24.043 29.5,24 29.389,24L20.418,24C20.307,24 20.201,24.043 20.123,24.12C20.044,24.197 20,24.301 20,24.409L20,27.2L29.807,27.2Z" />
<path
android:fillColor="#ffffff"
android:pathData="M45.32,28.163C44.719,27.921 44.068,27.812 43.416,27.846C42.555,27.844 41.696,27.934 40.856,28.113L40.856,29.532C41.526,29.455 42.199,29.412 42.874,29.4C43.236,29.385 43.598,29.418 43.949,29.5C44.062,29.532 44.164,29.59 44.247,29.669C44.33,29.748 44.392,29.846 44.425,29.953C44.531,30.334 44.576,30.728 44.558,31.122L44.558,31.539C43.764,31.498 43.131,31.476 42.698,31.476C42.236,31.454 41.775,31.539 41.356,31.725C41.025,31.903 40.779,32.195 40.666,32.54C40.509,33.044 40.438,33.569 40.456,34.095C40.398,34.771 40.564,35.448 40.932,36.029C41.127,36.243 41.372,36.409 41.648,36.513C41.924,36.618 42.222,36.659 42.517,36.632C42.864,36.632 43.945,36.605 44.421,36.007C44.499,35.912 44.566,35.808 44.62,35.699L44.673,36.496L46.424,36.496L46.424,31.222C46.45,30.539 46.371,29.857 46.191,29.196C46.127,28.977 46.017,28.772 45.867,28.594C45.717,28.417 45.531,28.27 45.32,28.163ZM44.54,33.256C44.589,33.736 44.503,34.219 44.292,34.656C44.189,34.783 44.048,34.876 43.887,34.924C43.706,34.972 43.519,34.995 43.331,34.992C43.037,35.028 42.74,34.953 42.503,34.783C42.343,34.523 42.277,34.221 42.312,33.922C42.302,33.681 42.327,33.44 42.388,33.206C42.407,33.139 42.44,33.076 42.486,33.021C42.532,32.966 42.59,32.921 42.655,32.889C42.834,32.821 43.028,32.791 43.221,32.803L44.54,32.803L44.54,33.256Z" />
<path
android:fillColor="#ffffff"
android:pathData="M60.474,28.645C60.438,28.584 60.398,28.526 60.354,28.471C59.777,27.736 58.401,27.846 58.401,27.846C57.946,27.832 57.499,27.964 57.126,28.222C56.75,28.573 56.507,29.042 56.438,29.55C56.168,31.328 56.168,33.136 56.438,34.915C56.504,35.417 56.74,35.882 57.108,36.233C57.509,36.52 58,36.659 58.493,36.623C58.493,36.623 58.521,36.623 58.539,36.623C58.539,36.623 59.782,36.72 60.386,36.104C60.398,36.607 60.364,37.11 60.285,37.606C60.264,37.738 60.212,37.864 60.131,37.971C60.051,38.078 59.945,38.164 59.823,38.222C59.439,38.351 59.033,38.406 58.627,38.382L56.72,38.35L56.72,39.788C57.071,39.853 57.426,39.9 57.782,39.93C58.221,39.972 58.636,39.99 59.034,39.99C59.773,40.044 60.512,39.884 61.162,39.53C61.39,39.374 61.583,39.174 61.732,38.943C61.88,38.71 61.98,38.451 62.026,38.18C62.174,37.297 62.237,36.402 62.215,35.507L62.215,27.979L60.516,27.979L60.474,28.645ZM60.262,34.221C60.233,34.452 60.118,34.664 59.938,34.813C59.729,34.939 59.486,34.999 59.241,34.984C58.968,35.006 58.694,34.95 58.452,34.823C58.263,34.636 58.153,34.384 58.147,34.12C58.042,32.876 58.042,31.625 58.147,30.381C58.153,30.125 58.259,29.882 58.442,29.701C58.69,29.579 58.966,29.526 59.241,29.55C59.502,29.529 59.761,29.599 59.975,29.747C60.166,29.947 60.28,30.207 60.299,30.482C60.369,31.072 60.396,31.666 60.382,32.26C60.405,32.916 60.364,33.573 60.262,34.221Z" />
<path
android:fillColor="#ffffff"
android:pathData="M69.304,28.124C68.702,27.787 68.01,27.636 67.319,27.691C66.601,27.642 65.884,27.802 65.259,28.152C64.804,28.473 64.497,28.959 64.408,29.502C64.259,30.388 64.196,31.286 64.219,32.184C64.195,33.083 64.261,33.983 64.417,34.87C64.465,35.139 64.568,35.395 64.72,35.623C64.872,35.851 65.07,36.046 65.301,36.197C65.961,36.531 66.701,36.681 67.441,36.631C67.913,36.63 68.384,36.603 68.852,36.552C69.241,36.517 69.626,36.453 70.005,36.359L70.005,34.916C69.139,35.004 68.429,35.05 67.874,35.05C67.464,35.074 67.053,35.025 66.66,34.907C66.54,34.857 66.433,34.778 66.351,34.677C66.269,34.577 66.214,34.458 66.19,34.331C66.106,33.822 66.071,33.307 66.087,32.792L70.193,32.792L70.193,32.165C70.215,31.241 70.167,30.317 70.047,29.4C69.98,28.896 69.713,28.438 69.304,28.124ZM68.41,31.428L66.087,31.428C66.086,30.95 66.119,30.473 66.186,30C66.195,29.887 66.228,29.777 66.283,29.678C66.338,29.579 66.414,29.492 66.505,29.424C66.988,29.233 67.527,29.233 68.01,29.424C68.1,29.494 68.173,29.583 68.227,29.682C68.28,29.782 68.312,29.892 68.321,30.004C68.394,30.475 68.424,30.952 68.41,31.428Z" />
<path
android:fillColor="#ffffff"
android:pathData="M81.818,29.12C81.739,28.745 81.527,28.406 81.218,28.161C80.849,27.928 80.409,27.817 79.966,27.844C79.966,27.844 79.924,27.844 79.895,27.844C79.895,27.844 78.467,27.767 77.877,28.464C77.813,28.542 77.755,28.625 77.705,28.713C77.61,28.478 77.445,28.275 77.229,28.129C76.858,27.924 76.431,27.826 76.001,27.848C76.001,27.848 74.573,27.767 73.983,28.468C73.917,28.545 73.86,28.629 73.812,28.717L73.764,27.984L72.027,27.984L72.027,36.48L73.931,36.48L73.931,32.051C73.921,31.52 73.942,30.988 73.993,30.459C74.005,30.198 74.107,29.949 74.283,29.749C74.493,29.584 74.764,29.506 75.035,29.531C75.276,29.513 75.518,29.557 75.735,29.658C75.815,29.731 75.879,29.819 75.923,29.916C75.967,30.013 75.991,30.118 75.992,30.224C76.042,30.747 76.063,31.272 76.054,31.798L76.054,36.48L77.958,36.48L77.958,32.051C77.958,31.314 77.958,30.785 78.015,30.459C78.028,30.197 78.133,29.946 78.315,29.749C78.53,29.586 78.802,29.508 79.076,29.531C79.319,29.513 79.562,29.557 79.781,29.658C79.86,29.732 79.923,29.82 79.966,29.917C80.01,30.014 80.033,30.118 80.033,30.224C80.086,30.747 80.106,31.272 80.095,31.798L80.095,36.48L81.999,36.48L81.999,30.884C82.009,30.291 81.949,29.699 81.818,29.12Z" />
<path
android:fillColor="#ffffff"
android:pathData="M53.781,28.171C53.383,27.928 52.913,27.814 52.44,27.845C52.44,27.845 52.402,27.845 52.382,27.845C52.382,27.845 50.936,27.773 50.343,28.465C50.301,28.514 50.262,28.565 50.227,28.619L50.184,27.981L48.424,27.981L48.424,36.48L50.353,36.48L50.353,32.049C50.342,31.521 50.363,30.992 50.415,30.465C50.429,30.203 50.538,29.952 50.724,29.755C50.957,29.584 51.25,29.503 51.543,29.529C51.8,29.511 52.057,29.555 52.291,29.655C52.378,29.728 52.448,29.816 52.497,29.915C52.546,30.014 52.572,30.121 52.575,30.23C52.63,30.75 52.653,31.273 52.643,31.796L52.643,36.48L54.571,36.48L54.571,30.963C54.587,30.356 54.533,29.75 54.412,29.153C54.333,28.765 54.109,28.417 53.781,28.171Z" />
<path
android:fillColor="#ffffff"
android:pathData="M38.128,25.44L36.175,25.44L36.175,27.877L34.96,27.877L34.96,29.434L36.175,29.434L36.175,36.48L38.128,36.48L38.128,29.434L39.614,29.434L39.614,27.877L38.128,27.877L38.128,25.44Z" />
</vector>

View file

@ -0,0 +1,108 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="214dp"
android:height="130dp"
android:viewportWidth="214"
android:viewportHeight="130">
<path
android:fillColor="#E4E4E4"
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z" />
<path
android:fillColor="#00000000"
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z"
android:strokeWidth="0.5">
<aapt:attr name="android:strokeColor">
<gradient
android:endX="216.304"
android:endY="122.149"
android:startX="-1.98014"
android:startY="-1.3567"
android:type="linear">
<item
android:color="#FFDCDCDC"
android:offset="0" />
<item
android:color="#FFDEDEDE"
android:offset="0.9999" />
<item
android:color="#00000000"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#707070"
android:pathData="M25.274,26.4L25.274,32.64L28.079,32.64C28.188,32.639 28.292,32.596 28.369,32.522C28.446,32.447 28.49,32.346 28.491,32.241L28.491,26.4L25.274,26.4Z" />
<path
android:fillColor="#707070"
android:pathData="M19,32.241C19.001,32.346 19.045,32.447 19.122,32.522C19.199,32.596 19.303,32.639 19.412,32.64L22.217,32.64L22.217,26.4L19,26.4L19,32.241Z" />
<path
android:fillColor="#707070"
android:pathData="M28.491,23.2L28.491,20.409C28.491,20.301 28.448,20.197 28.372,20.12C28.296,20.043 28.194,20 28.086,20L19.405,20C19.297,20 19.195,20.043 19.119,20.12C19.043,20.197 19,20.301 19,20.409L19,23.2L28.491,23.2Z" />
<path
android:fillColor="#707070"
android:pathData="M43.503,24.163C42.922,23.921 42.292,23.812 41.661,23.846C40.827,23.844 39.996,23.934 39.183,24.113L39.183,25.532C39.832,25.455 40.483,25.412 41.136,25.4C41.486,25.385 41.836,25.418 42.177,25.5C42.286,25.532 42.385,25.59 42.465,25.669C42.546,25.748 42.605,25.846 42.637,25.953C42.74,26.334 42.783,26.728 42.766,27.122L42.766,27.539C41.997,27.498 41.385,27.476 40.965,27.476C40.519,27.454 40.073,27.539 39.667,27.725C39.347,27.903 39.109,28.195 38.999,28.54C38.847,29.044 38.779,29.569 38.796,30.094C38.74,30.771 38.901,31.448 39.257,32.029C39.445,32.243 39.683,32.409 39.95,32.513C40.216,32.618 40.505,32.659 40.791,32.632C41.127,32.632 42.172,32.605 42.633,32.007C42.709,31.912 42.774,31.808 42.826,31.698L42.877,32.496L44.572,32.496L44.572,27.222C44.596,26.539 44.52,25.857 44.346,25.196C44.284,24.977 44.178,24.772 44.033,24.594C43.888,24.417 43.708,24.27 43.503,24.163ZM42.748,29.256C42.796,29.736 42.713,30.219 42.508,30.656C42.409,30.782 42.272,30.876 42.117,30.924C41.942,30.972 41.76,30.995 41.578,30.992C41.294,31.028 41.006,30.953 40.777,30.783C40.623,30.523 40.558,30.221 40.592,29.922C40.582,29.681 40.607,29.44 40.666,29.206C40.684,29.139 40.716,29.075 40.761,29.021C40.805,28.966 40.861,28.921 40.924,28.889C41.098,28.821 41.285,28.791 41.472,28.803L42.748,28.803L42.748,29.256Z" />
<path
android:fillColor="#707070"
android:pathData="M58.169,24.645C58.133,24.584 58.095,24.526 58.053,24.471C57.494,23.736 56.162,23.846 56.162,23.846C55.722,23.831 55.29,23.964 54.929,24.222C54.564,24.573 54.329,25.042 54.263,25.55C54.001,27.328 54.001,29.136 54.263,30.914C54.326,31.417 54.555,31.882 54.911,32.233C55.299,32.52 55.774,32.659 56.251,32.623C56.251,32.623 56.278,32.623 56.296,32.623C56.296,32.623 57.498,32.72 58.084,32.104C58.095,32.607 58.062,33.11 57.985,33.606C57.966,33.738 57.915,33.864 57.837,33.971C57.759,34.078 57.656,34.164 57.539,34.222C57.166,34.351 56.773,34.406 56.381,34.382L54.535,34.35L54.535,35.788C54.876,35.853 55.219,35.9 55.563,35.93C55.988,35.972 56.39,35.99 56.774,35.99C57.49,36.044 58.205,35.884 58.834,35.53C59.055,35.374 59.242,35.174 59.386,34.943C59.529,34.71 59.626,34.451 59.67,34.18C59.813,33.297 59.875,32.402 59.853,31.507L59.853,23.979L58.209,23.979L58.169,24.645ZM57.963,30.221C57.936,30.452 57.824,30.664 57.65,30.813C57.447,30.94 57.212,30.999 56.975,30.983C56.711,31.006 56.446,30.95 56.211,30.823C56.028,30.636 55.923,30.385 55.916,30.12C55.815,28.876 55.815,27.625 55.916,26.381C55.923,26.125 56.025,25.882 56.202,25.701C56.442,25.579 56.709,25.526 56.975,25.55C57.227,25.529 57.479,25.599 57.686,25.747C57.87,25.947 57.981,26.207 57.999,26.482C58.066,27.072 58.093,27.666 58.079,28.26C58.101,28.916 58.062,29.573 57.963,30.221Z" />
<path
android:fillColor="#707070"
android:pathData="M66.713,24.124C66.131,23.787 65.461,23.636 64.793,23.691C64.098,23.642 63.404,23.802 62.799,24.152C62.358,24.473 62.062,24.959 61.975,25.502C61.831,26.388 61.77,27.286 61.793,28.184C61.769,29.083 61.833,29.983 61.984,30.87C62.031,31.139 62.131,31.395 62.278,31.623C62.425,31.851 62.616,32.046 62.84,32.197C63.478,32.531 64.194,32.681 64.911,32.631C65.367,32.63 65.823,32.603 66.276,32.552C66.653,32.517 67.025,32.453 67.392,32.359L67.392,30.916C66.554,31.004 65.867,31.05 65.33,31.05C64.933,31.074 64.535,31.025 64.155,30.907C64.038,30.857 63.936,30.778 63.856,30.677C63.777,30.577 63.723,30.458 63.7,30.331C63.618,29.823 63.585,29.307 63.6,28.792L67.574,28.792L67.574,28.165C67.596,27.241 67.548,26.317 67.433,25.4C67.368,24.896 67.109,24.438 66.713,24.124ZM65.849,27.428L63.6,27.428C63.6,26.95 63.632,26.473 63.696,26C63.704,25.887 63.736,25.777 63.79,25.678C63.843,25.579 63.917,25.492 64.005,25.424C64.473,25.233 64.994,25.233 65.462,25.424C65.548,25.494 65.619,25.583 65.671,25.683C65.723,25.782 65.754,25.892 65.762,26.004C65.833,26.475 65.862,26.952 65.849,27.428Z" />
<path
android:fillColor="#707070"
android:pathData="M78.824,25.12C78.747,24.744 78.542,24.406 78.243,24.161C77.886,23.928 77.46,23.817 77.032,23.844C77.032,23.844 76.991,23.844 76.963,23.844C76.963,23.844 75.581,23.767 75.01,24.464C74.948,24.542 74.892,24.625 74.844,24.712C74.752,24.478 74.592,24.275 74.383,24.129C74.024,23.923 73.61,23.826 73.195,23.848C73.195,23.848 71.813,23.767 71.242,24.468C71.178,24.545 71.122,24.629 71.076,24.717L71.03,23.984L69.349,23.984L69.349,32.48L71.191,32.48L71.191,28.051C71.182,27.52 71.201,26.988 71.251,26.459C71.263,26.198 71.362,25.949 71.532,25.749C71.736,25.584 71.997,25.506 72.26,25.531C72.493,25.513 72.727,25.557 72.937,25.658C73.015,25.731 73.076,25.819 73.119,25.916C73.162,26.013 73.185,26.118 73.186,26.223C73.234,26.747 73.254,27.272 73.245,27.798L73.245,32.48L75.088,32.48L75.088,28.051C75.088,27.314 75.088,26.785 75.143,26.459C75.156,26.197 75.258,25.946 75.434,25.749C75.642,25.586 75.906,25.508 76.171,25.531C76.405,25.513 76.641,25.557 76.852,25.658C76.929,25.732 76.99,25.82 77.032,25.917C77.074,26.014 77.096,26.118 77.096,26.223C77.147,26.747 77.167,27.272 77.156,27.798L77.156,32.48L78.999,32.48L78.999,26.884C79.009,26.291 78.951,25.699 78.824,25.12Z" />
<path
android:fillColor="#707070"
android:pathData="M51.691,24.171C51.307,23.928 50.851,23.814 50.394,23.845C50.394,23.845 50.356,23.845 50.338,23.845C50.338,23.845 48.938,23.773 48.364,24.465C48.323,24.514 48.286,24.565 48.252,24.619L48.21,23.981L46.507,23.981L46.507,32.48L48.373,32.48L48.373,28.049C48.363,27.521 48.384,26.992 48.434,26.465C48.448,26.203 48.553,25.952 48.733,25.755C48.958,25.584 49.242,25.503 49.526,25.528C49.774,25.511 50.023,25.555 50.249,25.655C50.333,25.728 50.401,25.816 50.448,25.915C50.496,26.014 50.521,26.121 50.524,26.23C50.578,26.75 50.6,27.273 50.59,27.796L50.59,32.48L52.456,32.48L52.456,26.963C52.471,26.356 52.419,25.749 52.302,25.153C52.226,24.765 52.009,24.417 51.691,24.171Z" />
<path
android:fillColor="#707070"
android:pathData="M36.543,21.44L34.653,21.44L34.653,23.877L33.477,23.877L33.477,25.434L34.653,25.434L34.653,32.48L36.543,32.48L36.543,25.434L37.981,25.434L37.981,23.877L36.543,23.877L36.543,21.44Z" />
<path
android:fillColor="#707070"
android:pathData="M23.463,105.118L21.826,104.112L21.904,106H21.024L21.087,104.112L19.441,105.125L19.02,104.335L20.707,103.493L19.016,102.645L19.428,101.882L21.08,102.875L21.001,101H21.874L21.818,102.882L23.451,101.882L23.885,102.645L22.184,103.5L23.895,104.342L23.463,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M32.161,105.118L30.524,104.112L30.602,106H29.722L29.785,104.112L28.139,105.125L27.718,104.335L29.405,103.493L27.715,102.645L28.126,101.882L29.778,102.875L29.699,101H30.573L30.517,102.882L32.149,101.882L32.583,102.645L30.883,103.5L32.593,104.342L32.161,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M40.86,105.118L39.222,104.112L39.3,106H38.42L38.483,104.112L36.837,105.125L36.416,104.335L38.104,103.493L36.413,102.645L36.825,101.882L38.476,102.875L38.398,101H39.271L39.215,102.882L40.847,101.882L41.282,102.645L39.581,103.5L41.292,104.342L40.86,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M49.558,105.118L47.92,104.112L47.998,106H47.119L47.181,104.112L45.535,105.125L45.114,104.335L46.802,103.493L45.111,102.645L45.523,101.882L47.174,102.875L47.096,101H47.969L47.913,102.882L49.546,101.882L49.98,102.645L48.279,103.5L49.99,104.342L49.558,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M63.14,105.118L61.502,104.112L61.58,106H60.701L60.763,104.112L59.117,105.125L58.696,104.335L60.384,103.493L58.693,102.645L59.105,101.882L60.756,102.875L60.678,101H61.551L61.495,102.882L63.128,101.882L63.562,102.645L61.861,103.5L63.572,104.342L63.14,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M71.838,105.118L70.2,104.112L70.279,106H69.399L69.462,104.112L67.815,105.125L67.394,104.335L69.082,103.493L67.391,102.645L67.803,101.882L69.454,102.875L69.376,101H70.249L70.193,102.882L71.826,101.882L72.26,102.645L70.559,103.5L72.27,104.342L71.838,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M80.536,105.118L78.898,104.112L78.977,106H78.097L78.16,104.112L76.514,105.125L76.093,104.335L77.78,103.493L76.089,102.645L76.501,101.882L78.152,102.875L78.074,101H78.947L78.891,102.882L80.524,101.882L80.958,102.645L79.257,103.5L80.968,104.342L80.536,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M89.234,105.118L87.597,104.112L87.675,106H86.795L86.858,104.112L85.212,105.125L84.791,104.335L86.478,103.493L84.788,102.645L85.2,101.882L86.851,102.875L86.772,101H87.646L87.59,102.882L89.222,101.882L89.657,102.645L87.956,103.5L89.666,104.342L89.234,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M102.816,105.118L101.179,104.112L101.257,106H100.377L100.44,104.112L98.794,105.125L98.373,104.335L100.06,103.493L98.369,102.645L98.781,101.882L100.433,102.875L100.354,101H101.227L101.171,102.882L102.804,101.882L103.238,102.645L101.538,103.5L103.248,104.342L102.816,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M111.515,105.118L109.877,104.112L109.955,106H109.075L109.138,104.112L107.492,105.125L107.071,104.335L108.758,103.493L107.068,102.645L107.48,101.882L109.131,102.875L109.053,101H109.926L109.87,102.882L111.502,101.882L111.937,102.645L110.236,103.5L111.947,104.342L111.515,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M120.213,105.118L118.575,104.112L118.653,106H117.774L117.836,104.112L116.19,105.125L115.769,104.335L117.457,103.493L115.766,102.645L116.178,101.882L117.829,102.875L117.751,101H118.624L118.568,102.882L120.201,101.882L120.635,102.645L118.934,103.5L120.645,104.342L120.213,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M128.911,105.118L127.273,104.112L127.352,106H126.472L126.535,104.112L124.888,105.125L124.467,104.335L126.155,103.493L124.464,102.645L124.876,101.882L126.527,102.875L126.449,101H127.322L127.266,102.882L128.899,101.882L129.333,102.645L127.632,103.5L129.343,104.342L128.911,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M142.493,105.118L140.855,104.112L140.933,106H140.054L140.116,104.112L138.47,105.125L138.049,104.335L139.737,103.493L138.046,102.645L138.458,101.882L140.109,102.875L140.031,101H140.904L140.848,102.882L142.481,101.882L142.915,102.645L141.214,103.5L142.925,104.342L142.493,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M151.191,105.118L149.553,104.112L149.632,106H148.752L148.815,104.112L147.169,105.125L146.748,104.335L148.435,103.493L146.744,102.645L147.156,101.882L148.807,102.875L148.729,101H149.602L149.546,102.882L151.179,101.882L151.613,102.645L149.912,103.5L151.623,104.342L151.191,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M159.889,105.118L158.252,104.112L158.33,106H157.45L157.513,104.112L155.867,105.125L155.446,104.335L157.133,103.493L155.443,102.645L155.854,101.882L157.506,102.875L157.427,101H158.3L158.244,102.882L159.877,101.882L160.311,102.645L158.611,103.5L160.321,104.342L159.889,105.118Z" />
<path
android:fillColor="#707070"
android:pathData="M168.588,105.118L166.95,104.112L167.028,106H166.148L166.211,104.112L164.565,105.125L164.144,104.335L165.831,103.493L164.141,102.645L164.553,101.882L166.204,102.875L166.126,101H166.999L166.943,102.882L168.575,101.882L169.01,102.645L167.309,103.5L169.02,104.342L168.588,105.118Z" />
</vector>

View file

@ -1,102 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="214dp"
android:height="130dp"
android:viewportWidth="214"
android:viewportHeight="130">
<path
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z"
android:fillColor="#E4E4E4"/>
<path
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z"
android:strokeWidth="0.5"
android:fillColor="#00000000">
<aapt:attr name="android:strokeColor">
<gradient
android:startY="-1.3567"
android:startX="-1.98014"
android:endY="122.149"
android:endX="216.304"
android:type="linear">
<item android:offset="0" android:color="#FFDCDCDC"/>
<item android:offset="0.9999" android:color="#FFDEDEDE"/>
<item android:offset="1" android:color="#00000000"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M25.274,26.4L25.274,32.64L28.079,32.64C28.188,32.639 28.292,32.596 28.369,32.522C28.446,32.447 28.49,32.346 28.491,32.241L28.491,26.4L25.274,26.4Z"
android:fillColor="#707070"/>
<path
android:pathData="M19,32.241C19.001,32.346 19.045,32.447 19.122,32.522C19.199,32.596 19.303,32.639 19.412,32.64L22.217,32.64L22.217,26.4L19,26.4L19,32.241Z"
android:fillColor="#707070"/>
<path
android:pathData="M28.491,23.2L28.491,20.409C28.491,20.301 28.448,20.197 28.372,20.12C28.296,20.043 28.194,20 28.086,20L19.405,20C19.297,20 19.195,20.043 19.119,20.12C19.043,20.197 19,20.301 19,20.409L19,23.2L28.491,23.2Z"
android:fillColor="#707070"/>
<path
android:pathData="M43.503,24.163C42.922,23.921 42.292,23.812 41.661,23.846C40.827,23.844 39.996,23.934 39.183,24.113L39.183,25.532C39.832,25.455 40.483,25.412 41.136,25.4C41.486,25.385 41.836,25.418 42.177,25.5C42.286,25.532 42.385,25.59 42.465,25.669C42.546,25.748 42.605,25.846 42.637,25.953C42.74,26.334 42.783,26.728 42.766,27.122L42.766,27.539C41.997,27.498 41.385,27.476 40.965,27.476C40.519,27.454 40.073,27.539 39.667,27.725C39.347,27.903 39.109,28.195 38.999,28.54C38.847,29.044 38.779,29.569 38.796,30.094C38.74,30.771 38.901,31.448 39.257,32.029C39.445,32.243 39.683,32.409 39.95,32.513C40.216,32.618 40.505,32.659 40.791,32.632C41.127,32.632 42.172,32.605 42.633,32.007C42.709,31.912 42.774,31.808 42.826,31.698L42.877,32.496L44.572,32.496L44.572,27.222C44.596,26.539 44.52,25.857 44.346,25.196C44.284,24.977 44.178,24.772 44.033,24.594C43.888,24.417 43.708,24.27 43.503,24.163ZM42.748,29.256C42.796,29.736 42.713,30.219 42.508,30.656C42.409,30.782 42.272,30.876 42.117,30.924C41.942,30.972 41.76,30.995 41.578,30.992C41.294,31.028 41.006,30.953 40.777,30.783C40.623,30.523 40.558,30.221 40.592,29.922C40.582,29.681 40.607,29.44 40.666,29.206C40.684,29.139 40.716,29.075 40.761,29.021C40.805,28.966 40.861,28.921 40.924,28.889C41.098,28.821 41.285,28.791 41.472,28.803L42.748,28.803L42.748,29.256Z"
android:fillColor="#707070"/>
<path
android:pathData="M58.169,24.645C58.133,24.584 58.095,24.526 58.053,24.471C57.494,23.736 56.162,23.846 56.162,23.846C55.722,23.831 55.29,23.964 54.929,24.222C54.564,24.573 54.329,25.042 54.263,25.55C54.001,27.328 54.001,29.136 54.263,30.914C54.326,31.417 54.555,31.882 54.911,32.233C55.299,32.52 55.774,32.659 56.251,32.623C56.251,32.623 56.278,32.623 56.296,32.623C56.296,32.623 57.498,32.72 58.084,32.104C58.095,32.607 58.062,33.11 57.985,33.606C57.966,33.738 57.915,33.864 57.837,33.971C57.759,34.078 57.656,34.164 57.539,34.222C57.166,34.351 56.773,34.406 56.381,34.382L54.535,34.35L54.535,35.788C54.876,35.853 55.219,35.9 55.563,35.93C55.988,35.972 56.39,35.99 56.774,35.99C57.49,36.044 58.205,35.884 58.834,35.53C59.055,35.374 59.242,35.174 59.386,34.943C59.529,34.71 59.626,34.451 59.67,34.18C59.813,33.297 59.875,32.402 59.853,31.507L59.853,23.979L58.209,23.979L58.169,24.645ZM57.963,30.221C57.936,30.452 57.824,30.664 57.65,30.813C57.447,30.94 57.212,30.999 56.975,30.983C56.711,31.006 56.446,30.95 56.211,30.823C56.028,30.636 55.923,30.385 55.916,30.12C55.815,28.876 55.815,27.625 55.916,26.381C55.923,26.125 56.025,25.882 56.202,25.701C56.442,25.579 56.709,25.526 56.975,25.55C57.227,25.529 57.479,25.599 57.686,25.747C57.87,25.947 57.981,26.207 57.999,26.482C58.066,27.072 58.093,27.666 58.079,28.26C58.101,28.916 58.062,29.573 57.963,30.221Z"
android:fillColor="#707070"/>
<path
android:pathData="M66.713,24.124C66.131,23.787 65.461,23.636 64.793,23.691C64.098,23.642 63.404,23.802 62.799,24.152C62.358,24.473 62.062,24.959 61.975,25.502C61.831,26.388 61.77,27.286 61.793,28.184C61.769,29.083 61.833,29.983 61.984,30.87C62.031,31.139 62.131,31.395 62.278,31.623C62.425,31.851 62.616,32.046 62.84,32.197C63.478,32.531 64.194,32.681 64.911,32.631C65.367,32.63 65.823,32.603 66.276,32.552C66.653,32.517 67.025,32.453 67.392,32.359L67.392,30.916C66.554,31.004 65.867,31.05 65.33,31.05C64.933,31.074 64.535,31.025 64.155,30.907C64.038,30.857 63.936,30.778 63.856,30.677C63.777,30.577 63.723,30.458 63.7,30.331C63.618,29.823 63.585,29.307 63.6,28.792L67.574,28.792L67.574,28.165C67.596,27.241 67.548,26.317 67.433,25.4C67.368,24.896 67.109,24.438 66.713,24.124ZM65.849,27.428L63.6,27.428C63.6,26.95 63.632,26.473 63.696,26C63.704,25.887 63.736,25.777 63.79,25.678C63.843,25.579 63.917,25.492 64.005,25.424C64.473,25.233 64.994,25.233 65.462,25.424C65.548,25.494 65.619,25.583 65.671,25.683C65.723,25.782 65.754,25.892 65.762,26.004C65.833,26.475 65.862,26.952 65.849,27.428Z"
android:fillColor="#707070"/>
<path
android:pathData="M78.824,25.12C78.747,24.744 78.542,24.406 78.243,24.161C77.886,23.928 77.46,23.817 77.032,23.844C77.032,23.844 76.991,23.844 76.963,23.844C76.963,23.844 75.581,23.767 75.01,24.464C74.948,24.542 74.892,24.625 74.844,24.712C74.752,24.478 74.592,24.275 74.383,24.129C74.024,23.923 73.61,23.826 73.195,23.848C73.195,23.848 71.813,23.767 71.242,24.468C71.178,24.545 71.122,24.629 71.076,24.717L71.03,23.984L69.349,23.984L69.349,32.48L71.191,32.48L71.191,28.051C71.182,27.52 71.201,26.988 71.251,26.459C71.263,26.198 71.362,25.949 71.532,25.749C71.736,25.584 71.997,25.506 72.26,25.531C72.493,25.513 72.727,25.557 72.937,25.658C73.015,25.731 73.076,25.819 73.119,25.916C73.162,26.013 73.185,26.118 73.186,26.223C73.234,26.747 73.254,27.272 73.245,27.798L73.245,32.48L75.088,32.48L75.088,28.051C75.088,27.314 75.088,26.785 75.143,26.459C75.156,26.197 75.258,25.946 75.434,25.749C75.642,25.586 75.906,25.508 76.171,25.531C76.405,25.513 76.641,25.557 76.852,25.658C76.929,25.732 76.99,25.82 77.032,25.917C77.074,26.014 77.096,26.118 77.096,26.223C77.147,26.747 77.167,27.272 77.156,27.798L77.156,32.48L78.999,32.48L78.999,26.884C79.009,26.291 78.951,25.699 78.824,25.12Z"
android:fillColor="#707070"/>
<path
android:pathData="M51.691,24.171C51.307,23.928 50.851,23.814 50.394,23.845C50.394,23.845 50.356,23.845 50.338,23.845C50.338,23.845 48.938,23.773 48.364,24.465C48.323,24.514 48.286,24.565 48.252,24.619L48.21,23.981L46.507,23.981L46.507,32.48L48.373,32.48L48.373,28.049C48.363,27.521 48.384,26.992 48.434,26.465C48.448,26.203 48.553,25.952 48.733,25.755C48.958,25.584 49.242,25.503 49.526,25.528C49.774,25.511 50.023,25.555 50.249,25.655C50.333,25.728 50.401,25.816 50.448,25.915C50.496,26.014 50.521,26.121 50.524,26.23C50.578,26.75 50.6,27.273 50.59,27.796L50.59,32.48L52.456,32.48L52.456,26.963C52.471,26.356 52.419,25.749 52.302,25.153C52.226,24.765 52.009,24.417 51.691,24.171Z"
android:fillColor="#707070"/>
<path
android:pathData="M36.543,21.44L34.653,21.44L34.653,23.877L33.477,23.877L33.477,25.434L34.653,25.434L34.653,32.48L36.543,32.48L36.543,25.434L37.981,25.434L37.981,23.877L36.543,23.877L36.543,21.44Z"
android:fillColor="#707070"/>
<path
android:pathData="M23.463,105.118L21.826,104.112L21.904,106H21.024L21.087,104.112L19.441,105.125L19.02,104.335L20.707,103.493L19.016,102.645L19.428,101.882L21.08,102.875L21.001,101H21.874L21.818,102.882L23.451,101.882L23.885,102.645L22.184,103.5L23.895,104.342L23.463,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M32.161,105.118L30.524,104.112L30.602,106H29.722L29.785,104.112L28.139,105.125L27.718,104.335L29.405,103.493L27.715,102.645L28.126,101.882L29.778,102.875L29.699,101H30.573L30.517,102.882L32.149,101.882L32.583,102.645L30.883,103.5L32.593,104.342L32.161,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M40.86,105.118L39.222,104.112L39.3,106H38.42L38.483,104.112L36.837,105.125L36.416,104.335L38.104,103.493L36.413,102.645L36.825,101.882L38.476,102.875L38.398,101H39.271L39.215,102.882L40.847,101.882L41.282,102.645L39.581,103.5L41.292,104.342L40.86,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M49.558,105.118L47.92,104.112L47.998,106H47.119L47.181,104.112L45.535,105.125L45.114,104.335L46.802,103.493L45.111,102.645L45.523,101.882L47.174,102.875L47.096,101H47.969L47.913,102.882L49.546,101.882L49.98,102.645L48.279,103.5L49.99,104.342L49.558,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M63.14,105.118L61.502,104.112L61.58,106H60.701L60.763,104.112L59.117,105.125L58.696,104.335L60.384,103.493L58.693,102.645L59.105,101.882L60.756,102.875L60.678,101H61.551L61.495,102.882L63.128,101.882L63.562,102.645L61.861,103.5L63.572,104.342L63.14,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M71.838,105.118L70.2,104.112L70.279,106H69.399L69.462,104.112L67.815,105.125L67.394,104.335L69.082,103.493L67.391,102.645L67.803,101.882L69.454,102.875L69.376,101H70.249L70.193,102.882L71.826,101.882L72.26,102.645L70.559,103.5L72.27,104.342L71.838,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M80.536,105.118L78.898,104.112L78.977,106H78.097L78.16,104.112L76.514,105.125L76.093,104.335L77.78,103.493L76.089,102.645L76.501,101.882L78.152,102.875L78.074,101H78.947L78.891,102.882L80.524,101.882L80.958,102.645L79.257,103.5L80.968,104.342L80.536,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M89.234,105.118L87.597,104.112L87.675,106H86.795L86.858,104.112L85.212,105.125L84.791,104.335L86.478,103.493L84.788,102.645L85.2,101.882L86.851,102.875L86.772,101H87.646L87.59,102.882L89.222,101.882L89.657,102.645L87.956,103.5L89.666,104.342L89.234,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M102.816,105.118L101.179,104.112L101.257,106H100.377L100.44,104.112L98.794,105.125L98.373,104.335L100.06,103.493L98.369,102.645L98.781,101.882L100.433,102.875L100.354,101H101.227L101.171,102.882L102.804,101.882L103.238,102.645L101.538,103.5L103.248,104.342L102.816,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M111.515,105.118L109.877,104.112L109.955,106H109.075L109.138,104.112L107.492,105.125L107.071,104.335L108.758,103.493L107.068,102.645L107.48,101.882L109.131,102.875L109.053,101H109.926L109.87,102.882L111.502,101.882L111.937,102.645L110.236,103.5L111.947,104.342L111.515,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M120.213,105.118L118.575,104.112L118.653,106H117.774L117.836,104.112L116.19,105.125L115.769,104.335L117.457,103.493L115.766,102.645L116.178,101.882L117.829,102.875L117.751,101H118.624L118.568,102.882L120.201,101.882L120.635,102.645L118.934,103.5L120.645,104.342L120.213,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M128.911,105.118L127.273,104.112L127.352,106H126.472L126.535,104.112L124.888,105.125L124.467,104.335L126.155,103.493L124.464,102.645L124.876,101.882L126.527,102.875L126.449,101H127.322L127.266,102.882L128.899,101.882L129.333,102.645L127.632,103.5L129.343,104.342L128.911,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M142.493,105.118L140.855,104.112L140.933,106H140.054L140.116,104.112L138.47,105.125L138.049,104.335L139.737,103.493L138.046,102.645L138.458,101.882L140.109,102.875L140.031,101H140.904L140.848,102.882L142.481,101.882L142.915,102.645L141.214,103.5L142.925,104.342L142.493,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M151.191,105.118L149.553,104.112L149.632,106H148.752L148.815,104.112L147.169,105.125L146.748,104.335L148.435,103.493L146.744,102.645L147.156,101.882L148.807,102.875L148.729,101H149.602L149.546,102.882L151.179,101.882L151.613,102.645L149.912,103.5L151.623,104.342L151.191,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M159.889,105.118L158.252,104.112L158.33,106H157.45L157.513,104.112L155.867,105.125L155.446,104.335L157.133,103.493L155.443,102.645L155.854,101.882L157.506,102.875L157.427,101H158.3L158.244,102.882L159.877,101.882L160.311,102.645L158.611,103.5L160.321,104.342L159.889,105.118Z"
android:fillColor="#707070"/>
<path
android:pathData="M168.588,105.118L166.95,104.112L167.028,106H166.148L166.211,104.112L164.565,105.125L164.144,104.335L165.831,103.493L164.141,102.645L164.553,101.882L166.204,102.875L166.126,101H166.999L166.943,102.882L168.575,101.882L169.01,102.645L167.309,103.5L169.02,104.342L168.588,105.118Z"
android:fillColor="#707070"/>
</vector>

View file

@ -1,118 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="249dp"
android:height="150dp"
android:viewportWidth="249"
android:viewportHeight="150">
<path
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z"
android:fillAlpha="0.03"/>
<path
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z"
android:fillAlpha="0.5">
<aapt:attr name="android:fillColor">
<gradient
android:gradientRadius="267.36932"
android:centerX="7.404067"
android:centerY="8.65384"
android:type="radial">
<item android:offset="0" android:color="#FFFFFFFF"/>
<item android:offset="1" android:color="#19FFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z"
android:fillColor="#1B1D1C"/>
<path
android:strokeWidth="1"
android:pathData="M10,0.5L238,0.5A9.499,9.501 134.998,0 1,247.501 10L247.517,140A9.499,9.501 134.998,0 1,238.019 149.5L10.019,149.5A9.499,9.501 134.998,0 1,0.517 140L0.501,10A9.499,9.501 134.998,0 1,10 0.5z"
android:fillColor="#00000000">
<aapt:attr name="android:strokeColor">
<gradient
android:startY="3.38345"
android:startX="2.844399"
android:endY="144.774"
android:endX="246.96196"
android:type="linear">
<item android:offset="0" android:color="#FF1A1A1A"/>
<item android:offset="1" android:color="#FF565656"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M28.359,121.942L26.389,120.734L26.472,123H25.41L25.498,120.734L23.505,121.95L23.002,121.003L25.044,119.992L23.009,118.974L23.511,118.058L25.497,119.25L25.415,117H26.468L26.389,119.258L28.365,118.058L28.884,118.974L26.826,120L28.885,121.011L28.359,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M38.855,121.942L36.885,120.734L36.967,123H35.906L35.994,120.734L34.001,121.95L33.498,121.003L35.54,119.992L33.505,118.974L34.007,118.058L35.993,119.25L35.911,117H36.964L36.884,119.258L38.861,118.058L39.38,118.974L37.322,120L39.381,121.011L38.855,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M49.351,121.942L47.381,120.734L47.463,123H46.402L46.49,120.734L44.497,121.95L43.994,121.003L46.035,119.992L44.001,118.974L44.503,118.058L46.489,119.25L46.407,117H47.46L47.38,119.258L49.357,118.058L49.876,118.974L47.818,120L49.877,121.011L49.351,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M59.847,121.942L57.877,120.734L57.959,123H56.897L56.985,120.734L54.993,121.95L54.49,121.003L56.531,119.992L54.497,118.974L54.999,118.058L56.985,119.25L56.902,117H57.956L57.876,119.258L59.853,118.058L60.372,118.974L58.314,120L60.373,121.011L59.847,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M76.235,121.942L74.266,120.734L74.348,123H73.286L73.374,120.734L71.381,121.95L70.879,121.003L72.92,119.992L70.885,118.974L71.387,118.058L73.373,119.25L73.291,117H74.345L74.265,119.258L76.242,118.058L76.761,118.974L74.703,120L76.762,121.011L76.235,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M86.731,121.942L84.761,120.734L84.844,123H83.782L83.87,120.734L81.877,121.95L81.374,121.003L83.416,119.992L81.381,118.974L81.883,118.058L83.869,119.25L83.787,117H84.841L84.761,119.258L86.737,118.058L87.257,118.974L85.199,120L87.258,121.011L86.731,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M97.227,121.942L95.257,120.734L95.34,123H94.278L94.366,120.734L92.373,121.95L91.87,121.003L93.912,119.992L91.877,118.974L92.379,118.058L94.365,119.25L94.283,117H95.337L95.257,119.258L97.233,118.058L97.752,118.974L95.695,120L97.753,121.011L97.227,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M107.723,121.942L105.753,120.734L105.835,123H104.774L104.862,120.734L102.869,121.95L102.366,121.003L104.408,119.992L102.373,118.974L102.875,118.058L104.861,119.25L104.779,117H105.832L105.752,119.258L107.729,118.058L108.248,118.974L106.19,120L108.249,121.011L107.723,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M124.112,121.942L122.142,120.734L122.224,123H121.163L121.251,120.734L119.258,121.95L118.755,121.003L120.796,119.992L118.762,118.974L119.264,118.058L121.25,119.25L121.168,117H122.221L122.141,119.258L124.118,118.058L124.637,118.974L122.579,120L124.638,121.011L124.112,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M134.608,121.942L132.638,120.734L132.72,123H131.659L131.746,120.734L129.754,121.95L129.251,121.003L131.292,119.992L129.258,118.974L129.76,118.058L131.746,119.25L131.664,117H132.717L132.637,119.258L134.614,118.058L135.133,118.974L133.075,120L135.134,121.011L134.608,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M145.104,121.942L143.134,120.734L143.216,123H142.154L142.242,120.734L140.249,121.95L139.747,121.003L141.788,119.992L139.754,118.974L140.256,118.058L142.242,119.25L142.159,117H143.213L143.133,119.258L145.11,118.058L145.629,118.974L143.571,120L145.63,121.011L145.104,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M155.599,121.942L153.63,120.734L153.712,123H152.65L152.738,120.734L150.745,121.95L150.242,121.003L152.284,119.992L150.249,118.974L150.751,118.058L152.737,119.25L152.655,117H153.709L153.629,119.258L155.606,118.058L156.125,118.974L154.067,120L156.126,121.011L155.599,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M171.988,121.942L170.018,120.734L170.101,123H169.039L169.127,120.734L167.134,121.95L166.631,121.003L168.673,119.992L166.638,118.974L167.14,118.058L169.126,119.25L169.044,117H170.098L170.018,119.258L171.994,118.058L172.513,118.974L170.456,120L172.515,121.011L171.988,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M182.484,121.942L180.514,120.734L180.597,123H179.535L179.623,120.734L177.63,121.95L177.127,121.003L179.169,119.992L177.134,118.974L177.636,118.058L179.622,119.25L179.54,117H180.593L180.514,119.258L182.49,118.058L183.009,118.974L180.952,120L183.01,121.011L182.484,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M192.98,121.942L191.01,120.734L191.092,123H190.031L190.119,120.734L188.126,121.95L187.623,121.003L189.665,119.992L187.63,118.974L188.132,118.058L190.118,119.25L190.036,117H191.089L191.009,119.258L192.986,118.058L193.505,118.974L191.447,120L193.506,121.011L192.98,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M203.476,121.942L201.506,120.734L201.588,123H200.527L200.615,120.734L198.622,121.95L198.119,121.003L200.16,119.992L198.126,118.974L198.628,118.058L200.614,119.25L200.532,117H201.585L201.505,119.258L203.482,118.058L204.001,118.974L201.943,120L204.002,121.011L203.476,121.942Z"
android:fillColor="#A5A5A5"/>
<path
android:pathData="M26.483,30.4L26.483,36.64L29.382,36.64C29.494,36.639 29.602,36.596 29.681,36.522C29.761,36.447 29.806,36.346 29.807,36.241L29.807,30.4L26.483,30.4Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M20,36.241C20.001,36.346 20.046,36.447 20.126,36.522C20.205,36.596 20.313,36.639 20.425,36.64L23.324,36.64L23.324,30.4L20,30.4L20,36.241Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M29.807,27.2L29.807,24.409C29.807,24.301 29.763,24.197 29.685,24.12C29.606,24.043 29.5,24 29.389,24L20.418,24C20.307,24 20.201,24.043 20.123,24.12C20.044,24.197 20,24.301 20,24.409L20,27.2L29.807,27.2Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M45.32,28.163C44.719,27.921 44.068,27.812 43.416,27.846C42.555,27.844 41.696,27.934 40.856,28.113L40.856,29.532C41.526,29.455 42.199,29.412 42.874,29.4C43.236,29.385 43.598,29.418 43.949,29.5C44.062,29.532 44.164,29.59 44.247,29.669C44.33,29.748 44.392,29.846 44.425,29.953C44.531,30.334 44.576,30.728 44.558,31.122L44.558,31.539C43.764,31.498 43.131,31.476 42.698,31.476C42.236,31.454 41.775,31.539 41.356,31.725C41.025,31.903 40.779,32.195 40.666,32.54C40.509,33.044 40.438,33.569 40.456,34.095C40.398,34.771 40.564,35.448 40.932,36.029C41.127,36.243 41.372,36.409 41.648,36.513C41.924,36.618 42.222,36.659 42.517,36.632C42.864,36.632 43.945,36.605 44.421,36.007C44.499,35.912 44.566,35.808 44.62,35.699L44.673,36.496L46.424,36.496L46.424,31.222C46.45,30.539 46.371,29.857 46.191,29.196C46.127,28.977 46.017,28.772 45.867,28.594C45.717,28.417 45.531,28.27 45.32,28.163ZM44.54,33.256C44.589,33.736 44.503,34.219 44.292,34.656C44.189,34.783 44.048,34.876 43.887,34.924C43.706,34.972 43.519,34.995 43.331,34.992C43.037,35.028 42.74,34.953 42.503,34.783C42.343,34.523 42.277,34.221 42.312,33.922C42.302,33.681 42.327,33.44 42.388,33.206C42.407,33.139 42.44,33.076 42.486,33.021C42.532,32.966 42.59,32.921 42.655,32.889C42.834,32.821 43.028,32.791 43.221,32.803L44.54,32.803L44.54,33.256Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M60.474,28.645C60.438,28.584 60.398,28.526 60.354,28.471C59.777,27.736 58.401,27.846 58.401,27.846C57.946,27.832 57.499,27.964 57.126,28.222C56.75,28.573 56.507,29.042 56.438,29.55C56.168,31.328 56.168,33.136 56.438,34.915C56.504,35.417 56.74,35.882 57.108,36.233C57.509,36.52 58,36.659 58.493,36.623C58.493,36.623 58.521,36.623 58.539,36.623C58.539,36.623 59.782,36.72 60.386,36.104C60.398,36.607 60.364,37.11 60.285,37.606C60.264,37.738 60.212,37.864 60.131,37.971C60.051,38.078 59.945,38.164 59.823,38.222C59.439,38.351 59.033,38.406 58.627,38.382L56.72,38.35L56.72,39.788C57.071,39.853 57.426,39.9 57.782,39.93C58.221,39.972 58.636,39.99 59.034,39.99C59.773,40.044 60.512,39.884 61.162,39.53C61.39,39.374 61.583,39.174 61.732,38.943C61.88,38.71 61.98,38.451 62.026,38.18C62.174,37.297 62.237,36.402 62.215,35.507L62.215,27.979L60.516,27.979L60.474,28.645ZM60.262,34.221C60.233,34.452 60.118,34.664 59.938,34.813C59.729,34.939 59.486,34.999 59.241,34.984C58.968,35.006 58.694,34.95 58.452,34.823C58.263,34.636 58.153,34.384 58.147,34.12C58.042,32.876 58.042,31.625 58.147,30.381C58.153,30.125 58.259,29.882 58.442,29.701C58.69,29.579 58.966,29.526 59.241,29.55C59.502,29.529 59.761,29.599 59.975,29.747C60.166,29.947 60.28,30.207 60.299,30.482C60.369,31.072 60.396,31.666 60.382,32.26C60.405,32.916 60.364,33.573 60.262,34.221Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M69.304,28.124C68.702,27.787 68.01,27.636 67.319,27.691C66.601,27.642 65.884,27.802 65.259,28.152C64.804,28.473 64.497,28.959 64.408,29.502C64.259,30.388 64.196,31.286 64.219,32.184C64.195,33.083 64.261,33.983 64.417,34.87C64.465,35.139 64.568,35.395 64.72,35.623C64.872,35.851 65.07,36.046 65.301,36.197C65.961,36.531 66.701,36.681 67.441,36.631C67.913,36.63 68.384,36.603 68.852,36.552C69.241,36.517 69.626,36.453 70.005,36.359L70.005,34.916C69.139,35.004 68.429,35.05 67.874,35.05C67.464,35.074 67.053,35.025 66.66,34.907C66.54,34.857 66.433,34.778 66.351,34.677C66.269,34.577 66.214,34.458 66.19,34.331C66.106,33.822 66.071,33.307 66.087,32.792L70.193,32.792L70.193,32.165C70.215,31.241 70.167,30.317 70.047,29.4C69.98,28.896 69.713,28.438 69.304,28.124ZM68.41,31.428L66.087,31.428C66.086,30.95 66.119,30.473 66.186,30C66.195,29.887 66.228,29.777 66.283,29.678C66.338,29.579 66.414,29.492 66.505,29.424C66.988,29.233 67.527,29.233 68.01,29.424C68.1,29.494 68.173,29.583 68.227,29.682C68.28,29.782 68.312,29.892 68.321,30.004C68.394,30.475 68.424,30.952 68.41,31.428Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M81.818,29.12C81.739,28.745 81.527,28.406 81.218,28.161C80.849,27.928 80.409,27.817 79.966,27.844C79.966,27.844 79.924,27.844 79.895,27.844C79.895,27.844 78.467,27.767 77.877,28.464C77.813,28.542 77.755,28.625 77.705,28.713C77.61,28.478 77.445,28.275 77.229,28.129C76.858,27.924 76.431,27.826 76.001,27.848C76.001,27.848 74.573,27.767 73.983,28.468C73.917,28.545 73.86,28.629 73.812,28.717L73.764,27.984L72.027,27.984L72.027,36.48L73.931,36.48L73.931,32.051C73.921,31.52 73.942,30.988 73.993,30.459C74.005,30.198 74.107,29.949 74.283,29.749C74.493,29.584 74.764,29.506 75.035,29.531C75.276,29.513 75.518,29.557 75.735,29.658C75.815,29.731 75.879,29.819 75.923,29.916C75.967,30.013 75.991,30.118 75.992,30.224C76.042,30.747 76.063,31.272 76.054,31.798L76.054,36.48L77.958,36.48L77.958,32.051C77.958,31.314 77.958,30.785 78.015,30.459C78.028,30.197 78.133,29.946 78.315,29.749C78.53,29.586 78.802,29.508 79.076,29.531C79.319,29.513 79.562,29.557 79.781,29.658C79.86,29.732 79.923,29.82 79.966,29.917C80.01,30.014 80.033,30.118 80.033,30.224C80.086,30.747 80.106,31.272 80.095,31.798L80.095,36.48L81.999,36.48L81.999,30.884C82.009,30.291 81.949,29.699 81.818,29.12Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M53.781,28.171C53.383,27.928 52.913,27.814 52.44,27.845C52.44,27.845 52.402,27.845 52.382,27.845C52.382,27.845 50.936,27.773 50.343,28.465C50.301,28.514 50.262,28.565 50.227,28.619L50.184,27.981L48.424,27.981L48.424,36.48L50.353,36.48L50.353,32.049C50.342,31.521 50.363,30.992 50.415,30.465C50.429,30.203 50.538,29.952 50.724,29.755C50.957,29.584 51.25,29.503 51.543,29.529C51.8,29.511 52.057,29.555 52.291,29.655C52.378,29.728 52.448,29.816 52.497,29.915C52.546,30.014 52.572,30.121 52.575,30.23C52.63,30.75 52.653,31.273 52.643,31.796L52.643,36.48L54.571,36.48L54.571,30.963C54.587,30.356 54.533,29.75 54.412,29.153C54.333,28.765 54.109,28.417 53.781,28.171Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M38.128,25.44L36.175,25.44L36.175,27.877L34.96,27.877L34.96,29.434L36.175,29.434L36.175,36.48L38.128,36.48L38.128,29.434L39.614,29.434L39.614,27.877L38.128,27.877L38.128,25.44Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -7,6 +7,7 @@
android:clipChildren="false">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/linearLayoutCompat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
@ -37,6 +38,7 @@
android:layout_marginStart="64dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="64dp"
android:minLines="3"
app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -45,6 +47,20 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/chb_understand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alert_button_i_understand"
android:textStyle="bold"
android:visibility="gone"
app:buttonTint="@color/accent"
app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayoutCompat"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/fl_scan_card_container"
android:layout_width="wrap_content"

View file

@ -1,160 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="Re-create wallet" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="16dp"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<TextView
android:id="@+id/tv_step_number"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="start"
android:text="@string/details_twins_recreate_step_format"
android:textColor="@color/colorSecondary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="@+id/v_step_1"
app:layout_constraintStart_toStartOf="@id/v_step_1"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/v_step_1"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="24dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_2"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_step_number" />
<View
android:id="@+id/v_step_2"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toStartOf="@+id/v_step_3"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_1"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<View
android:id="@+id/v_step_3"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="24dp"
android:background="@color/colorSecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/v_step_2"
app:layout_constraintTop_toTopOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_marginBottom="8dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_title_format"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/v_step_1" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_subtitle"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_title" />
<ImageView
android:id="@+id/iv_card_1"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="47dp"
android:layout_marginTop="16dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_twin_cards_description" />
<ImageView
android:id="@+id/iv_card_2"
android:layout_width="130dp"
android:layout_height="70dp"
android:layout_marginStart="24dp"
android:layout_marginTop="20dp"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/iv_card_1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tap"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline"
tools:text="@string/details_twins_recreate_button_format" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,103 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.55"
android:background="@color/warning"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards_orange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
android:layout_marginBottom="24dp"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:drawablePadding="4dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/common_warning"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:drawableStartCompat="@drawable/ic_warning"
app:drawableTint="@color/darkGray6"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/details_twins_recreate_warning"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_cancel"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:gravity="center"
android:text="@string/common_back"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_start"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_start"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -41,7 +41,7 @@
<include
android:id="@+id/onboarding_action_container"
layout="@layout/layout_onboarding_bottom_action_views"
layout="@layout/layout_onboarding_container_bottom"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />

View file

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/l_twin_cards"
android:alpha="0.22"
android:background="@color/darkGray6"/>
<include
android:id="@+id/l_twin_cards"
layout="@layout/layout_twin_cards"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp"
android:layout_marginBottom="24dp"
android:gravity="top"
app:layout_constraintBottom_toTopOf="@id/tv_twin_title"
app:layout_constraintVertical_bias="0" />
<TextView
android:id="@+id/tv_twin_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24sp"
android:layout_marginBottom="8dp"
android:text="@string/twins_onboarding_title"
android:textColor="@color/darkGray6"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_subtitle"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="@string/twins_onboarding_subtitle"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/tv_twin_cards_description_1"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_twin_cards_description_1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/twins_onboarding_description_format"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/btn_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.37" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_continue"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:text="@string/common_continue"
app:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/guideline" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -58,7 +58,7 @@
android:elevation="3dp"
android:maxHeight="215dp"
android:scaleType="fitCenter"
android:src="@drawable/card_placeholder"
android:src="@drawable/card_placeholder_black"
app:layout_constraintTop_toTopOf="parent" />
<TextView

View file

@ -7,9 +7,10 @@
android:clipChildren="false">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/linearLayoutCompat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginBottom="8dp"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container"
@ -37,6 +38,7 @@
android:layout_marginStart="64dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="64dp"
android:minLines="3"
app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -45,6 +47,20 @@
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.AppCompatCheckBox
android:id="@+id/chb_understand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/alert_button_i_understand"
android:textStyle="bold"
android:visibility="gone"
app:buttonTint="@color/accent"
app:layout_constraintBottom_toTopOf="@+id/fl_scan_card_container"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearLayoutCompat"
tools:visibility="visible" />
<FrameLayout
android:id="@+id/fl_scan_card_container"
android:layout_width="wrap_content"

View file

@ -1,11 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/onboarding_main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:visibility="visible">
<include
android:id="@+id/onboarding_twins_welcome_bg"
layout="@layout/view_bg_twins_welcome"
android:visibility="gone"
tools:visibility="visible" />
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
@ -38,12 +46,43 @@
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:src="@drawable/ic_home_front_card"
android:transitionName="imv_front_card"
android:src="@drawable/card_placeholder_black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/cards_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:clipChildren="false"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_back_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_white" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_front_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>
<include
android:id="@+id/onboarding_tv_balance"
layout="@layout/view_onboarding_tv_balance"

View file

@ -13,7 +13,7 @@
android:elevation="0dp"
android:rotation="-21"
android:scaleType="fitXY"
android:src="@drawable/ic_home_back_card"
android:src="@drawable/card_placeholder_white"
android:translationY="@dimen/home_back_card_translationY"
app:layout_constraintEnd_toEndOf="@+id/imv_front_card"
app:layout_constraintStart_toStartOf="@+id/imv_front_card"
@ -26,8 +26,7 @@
android:elevation="1dp"
android:rotation="-3"
android:scaleType="fitXY"
android:src="@drawable/ic_home_front_card"
android:transitionName="imv_front_card"
android:src="@drawable/card_placeholder_black"
android:translationY="@dimen/home_front_card_translationY"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_group_1258"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="-40dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="60dp"
android:scaleType="fitXY"
android:src="@drawable/ic_twin_cards_background_warning"
app:layout_constraintBottom_toBottomOf="@id/iv_twin_card_2"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_1"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="50dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/iv_twin_card_2"
android:layout_width="wrap_content"
android:layout_height="170dp"
android:layout_marginTop="20dp"
android:rotation="-19.5"
android:src="@drawable/card_placeholder"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_twin_card_1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -6,6 +6,11 @@
android:layout_height="match_parent"
android:visibility="visible">
<include
android:id="@+id/onboarding_twins_welcome_bg"
layout="@layout/view_bg_twins_welcome"
android:visibility="gone" />
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
@ -38,12 +43,43 @@
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:src="@drawable/ic_home_front_card"
android:transitionName="imv_front_card"
android:src="@drawable/card_placeholder_black"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/cards_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:clipChildren="false"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_back_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_white" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_front_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>
<include
android:id="@+id/onboarding_tv_balance"
layout="@layout/view_onboarding_tv_balance"

View file

@ -6,6 +6,11 @@
android:layout_height="match_parent"
android:visibility="visible">
<include
android:id="@+id/onboarding_twins_welcome_bg"
layout="@layout/view_bg_twins_welcome"
android:visibility="gone" />
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
@ -38,14 +43,45 @@
android:layout_width="140dp"
android:layout_height="88dp"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder"
android:transitionName="imv_front_card"
android:src="@drawable/card_placeholder_black"
android:translationY="-30dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/imv_card_background" />
<FrameLayout
android:id="@+id/cards_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:clipChildren="false"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_back_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_white" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_front_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>
<include
android:id="@+id/onboarding_tv_balance"
layout="@layout/view_onboarding_tv_balance"

View file

@ -6,6 +6,11 @@
android:layout_height="match_parent"
android:visibility="visible">
<include
android:id="@+id/onboarding_twins_welcome_bg"
layout="@layout/view_bg_twins_welcome"
android:visibility="gone" />
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
@ -37,14 +42,45 @@
android:layout_width="140dp"
android:layout_height="88dp"
android:scaleType="fitXY"
android:src="@drawable/ic_home_front_card"
android:transitionName="imv_front_card"
android:src="@drawable/card_placeholder_black"
android:translationY="-30dp"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/imv_card_background" />
<FrameLayout
android:id="@+id/cards_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:clipChildren="false"
android:layout_marginEnd="32dp"
android:src="@drawable/card_placeholder_black"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_back_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_white" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_front_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>
<include
android:id="@+id/onboarding_tv_balance"
layout="@layout/view_onboarding_tv_balance"

View file

@ -0,0 +1,116 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/onboarding_main_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible">
<include
android:id="@+id/onboarding_twins_welcome_bg"
layout="@layout/view_bg_twins_welcome"
android:visibility="gone" />
<include
layout="@layout/view_onboarding_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_progress_margin_top"
android:layout_marginEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="match_parent"
android:layout_height="230dp"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_square_background_margin_top"
android:layout_marginEnd="32dp"
android:background="@drawable/shape_rectangle_rounded_8"
android:backgroundTint="@color/lightGray0"
android:elevation="0dp"
android:scaleType="fitCenter"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pb_state" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_front_card"
android:layout_width="140dp"
android:layout_height="88dp"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black"
android:translationY="-30dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/imv_card_background" />
<FrameLayout
android:id="@+id/cards_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_card_margin_top"
android:layout_marginEnd="32dp"
android:clipChildren="false"
android:src="@drawable/card_placeholder_black"
android:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_back_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_white" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_twin_front_card"
android:layout_width="328dp"
android:layout_height="174dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="@drawable/card_placeholder_black" />
</FrameLayout>
<include
android:id="@+id/onboarding_tv_balance"
layout="@layout/view_onboarding_tv_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="@+id/imv_card_background"
app:layout_constraintEnd_toEndOf="@+id/imv_card_background"
app:layout_constraintStart_toStartOf="@+id/imv_card_background"
app:layout_constraintTop_toTopOf="@+id/imv_card_background" />
<include
android:id="@+id/btn_refresh_balance"
layout="@layout/view_onboarding_refresh_balance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/imv_card_background"
app:layout_constraintEnd_toEndOf="@+id/imv_card_background"
app:layout_constraintStart_toStartOf="@+id/imv_card_background"
app:layout_constraintTop_toBottomOf="@+id/imv_card_background" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_activation_success"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_activation_success"
app:layout_constraintBottom_toBottomOf="@+id/imv_card_background"
app:layout_constraintEnd_toEndOf="@+id/imv_card_background"
app:layout_constraintStart_toStartOf="@+id/imv_card_background"
app:layout_constraintTop_toBottomOf="@+id/imv_card_background" />
</androidx.constraintlayout.widget.ConstraintLayout>

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