diff --git a/app/build.gradle b/app/build.gradle index 686a683503..c8e2711c22 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,13 +72,14 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.3.6' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' implementation 'com.google.android.material:material:1.4.0' + implementation "androidx.core:core-ktx:1.6.0" implementation 'com.google.android.play:core:1.10.2' implementation 'com.google.android.play:core-ktx:1.8.1' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'com.tangem:blockchain:develop-48' - implementation 'com.tangem.tangem-sdk-kotlin:core:develop-95' - implementation 'com.tangem.tangem-sdk-kotlin:android:develop-95' + implementation 'com.tangem:blockchain:develop-51' + implementation 'com.tangem.tangem-sdk-kotlin:core:develop-106' + implementation 'com.tangem.tangem-sdk-kotlin:android:develop-106' // WebView implementation "androidx.browser:browser:1.3.0" diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index c0d6abea79..0c9f4a0bbf 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -8,6 +8,7 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import com.google.android.material.snackbar.Snackbar import com.tangem.TangemSdk +import com.tangem.operations.backup.BackupService import com.tangem.tangem_sdk_new.extensions.init import com.tangem.tap.common.DialogManager import com.tangem.tap.common.IntentHandler @@ -32,6 +33,7 @@ import kotlin.coroutines.CoroutineContext lateinit var tangemSdk: TangemSdk lateinit var tangemSdkManager: TangemSdkManager +lateinit var backupService: BackupService var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext @@ -67,6 +69,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { tangemSdk = TangemSdk.init(this, TangemSdkManager.config) tangemSdkManager = TangemSdkManager(tangemSdk, this) + backupService = BackupService.init(tangemSdk, this) store.dispatch(GlobalAction.SetResources(getAndroidResources())) store.dispatch(WalletConnectAction.RestoreSessions) @@ -93,7 +96,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler { val backStackIsEmpty = supportFragmentManager.backStackEntryCount == 0 val isScannedBefore = store.state.globalState.scanResponse != null - val isOnboardingServiceActive = store.state.globalState.onboardingManager != null + val isOnboardingServiceActive = store.state.globalState.onboardingState.onboardingStarted if (backStackIsEmpty || (!isOnboardingServiceActive && !isScannedBefore)) { store.dispatch(NavigationAction.NavigateTo(AppScreen.Home)) } diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index b3a0c345e2..8f1491368c 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -9,6 +9,11 @@ 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.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog import com.tangem.tap.store import com.tangem.wallet.R @@ -74,6 +79,10 @@ class DialogManager : StoreSubscriber { TransactionDialog.create(state.dialog.dialogData, context) is WalletConnectDialog.PersonalSign -> PersonalSignDialog.create(state.dialog.data, context) + is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) + is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) + is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context) + is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context) else -> null } dialog?.show() diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt index 0d6b58db28..233fa3702d 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkError.kt @@ -72,9 +72,39 @@ object TangemSdk { is TangemSdkError.WalletCannotBeCreated -> TangemSdkError.WalletCannotBeCreated() is TangemSdkError.UnsupportedCurve -> TangemSdkError.UnsupportedCurve() is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig() - is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError() + is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage) is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage) is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.throwable) + is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards() + is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired() + is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired() + is TangemSdkError.AccessCodeOrPasscodeRequired -> TangemSdkError.AccessCodeOrPasscodeRequired() + is TangemSdkError.ResetPinNoCardsToReset -> TangemSdkError.ResetPinNoCardsToReset() + is TangemSdkError.ResetPinWrongCard -> TangemSdkError.ResetPinWrongCard() + is TangemSdkError.BackupFailedCardNotLinked -> TangemSdkError.BackupFailedCardNotLinked() + is TangemSdkError.BackupNotAllowed -> TangemSdkError.BackupNotAllowed() + is TangemSdkError.BackupCardAlreadyAdded -> TangemSdkError.BackupCardAlreadyAdded() + is TangemSdkError.MissingPrimaryCard -> TangemSdkError.MissingPrimaryCard() + is TangemSdkError.MissingPrimaryAttestSignature -> TangemSdkError.MissingPrimaryAttestSignature() + is TangemSdkError.NoBackupDataForCard -> TangemSdkError.NoBackupDataForCard() + is TangemSdkError.BackupFailedEmptyWallets -> TangemSdkError.BackupFailedEmptyWallets() + is TangemSdkError.BackupFailedNotEmptyWallets -> TangemSdkError.BackupFailedNotEmptyWallets() + is TangemSdkError.NoActiveBackup -> TangemSdkError.NoActiveBackup() + is TangemSdkError.ResetBackupFailedHasBackupedWallets -> TangemSdkError.ResetBackupFailedHasBackupedWallets() + is TangemSdkError.BackupServiceInvalidState -> TangemSdkError.BackupServiceInvalidState() + is TangemSdkError.NoBackupCardForIndex -> TangemSdkError.NoBackupCardForIndex() + is TangemSdkError.EmptyBackupCards -> TangemSdkError.EmptyBackupCards() + is TangemSdkError.BackupFailedWrongIssuer -> TangemSdkError.BackupFailedWrongIssuer() + is TangemSdkError.BackupFailedHDWalletSettings -> TangemSdkError.BackupFailedHDWalletSettings() + is TangemSdkError.BackupFailedNotEnoughCurves -> TangemSdkError.BackupFailedNotEnoughCurves() + is TangemSdkError.BackupFailedNotEnoughWallets -> TangemSdkError.BackupFailedNotEnoughWallets() + is TangemSdkError.FileSettingsUnsupported -> TangemSdkError.FileSettingsUnsupported() + is TangemSdkError.FilesIsEmpty -> TangemSdkError.FilesIsEmpty() + is TangemSdkError.FilesDisabled -> TangemSdkError.FilesDisabled() + is TangemSdkError.HDWalletDisabled -> TangemSdkError.HDWalletDisabled() + is TangemSdkError.WrongInteractionMode -> TangemSdkError.WrongInteractionMode() + is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed() + is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware() } } diff --git a/app/src/main/java/com/tangem/tap/common/extensions/ByteArray.kt b/app/src/main/java/com/tangem/tap/common/extensions/ByteArray.kt new file mode 100644 index 0000000000..376b56f399 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/extensions/ByteArray.kt @@ -0,0 +1,17 @@ +package com.tangem.tap.common.extensions + +/** +[REDACTED_AUTHOR] + */ + +data class ByteArrayKey(val bytes: ByteArray) { + override fun equals(other: Any?): Boolean { + return this === other || other is ByteArrayKey && this.bytes.contentEquals(other.bytes) + } + + override fun hashCode(): Int = bytes.contentHashCode() +} + +fun ByteArray.toMapKey(): ByteArrayKey = ByteArrayKey(this) + +fun ByteArrayKey.bytes(): ByteArray = this.bytes \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt index f96f16f5ee..30d8e1b18a 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt @@ -17,7 +17,7 @@ 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.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.send.ui.SendFragment import com.tangem.tap.features.tokens.ui.AddTokensFragment import com.tangem.tap.features.wallet.ui.WalletDetailsFragment @@ -68,7 +68,6 @@ fun FragmentActivity.addOnBackPressedDispatcher( private fun fragmentFactory(screen: AppScreen): Fragment { return when (screen) { -// AppScreen.Home -> TestLeapfrogFragment() AppScreen.Home -> HomeFragment() AppScreen.OnboardingNote -> OnboardingNoteFragment() AppScreen.OnboardingWallet -> OnboardingWalletFragment() diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 39b221f517..d3c6f3656b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -18,6 +18,7 @@ import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOt 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.BackupMiddleware 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 @@ -60,7 +61,8 @@ data class AppState( DetailsMiddleware().detailsMiddleware, DisclaimerMiddleware().disclaimerMiddleware, TokensMiddleware().tokensMiddleware, - WalletConnectMiddleware().walletConnectMiddleware + WalletConnectMiddleware().walletConnectMiddleware, + BackupMiddleware().backupMiddleware ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a1e1e7b2b1..bbea6e43f9 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -31,11 +31,12 @@ sealed class GlobalAction : Action { object HideDialog : GlobalAction() sealed class Onboarding { - data class Start(val scanResponse: ScanResponse) : GlobalAction() + data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction() object Stop : GlobalAction() } data class ScanCard( + val shouldDeriveWC: Boolean, val onSuccess: ((ScanResponse) -> Unit)? = null, val onFailure: ((TangemError) -> Unit)? = null, val messageResId: Int? = null, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt index 06cc349269..258131199b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMidlleware.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.redux.global import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.services.Result +import com.tangem.tap.* import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.extensions.dispatchDialogShow import com.tangem.tap.common.extensions.dispatchOnMain @@ -13,10 +14,6 @@ import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager import com.tangem.tap.features.send.redux.SendAction import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.moonpay.MoonpayService -import com.tangem.tap.preferencesStorage -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager import kotlinx.coroutines.launch import org.rekotlin.Middleware import timber.log.Timber @@ -91,7 +88,12 @@ private val globalMiddlewareHandler: Middleware = { dispatch, appState } is GlobalAction.ScanCard -> { scope.launch { - val result = tangemSdkManager.scanProduct(FirebaseAnalyticsHandler, action.messageResId) + val result = tangemSdkManager.scanProduct( + FirebaseAnalyticsHandler, + currenciesRepository, + action.shouldDeriveWC, + action.messageResId + ) store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result)) withMainContext { when (result) { diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index 993830329a..e549a2f386 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -17,12 +17,16 @@ fun globalReducer(action: Action, state: AppState): GlobalState { globalState.copy(resources = action.resources) } is GlobalAction.Onboarding.Start -> { - val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage - val onboardingState = OnboardingManager(action.scanResponse, usedCardsPrefStorage) - globalState.copy(onboardingManager = onboardingState) + val onboardingManager = if (action.scanResponse != null) { + val usedCardsPrefStorage = preferencesStorage.usedCardsPrefStorage + OnboardingManager(action.scanResponse, usedCardsPrefStorage) + } else { + null + } + globalState.copy(onboardingState = OnboardingState(true, onboardingManager)) } is GlobalAction.Onboarding.Stop -> { - globalState.copy(onboardingManager = null) + globalState.copy(onboardingState = OnboardingState(false)) } is GlobalAction.ScanFailsCounter.Increment -> { globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1) diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 484142c611..f634a6a0ff 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -15,7 +15,7 @@ import org.rekotlin.StateType data class GlobalState( val scanResponse: ScanResponse? = null, - val onboardingManager: OnboardingManager? = null, + val onboardingState: OnboardingState = OnboardingState(), val cardVerifiedOnline: Boolean = false, val tapWalletManager: TapWalletManager = TapWalletManager(), val payIdManager: PayIdManager = PayIdManager(), @@ -32,12 +32,17 @@ data class GlobalState( data class AndroidResources( - val strings: RString = RString() + val strings: RString = RString(), ) { data class RString( val addressWasCopied: Int = -1, - val walletIsNotEmpty: Int = -1 + val walletIsNotEmpty: Int = -1, ) } typealias CryptoCurrencyName = String -typealias FiatCurrencyName = String \ No newline at end of file +typealias FiatCurrencyName = String + +data class OnboardingState( + val onboardingStarted: Boolean = false, + val onboardingManager: OnboardingManager? = null, +) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt index 27cb46c500..7131feb7b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt @@ -1,6 +1,6 @@ package com.tangem.tap.domain -import CreateProductWalletAndRescanTask +import CreateProductWalletTask import android.content.Context import com.tangem.Message import com.tangem.TangemSdk @@ -12,17 +12,21 @@ import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.Config import com.tangem.common.core.TangemSdkError +import com.tangem.common.hdWallet.DerivationPath import com.tangem.operations.CommandResponse +import com.tangem.operations.derivation.DeriveWalletPublicKeysTask +import com.tangem.operations.derivation.ExtendedPublicKeyList import com.tangem.operations.pins.CheckUserCodesCommand import com.tangem.operations.pins.CheckUserCodesResponse import com.tangem.operations.pins.SetUserCodeCommand -import com.tangem.operations.wallet.PurgeWalletCommand import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.AnalyticsHandler import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask +import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask import com.tangem.tap.domain.tasks.product.ScanProductTask import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.wallet.R import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -33,18 +37,20 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun scanProduct( analyticsHandler: AnalyticsHandler, + currenciesRepository: CurrenciesRepository, + shouldDeriveWC: Boolean, messageRes: Int? = null, ): CompletionResult { analyticsHandler.triggerEvent(AnalyticsEvent.READY_TO_SCAN, null) val message = Message(context.getString(messageRes ?: R.string.initial_message_scan_header)) - return runTaskAsyncReturnOnMain(ScanProductTask(), null, message) - .also { sendScanFailuresToAnalytics(analyticsHandler, it) } + return runTaskAsyncReturnOnMain(ScanProductTask(null, currenciesRepository, shouldDeriveWC), null, message) + .also { sendScanFailuresToAnalytics(analyticsHandler, it) } } suspend fun createProductWallet(scanResponse: ScanResponse): CompletionResult { return runTaskAsync( - CreateProductWalletAndRescanTask(scanResponse.productType), + CreateProductWalletTask(scanResponse.productType), scanResponse.card.cardId, Message(context.getString(R.string.initial_message_create_wallet_body)) ) @@ -66,11 +72,19 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co initialMessage = Message(context.getString(R.string.initial_message_create_wallet_body))) } - suspend fun eraseWallet(card: Card): CompletionResult { + suspend fun derivePublicKeys( + cardId: String, + walletPublicKey: ByteArray, + derivationPaths: List + ): CompletionResult { + return runTaskAsyncReturnOnMain(DeriveWalletPublicKeysTask(walletPublicKey, derivationPaths), cardId) + } + + suspend fun resetToFactorySettings(card: Card): CompletionResult { return runTaskAsyncReturnOnMain( - PurgeWalletCommand(card.wallets.first().publicKey), + ResetToFactorySettingsTask(), card.cardId, - initialMessage = Message(context.getString(R.string.initial_message_purge_wallet_body))) + initialMessage = Message(context.getString(R.string.details_row_title_reset_factory_settings))) } suspend fun setPasscode(cardId: String?): CompletionResult { @@ -104,13 +118,13 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co suspend fun runTaskAsync( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, ): CompletionResult = - 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 runTaskAsyncReturnOnMain( runnable: CardSessionRunnable, cardId: String? = null, initialMessage: Message? = null, diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt index 978ca5288f..20d1eda08c 100644 --- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt +++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain import com.tangem.Message import com.tangem.TangemSdk import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.Wallet import com.tangem.common.CompletionResult import com.tangem.tap.domain.tasks.SignHashTask import com.tangem.tap.domain.tasks.SignHashesTask @@ -15,13 +16,9 @@ class TangemSigner( private val signerCallback: (TangemSignerResponse) -> Unit, ) : TransactionSigner { - override suspend fun sign( - hash: ByteArray, - cardId: String, - walletPublicKey: ByteArray, - ): CompletionResult = - suspendCoroutine { continuation -> - val command = SignHashTask(hash, walletPublicKey) + override suspend fun sign(hash: ByteArray, cardId: String, publicKey: Wallet.PublicKey): CompletionResult { + return suspendCoroutine { continuation -> + val command = SignHashTask(hash, publicKey) tangemSdk.startSessionWithRunnable( runnable = command, cardId = cardId, @@ -42,15 +39,11 @@ class TangemSigner( } } } + } - - override suspend fun sign( - hashes: List, - cardId: String, - walletPublicKey: ByteArray, - ): CompletionResult> = - suspendCoroutine { continuation -> - val task = SignHashesTask(hashes, walletPublicKey) + override suspend fun sign(hashes: List, cardId: String, publicKey: Wallet.PublicKey): CompletionResult> { + return suspendCoroutine { continuation -> + val task = SignHashesTask(hashes, publicKey) tangemSdk.startSessionWithRunnable( runnable = task, cardId = cardId, @@ -71,6 +64,7 @@ class TangemSigner( } } } + } } data class TangemSignerResponse( diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 10f1a76a87..9f6d0f13e1 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -1,7 +1,6 @@ package com.tangem.tap.domain import com.tangem.blockchain.common.* -import com.tangem.common.card.Card import com.tangem.common.services.Result import com.tangem.tap.common.analytics.AnalyticsEvent import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler @@ -16,7 +15,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.features.tokens.redux.TokensAction import com.tangem.tap.features.wallet.redux.Currency import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.network.NetworkConnectivity @@ -85,12 +83,6 @@ class TapWalletManager { store.dispatch(GlobalAction.SaveScanNoteResponse(data)) store.dispatch(WalletAction.SetIfTestnetCard(data.card.isTestCard)) store.dispatch(WalletAction.MultiWallet.SetIsMultiwalletAllowed(data.card.isMultiwalletAllowed)) - - val blockchain = data.getBlockchain() - if (blockchain == Blockchain.Ethereum || - blockchain == Blockchain.EthereumTestnet) { - store.dispatch(TokensAction.LoadCardTokens) - } loadData(data) } } @@ -131,7 +123,7 @@ class TapWalletManager { store.dispatch(WalletAction.MultiWallet.SetPrimaryToken(primaryToken)) } if (data.card.isMultiwalletAllowed) { - loadMultiWalletData(data.card, blockchain, primaryWalletManager) + loadMultiWalletData(data, blockchain, primaryWalletManager) } else { store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager)) store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(blockchain))) @@ -139,7 +131,7 @@ class TapWalletManager { } else { if (data.card.isMultiwalletAllowed) { - loadMultiWalletData(data.card, blockchain, null) + loadMultiWalletData(data, blockchain, null) } } val moonPayStatus = store.state.globalState.moonpayStatus @@ -149,10 +141,10 @@ class TapWalletManager { } private fun loadMultiWalletData( - card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager? + scanResponse: ScanResponse, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager? ) { val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList() - val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId) + val savedCurrencies = currenciesRepository.loadCardCurrencies(scanResponse.card.cardId) if (savedCurrencies == null) { if (primaryBlockchain != null && primaryWalletManager != null) { @@ -169,11 +161,11 @@ class TapWalletManager { CardCurrencies(blockchains = blockchains, tokens = emptyList()) )) val walletManagers = - walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList()) + walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains.toList()) store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) store.dispatch(WalletAction.MultiWallet.AddBlockchains(blockchains.toList())) } - store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse(card, walletManagerFactory)) + store.dispatch(WalletAction.MultiWallet.FindBlockchainsInUse) store.dispatch(WalletAction.MultiWallet.FindTokensInUse) } else { val blockchains = savedCurrencies.blockchains.toList() @@ -182,10 +174,10 @@ class TapWalletManager { primaryWalletManager != null && primaryBlockchain != null ) { val blockchainsWithoutPrimary = blockchains.filterNot { it == primaryBlockchain } - walletManagerFactory.makeWalletManagersForApp(card, blockchainsWithoutPrimary) + walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchainsWithoutPrimary) .plus(primaryWalletManager) } else { - walletManagerFactory.makeWalletManagersForApp(card, blockchains) + walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains) } store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManagers)) diff --git a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt index 70ef2df88e..45e19eb81a 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWorkarounds.kt @@ -29,8 +29,7 @@ object TapWorkarounds { } fun Card.isNotSupportedInThatRelease():Boolean { - return tangemWalletBatches.contains(batchId) - + return false } @Deprecated("Use ScanResponse.isTangemNote") diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt index aa85331ab2..b505619739 100644 --- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt +++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt @@ -3,74 +3,70 @@ package com.tangem.tap.domain.extensions import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.WalletManagerFactory -import com.tangem.common.card.Card import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.hexToBytes +import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.tasks.product.ScanResponse fun WalletManagerFactory.makeWalletManagerForApp( - card: Card, - blockchain: Blockchain, + scanResponse: ScanResponse, blockchain: Blockchain ): WalletManager? { + val card = scanResponse.card + if (card.isTestCard && blockchain.getTestnetVersion() == null) return null val supportedCurves = blockchain.getSupportedCurves() ?: return null + val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) } - val wallet = selectWallet(wallets) - val publicKey = wallet?.publicKey ?: return null - val curveToUse = wallet.curve ?: return null - return if (card.isTestCard) { - blockchain.getTestnetVersion()?.let { - makeWalletManager(card.cardId, publicKey, it, curveToUse) + val wallet = selectWallet(wallets) ?: return null + + val environmentBlockchain = if (card.isTestCard) blockchain.getTestnetVersion()!! else blockchain + + val seedKey = wallet.extendedPublicKey + return when { + scanResponse.isTangemTwins() && scanResponse.secondTwinPublicKey != null -> { + makeTwinWalletManager( + card.cardId, + wallet.publicKey, scanResponse.secondTwinPublicKey.hexToBytes(), + environmentBlockchain, wallet.curve + ) + } + seedKey != null -> { + val derivedKeys = scanResponse.derivedKeys[wallet.publicKey.toMapKey()] + val derivedKey = derivedKeys?.firstOrNull { it.derivationPath == blockchain.derivationPath() } + ?: return null + + makeWalletManager(card.cardId, environmentBlockchain, seedKey, derivedKey) + } + else -> { + makeWalletManager(card.cardId, environmentBlockchain, wallet.publicKey, wallet.curve) } - } else { - makeWalletManager(card.cardId, publicKey, blockchain, curveToUse) } } +fun WalletManagerFactory.makeWalletManagersForApp( + scanResponse: ScanResponse, blockchains: List, +): List { + val isTestCard = scanResponse.card.isTestCard + val filteredBlockchains = blockchains.mapNotNull { if (isTestCard) it.getTestnetVersion() else it } + return filteredBlockchains.mapNotNull { makeWalletManagerForApp(scanResponse, it) } +} + +fun WalletManagerFactory.makePrimaryWalletManager( + scanResponse: ScanResponse, +): WalletManager? { + val blockchain = if (scanResponse.card.isTestCard) { + scanResponse.getBlockchain().getTestnetVersion() ?: return null + } else { + scanResponse.getBlockchain() + } + return makeWalletManagerForApp(scanResponse, blockchain) +} + private fun selectWallet(wallets: List): CardWallet? { return when (wallets.size) { 0 -> null 1 -> wallets[0] else -> wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: wallets[0] } -} - -fun WalletManagerFactory.makeWalletManagersForApp( - card: Card, blockchains: List, -): List { - return if (card.isTestCard) { - blockchains.mapNotNull { blockchain -> - blockchain.getTestnetVersion()?.let { makeWalletManagerForApp(card, it) } - } - } else { - blockchains.mapNotNull { blockchain -> makeWalletManagerForApp(card, blockchain) } - } -} - -fun WalletManagerFactory.makePrimaryWalletManager( - data: ScanResponse, -): WalletManager? { - val card = data.card - val blockchain = if (card.isTestCard) { - data.getBlockchain().getTestnetVersion() - } else { - data.getBlockchain() - } - val supportedCurves = blockchain?.getSupportedCurves() ?: return null - - val wallets = card.wallets.filter { wallet -> supportedCurves.contains(wallet.curve) } - val wallet = selectWallet(wallets) - val publicKey = wallet?.publicKey ?: return null - val curveToUse = wallet.curve - - return if (data.isTangemTwins() && data.secondTwinPublicKey != null) { - makeMultisigWalletManager( - cardId = card.cardId, - walletPublicKey = publicKey, pairPublicKey = data.secondTwinPublicKey.hexToBytes(), - blockchain = blockchain, curve = curveToUse - ) - } else { - makeWalletManager(card.cardId, publicKey, blockchain, curveToUse) - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt index 1487f07e37..81a967c20b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletAndRescanTask.kt @@ -9,7 +9,7 @@ import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletCommand +import com.tangem.operations.wallet.CreateWalletTask @Deprecated("Use CreateProductWalletAndRescanTask instead") class CreateWalletAndRescanTask : CardSessionRunnable { @@ -22,7 +22,7 @@ class CreateWalletAndRescanTask : CardSessionRunnable { val firmwareVersion = card.firmwareVersion val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - CreateWalletCommand(card.supportedCurves.first()) + CreateWalletTask(card.supportedCurves.first()) } else { CreateWalletsTask() } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt index 6de34f651a..140a0ef72d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/CreateWalletsTask.kt @@ -7,9 +7,9 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletCommand +import com.tangem.operations.wallet.CreateWalletTask -@Deprecated("Use CreateProductWalletsResponse instead") +@Deprecated("Use CreateProductWalletTask instead") class CreateWalletsTask(curves: List? = null) : CardSessionRunnable { private val curves = curves ?: listOf( @@ -30,7 +30,7 @@ class CreateWalletsTask(curves: List? = null) : CardSessionRunnab callback: (result: CompletionResult) -> Unit ) { - CreateWalletCommand(curve).run(session) { result -> + CreateWalletTask(curve).run(session) { result -> when (result) { is CompletionResult.Success -> { if (index == curves.lastIndex) { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/SignHashTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/SignHashTask.kt index 3c24418b42..58520125ce 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/SignHashTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/SignHashTask.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.tasks +import com.tangem.blockchain.common.Wallet import com.tangem.common.CompletionResult import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable @@ -15,17 +16,17 @@ class TangemSignHashResponse( class SignHashTask( private val hash: ByteArray, - private val walletPublicKey: ByteArray, + private val publicKey: Wallet.PublicKey, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { - SignHashCommand(hash, walletPublicKey).run(session) { response -> + SignHashCommand(hash, publicKey.seedKey, publicKey.derivationPath).run(session) { response -> when (response) { is CompletionResult.Success -> { callback(CompletionResult.Success(TangemSignHashResponse( response.data.signature, response.data.totalSignedHashes, - session.environment.card?.wallet(walletPublicKey)?.remainingSignatures + session.environment.card?.wallet(publicKey.seedKey)?.remainingSignatures ))) } is CompletionResult.Failure -> diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/SignHashesTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/SignHashesTask.kt index 2755d9aa82..4d25a1de56 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/SignHashesTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/SignHashesTask.kt @@ -1,5 +1,6 @@ package com.tangem.tap.domain.tasks +import com.tangem.blockchain.common.Wallet import com.tangem.common.CompletionResult import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable @@ -15,16 +16,16 @@ class TangemSignHashesResponse( class SignHashesTask( private val hashes: Collection, - private val walletPublicKey: ByteArray, + private val publicKey: Wallet.PublicKey, ) : CardSessionRunnable { override fun run(session: CardSession, callback: CompletionCallback) { - SignHashesCommand(hashes.toTypedArray(), walletPublicKey).run(session) { response -> + SignHashesCommand(hashes.toTypedArray(), publicKey.seedKey, publicKey.derivationPath).run(session) { response -> when (response) { is CompletionResult.Success -> { callback(CompletionResult.Success(TangemSignHashesResponse( response.data.signatures, response.data.totalSignedHashes, - session.environment.card?.wallet(walletPublicKey)?.remainingSignatures + session.environment.card?.wallet(publicKey.seedKey)?.remainingSignatures ))) } is CompletionResult.Failure -> diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletAndRescanTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt similarity index 59% rename from app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletAndRescanTask.kt rename to app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt index f1024cdf11..530b53ac4c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletAndRescanTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt @@ -1,25 +1,29 @@ +import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card +import com.tangem.common.card.CardWallet import com.tangem.common.card.EllipticCurve import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard -import com.tangem.operations.PreflightReadMode -import com.tangem.operations.PreflightReadTask -import com.tangem.operations.wallet.CreateWalletCommand +import com.tangem.operations.derivation.DeriveWalletPublicKeysTask +import com.tangem.operations.derivation.ExtendedPublicKeyList import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.operations.wallet.CreateWalletTask +import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain -import com.tangem.tap.domain.tasks.product.CreateProductWalletsTask +import com.tangem.tap.domain.tasks.product.CreateWalletsTask import com.tangem.tap.domain.tasks.product.ProductCommandProcessor import com.tangem.tap.domain.tasks.product.getCurvesForNonCreatedWallets +import com.tangem.tap.store /** [REDACTED_AUTHOR] */ -class CreateProductWalletAndRescanTask( +class CreateProductWalletTask( private val type: ProductType, ) : CardSessionRunnable { @@ -28,28 +32,19 @@ class CreateProductWalletAndRescanTask( callback(CompletionResult.Failure(TangemSdkError.CardError())) return } - CreateProductWalletTask(card, type).run(session) { result -> - when (result) { - is CompletionResult.Success -> PreflightReadTask(PreflightReadMode.FullCardRead).run(session, callback) - is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) - } - } - } -} -private class CreateProductWalletTask( - private val card: Card, - private val type: ProductType, -) : CardSessionRunnable { - - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val commandProcessor = when (type) { ProductType.Note -> CreateWalletTangemNote() ProductType.Twins -> throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet") ProductType.Wallet -> CreateWalletTangemWallet() else -> CreateWalletOtherCards() } - commandProcessor.proceed(card, session, callback) + commandProcessor.proceed(card, session) { + when (it) { + is CompletionResult.Success -> callback(CompletionResult.Success(session.environment.card!!)) + is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) + } + } } } @@ -57,7 +52,7 @@ private class CreateWalletTangemNote : ProductCommandProcessor) -> Unit + callback: (result: CompletionResult) -> Unit, ) { if (card.supportedCurves.isEmpty()) { callback(CompletionResult.Failure(TangemSdkError.CardError())) @@ -79,34 +74,63 @@ private class CreateWalletTangemNote : ProductCommandProcessor { + override fun proceed( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { - //TODO: + val supportedCurves = setOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519) + val curves = card.getCurvesForNonCreatedWallets().intersect(supportedCurves).toList() + CreateWalletsTask(curves).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + val response = result.data.createWalletResponses[0] + val derivationPaths = listOf(Blockchain.Bitcoin, Blockchain.Ethereum) + .mapNotNull { it.derivationPath() } + + DeriveWalletPublicKeysTask(response.wallet.publicKey, derivationPaths).run(session) { + when (it) { + is CompletionResult.Success -> { + updateDerivedKeys(response.wallet, it.data) + callback(CompletionResult.Success(response)) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(it.error)) + } + } + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } } } +//TODO: updating derived wallet public keys - make it better later +private fun updateDerivedKeys(wallet: CardWallet, derivedKeys: ExtendedPublicKeyList) { + val onboardingManager = store.state.globalState.onboardingState.onboardingManager ?: return + onboardingManager.scanResponse = onboardingManager.scanResponse.copy( + derivedKeys = mapOf(wallet.publicKey.toMapKey() to derivedKeys) + ) +} + private class CreateWalletOtherCards : ProductCommandProcessor { override fun proceed( card: Card, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit, ) { val firmwareVersion = card.firmwareVersion val task = if (firmwareVersion < FirmwareVersion.MultiWalletAvailable) { - CreateProductWalletsTask(listOf(card.supportedCurves.first())) + CreateWalletsTask(listOf(card.supportedCurves.first())) } else { - CreateProductWalletsTask(card.getCurvesForNonCreatedWallets()) + CreateWalletsTask(card.getCurvesForNonCreatedWallets()) } task.run(session) { result -> diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt similarity index 76% rename from app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletsTask.kt rename to app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt index fc00138a57..89130c4a16 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletsTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateWalletsTask.kt @@ -5,23 +5,23 @@ import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.operations.CommandResponse -import com.tangem.operations.wallet.CreateWalletCommand import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.operations.wallet.CreateWalletTask /** [REDACTED_AUTHOR] */ -class CreateProductWalletsResponse( +class CreateWalletsResponse( val createWalletResponses: List ) : CommandResponse -class CreateProductWalletsTask( +class CreateWalletsTask( private val curves: List, -) : CardSessionRunnable { +) : CardSessionRunnable { private val createdWalletsResponses = mutableListOf() - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { val curve = curves[createdWalletsResponses.size] createWallet(curve, session, callback) } @@ -29,15 +29,14 @@ class CreateProductWalletsTask( private fun createWallet( curve: EllipticCurve, session: CardSession, - callback: (result: CompletionResult) -> Unit + callback: (result: CompletionResult) -> Unit ) { - - CreateWalletCommand(curve).run(session) { result -> + CreateWalletTask(curve).run(session) { result -> when (result) { is CompletionResult.Success -> { createdWalletsResponses.add(result.data) if (createdWalletsResponses.size == curves.size) { - callback(CompletionResult.Success(CreateProductWalletsResponse(createdWalletsResponses))) + callback(CompletionResult.Success(CreateWalletsResponse(createdWalletsResponses))) return@run } createWallet(curves[createdWalletsResponses.size], session, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt new file mode 100644 index 0000000000..444a7b3971 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetToFactorySettingsTask.kt @@ -0,0 +1,57 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.common.CompletionResult +import com.tangem.common.card.Card +import com.tangem.common.core.CardSession +import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.extensions.guard +import com.tangem.operations.backup.ResetBackupCommand +import com.tangem.operations.wallet.PurgeWalletCommand + +class ResetToFactorySettingsTask() : CardSessionRunnable { + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + deleteWallets(session, callback) + } + + private fun deleteWallets( + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + + val wallet = session.environment.card?.wallets?.lastOrNull().guard { + resetBackup(session, callback) + return + } + + PurgeWalletCommand(wallet.publicKey).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + deleteWallets(session, callback) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun resetBackup( + session: CardSession, + callback: (result: CompletionResult) -> Unit, + ) { + + val backupStatus = session.environment.card?.backupStatus + if (backupStatus == null || backupStatus == Card.BackupStatus.NoBackup) { + callback(CompletionResult.Success(session.environment.card!!)) + return + } + + ResetBackupCommand().run(session) { result -> + when (result) { + is CompletionResult.Success -> { + callback(CompletionResult.Success(session.environment.card!!)) + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 14f454c6b5..4aa99403d2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -14,11 +14,16 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.guard import com.tangem.common.extensions.toHexString +import com.tangem.common.hdWallet.DerivationPath +import com.tangem.common.hdWallet.ExtendedPublicKey import com.tangem.operations.CommandResponse import com.tangem.operations.PreflightReadMode import com.tangem.operations.PreflightReadTask import com.tangem.operations.ScanTask +import com.tangem.operations.derivation.DeriveWalletPublicKeysTask import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand +import com.tangem.tap.common.extensions.ByteArrayKey +import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.domain.ProductType import com.tangem.tap.domain.TapSdkError import com.tangem.tap.domain.TapWorkarounds @@ -26,6 +31,7 @@ import com.tangem.tap.domain.TapWorkarounds.getTangemNoteBlockchain import com.tangem.tap.domain.TapWorkarounds.isExcluded import com.tangem.tap.domain.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.tap.domain.extensions.getSingleWallet +import com.tangem.tap.domain.tokens.CurrenciesRepository import com.tangem.tap.domain.twins.TwinsHelper data class ScanResponse( @@ -33,6 +39,7 @@ data class ScanResponse( val productType: ProductType, val walletData: WalletData?, val secondTwinPublicKey: String? = null, + val derivedKeys: Map> = mapOf() ) : CommandResponse { fun getBlockchain(): Blockchain { @@ -60,7 +67,13 @@ data class ScanResponse( fun twinsIsTwinned(): Boolean = card.isTangemTwins() && walletData != null && secondTwinPublicKey != null } -class ScanProductTask(val card: Card? = null) : CardSessionRunnable { +typealias KeyWalletPublicKey = ByteArrayKey + +class ScanProductTask( + val card: Card? = null, + private val currenciesRepository: CurrenciesRepository?, + private val shouldDeriveWC: Boolean +) : CardSessionRunnable { override fun run( session: CardSession, @@ -80,7 +93,7 @@ class ScanProductTask(val card: Card? = null) : CardSessionRunnable ScanNoteProcessor() card.isTangemTwins() -> ScanTwinProcessor() - TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor() + TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor(currenciesRepository, shouldDeriveWC) else -> ScanOtherCardsProcessor() } commandProcessor.proceed(card, session) { processorResult -> @@ -116,13 +129,60 @@ private class ScanNoteProcessor : ProductCommandProcessor { } } -private class ScanWalletProcessor : ProductCommandProcessor { +private class ScanWalletProcessor( + private val currenciesRepository: CurrenciesRepository?, + private val shouldDeriveWC: Boolean +) : ProductCommandProcessor { + override fun proceed( card: Card, session: CardSession, callback: (result: CompletionResult) -> Unit ) { - callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData))) + val derivationPaths = collectDerivationPaths(card)?.distinct() + val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + + if (derivationPaths.isNullOrEmpty() || wallet == null || wallet.chainCode == null) { + callback(CompletionResult.Success(ScanResponse(card, ProductType.Wallet, session.environment.walletData))) + return + } + + DeriveWalletPublicKeysTask(wallet.publicKey, derivationPaths).run(session) { result -> + when (result) { + is CompletionResult.Success -> { + val derivedKeys = mapOf(wallet.publicKey.toMapKey() to result.data) + val response = ScanResponse( + card, + ProductType.Wallet, + session.environment.walletData, + derivedKeys = derivedKeys + ) + callback(CompletionResult.Success(response)) + + } + is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) + } + } + } + + private fun collectDerivationPaths(card: Card): List? { + val currenciesRepository = currenciesRepository ?: return null + val cardCurrencies = currenciesRepository.loadCardCurrencies(card.cardId) + + val blockchainsToDerive = if (cardCurrencies == null) { + mutableListOf(Blockchain.Bitcoin, Blockchain.Ethereum) + } else { + val tokenBlockchains = cardCurrencies.tokens.map { it.blockchain } + (cardCurrencies.blockchains + tokenBlockchains).toMutableList() + } + + if (shouldDeriveWC) { + blockchainsToDerive.addAll(listOf(Blockchain.Ethereum, Blockchain.Binance, Blockchain.EthereumTestnet)) + } + + return blockchainsToDerive.toSet() + .filter { it.getSupportedCurves()?.contains(EllipticCurve.Secp256k1) == true } + .mapNotNull { it.derivationPath() } } } @@ -174,7 +234,7 @@ private class ScanOtherCardsProcessor : ProductCommandProcessor { return } - CreateProductWalletsTask(curvesToCreate).run(session) { result -> + CreateWalletsTask(curvesToCreate).run(session) { result -> when (result) { is CompletionResult.Success -> { PreflightReadTask(PreflightReadMode.FullCardRead, card.cardId).run(session) { readResult -> diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt index 9a6735ae2e..fc1ae2bcb2 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt @@ -68,7 +68,7 @@ class CurrenciesRepository(val context: Application) { } return try { - tokensAdapter.fromJson(json)!!.map { it.toToken() } + tokensAdapter.fromJson(json)!!.map { it.toToken() }.distinct() } catch (exception: Exception) { try { obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() } @@ -86,7 +86,7 @@ class CurrenciesRepository(val context: Application) { private fun loadSavedBlockchains(cardId: String): List { return try { val json = context.readFileText(getFileNameForBlockchains(cardId)) - blockchainsAdapter.fromJson(json) ?: emptyList() + blockchainsAdapter.fromJson(json)?.distinct() ?: emptyList() } catch (exception: Exception) { emptyList() } diff --git a/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt b/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt index 775d3748a7..9dd6e91960 100644 --- a/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/topup/TopUpManager.kt @@ -35,7 +35,7 @@ class TopUpManager { store.dispatch( GlobalAction.UpdateWalletSignedHashes( walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey, + walletPublicKey = walletManager.wallet.publicKey.seedKey, remainingSignatures = signResponse.remainingSignatures ) ) diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt index 400ee19ae3..872c130c30 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateFirstTwinWalletTask.kt @@ -4,8 +4,8 @@ import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable -import com.tangem.operations.wallet.CreateWalletCommand import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand import com.tangem.tap.domain.extensions.getSingleWallet @@ -20,15 +20,13 @@ class CreateFirstTwinWalletTask : CardSessionRunnable { when (response) { is CompletionResult.Success -> { session.environment.card = session.environment.card?.setWallets(emptyList()) - CreateWalletCommand(EllipticCurve.Secp256k1) - .run(session) { callback(it) } + CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) } } is CompletionResult.Failure -> callback(CompletionResult.Failure(response.error)) } } } else { - CreateWalletCommand(EllipticCurve.Secp256k1) - .run(session) { callback(it) } + CreateWalletTask(EllipticCurve.Secp256k1).run(session) { callback(it) } } } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt index 24b908cb0a..f7ef809d38 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/CreateSecondTwinWalletTask.kt @@ -8,8 +8,8 @@ import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError import com.tangem.common.extensions.hexToBytes -import com.tangem.operations.wallet.CreateWalletCommand import com.tangem.operations.wallet.CreateWalletResponse +import com.tangem.operations.wallet.CreateWalletTask import com.tangem.operations.wallet.PurgeWalletCommand import com.tangem.tap.domain.extensions.getSingleWallet import com.tangem.wallet.R @@ -47,7 +47,7 @@ class CreateSecondTwinWalletTask( private fun finishTask(session: CardSession, callback: (result: CompletionResult) -> Unit) { session.setMessage(creatingWalletMessage) - CreateWalletCommand(EllipticCurve.Secp256k1).run(session) { result -> + CreateWalletTask(EllipticCurve.Secp256k1).run(session) { result -> when (result) { is CompletionResult.Success -> { session.environment.card = session.environment.card?.updateWallet(result.data.wallet) diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index c035973ac7..6fb4bb28c6 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -23,7 +23,7 @@ class FinalizeTwinTask( PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> - ScanProductTask(readResult.data).run(session, callback) + ScanProductTask(readResult.data, null, false).run(session, callback) is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error)) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt index f93217e447..7c74264429 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt @@ -2,7 +2,6 @@ package com.tangem.tap.domain.walletconnect import com.tangem.blockchain.common.Blockchain import com.tangem.common.extensions.guard -import com.tangem.common.extensions.hexToBytes import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.redux.global.GlobalAction @@ -97,10 +96,10 @@ class WalletConnectManager { val activeData = sessions[session] ?: return removeSimilarSessions(activeData) + val key = activeData.wallet.derivedPublicKey ?: activeData.wallet.walletPublicKey + val accounts = listOf(Blockchain.Ethereum.makeAddresses(key).first().value) val approved = activeData.client.approveSession( - accounts = listOf(Blockchain.Ethereum.makeAddresses( - activeData.wallet.walletPublicKey.hexToBytes()).first().value - ), + accounts = accounts, chainId = activeData.wallet.chainId ) if (approved) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index e2ac447999..85ef0eaac6 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -5,14 +5,12 @@ import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.TransactionSender +import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.* import com.tangem.common.CompletionResult import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes +import com.tangem.crypto.CryptoUtils import com.tangem.operations.sign.SignHashCommand import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler import com.tangem.tap.common.extensions.toFormattedString @@ -36,9 +34,14 @@ class WalletConnectSdkHelper { type: WcTransactionType, ): WcTransactionData? { val factory = store.state.globalState.tapWalletManager.walletManagerFactory + val publicKey = Wallet.PublicKey( + session.wallet.walletPublicKey, + session.wallet.derivedPublicKey, + session.wallet.derivationPath, + ) val walletManager = factory.makeEthereumWalletManager( session.wallet.cardId, - session.wallet.walletPublicKey.hexToBytes(), + publicKey, emptyList(), isTestNet = session.wallet.isTestNet ) ?: return null @@ -151,7 +154,8 @@ class WalletConnectSdkHelper { val command = SignHashCommand( hash = dataToSign.hash, - walletPublicKey = data.walletManager.wallet.publicKey + walletPublicKey = data.walletManager.wallet.publicKey.seedKey, + derivationPath = data.walletManager.wallet.publicKey.derivationPath ) val result = tangemSdkManager.runTaskAsync(command, initialMessage = Message()) return when (result) { @@ -208,13 +212,13 @@ class WalletConnectSdkHelper { } suspend fun signPersonalMessage(hashToSign: ByteArray, wallet: WalletForSession): String? { - val publicKey = wallet.walletPublicKey.hexToBytes() - val command = SignHashCommand(hashToSign, publicKey) + val key = wallet.derivedPublicKey ?: wallet.walletPublicKey + val command = SignHashCommand(hashToSign, wallet.walletPublicKey, wallet.derivationPath) return when (val result = tangemSdkManager.runTaskAsync(command, wallet.cardId)) { is CompletionResult.Success -> { val hash = result.data.signature return EthereumUtils.prepareSignedMessageData( - hash, hashToSign, publicKey + hash, hashToSign, CryptoUtils.decompressPublicKey(key) ) } is CompletionResult.Failure -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 9c7a39b172..4cf2a6fde3 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -25,24 +25,26 @@ sealed class DetailsAction : Action { object ShowDisclaimer : DetailsAction() data class ReCreateTwinsWallet(val number: TwinCardNumber) : DetailsAction() - sealed class EraseWallet : DetailsAction() { - object Check : EraseWallet() - object Proceed : EraseWallet() { - object NotAllowedByCard : EraseWallet(), NotificationAction { + sealed class ResetToFactory : DetailsAction() { + object Check : ResetToFactory() + object Proceed : ResetToFactory() { + object NotAllowedByCard : ResetToFactory(), NotificationAction { override val messageResource = R.string.error_purge_prohibited } - object NotEmpty : EraseWallet(), NotificationAction { + object NotEmpty : ResetToFactory(), NotificationAction { override val messageResource = R.string.details_notification_erase_wallet_not_possible } } - object Confirm : EraseWallet() - object Cancel : EraseWallet() - object Failure : EraseWallet() - object Success : EraseWallet() + object Confirm : ResetToFactory() + object Cancel : ResetToFactory() + object Failure : ResetToFactory() + object Success : ResetToFactory() } + object CreateBackup : DetailsAction() + sealed class AppCurrencyAction : DetailsAction() { data class SetCurrencies(val currencies: List) : AppCurrencyAction() object ChooseAppCurrency : AppCurrencyAction() diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 05591c0e48..2a6d764f4c 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -36,7 +36,7 @@ class DetailsMiddleware { { action -> when (action) { is DetailsAction.PrepareScreen -> prepareData() - is DetailsAction.EraseWallet -> eraseWalletMiddleware.handle(action) + is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action) is DetailsAction.AppCurrencyAction -> appCurrencyMiddleware.handle(action) is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action) is DetailsAction.ShowDisclaimer -> { @@ -58,6 +58,12 @@ class DetailsMiddleware { } } } + is DetailsAction.CreateBackup -> { + store.state.detailsState.scanResponse?.let { + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + store.dispatch(GlobalAction.Onboarding.Start(it, fromHomeScreen = false)) + } + } } next(action) } @@ -86,25 +92,25 @@ class DetailsMiddleware { } class EraseWalletMiddleware() { - fun handle(action: DetailsAction.EraseWallet) { + fun handle(action: DetailsAction.ResetToFactory) { when (action) { - is DetailsAction.EraseWallet.Proceed -> { + is DetailsAction.ResetToFactory.Proceed -> { when (store.state.detailsState.eraseWalletState) { EraseWalletState.Allowed -> store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm)) EraseWalletState.NotAllowedByCard -> - store.dispatch(DetailsAction.EraseWallet.Proceed.NotAllowedByCard) + store.dispatch(DetailsAction.ResetToFactory.Proceed.NotAllowedByCard) EraseWalletState.NotEmpty -> - store.dispatch(DetailsAction.EraseWallet.Proceed.NotEmpty) + store.dispatch(DetailsAction.ResetToFactory.Proceed.NotEmpty) } } - is DetailsAction.EraseWallet.Cancel -> { + is DetailsAction.ResetToFactory.Cancel -> { store.dispatch(NavigationAction.PopBackTo()) } - is DetailsAction.EraseWallet.Confirm -> { + is DetailsAction.ResetToFactory.Confirm -> { val card = store.state.detailsState.scanResponse?.card ?: return scope.launch { - val result = tangemSdkManager.eraseWallet(card) + val result = tangemSdkManager.resetToFactorySettings(card) withContext(Dispatchers.Main) { when (result) { is CompletionResult.Success -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index ca2b6fb4bb..71f4bd5fd7 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -26,7 +26,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { is DetailsAction.PrepareScreen -> { handlePrepareScreen(action, detailsState) } - is DetailsAction.EraseWallet -> { + is DetailsAction.ResetToFactory -> { handleEraseWallet(action, detailsState) } is DetailsAction.AppCurrencyAction -> { @@ -39,23 +39,34 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { } } -private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState { +private fun handlePrepareScreen( + action: DetailsAction.PrepareScreen, + state: DetailsState, +): DetailsState { + val backupIsActive = action.scanResponse.card.backupStatus?.isActive ?: false + return DetailsState( scanResponse = action.scanResponse, wallets = action.wallets, cardInfo = action.scanResponse.card.toCardInfo(), appCurrencyState = AppCurrencyState(action.fiatCurrencyName), - cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card) + cardTermsOfUseUrl = action.cardTou.getUrl(action.scanResponse.card), + createBackupAllowed = action.scanResponse.isTangemWallet() && !backupIsActive, ) } -private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState { +private fun handleEraseWallet( + action: DetailsAction.ResetToFactory, + state: DetailsState, +): DetailsState { return when (action) { - DetailsAction.EraseWallet.Check -> { + DetailsAction.ResetToFactory.Check -> { val card = state.scanResponse?.card val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false val notAllowedByCard = notAllowedByAnyWallet || - (card?.isWalletDataSupported == true && !state.scanResponse.isTangemNote()) + (card?.isWalletDataSupported == true && + (!state.scanResponse.isTangemNote() && !state.scanResponse.isTangemWallet())) + val notEmpty = state.wallets.any { it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty() } @@ -66,22 +77,22 @@ private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsS } state.copy(eraseWalletState = eraseWalletState) } - DetailsAction.EraseWallet.Proceed -> { + DetailsAction.ResetToFactory.Proceed -> { if (state.eraseWalletState == EraseWalletState.Allowed) { state.copy(confirmScreenState = ConfirmScreenState.EraseWallet) } else { state } } - DetailsAction.EraseWallet.Cancel -> state.copy(eraseWalletState = null) - DetailsAction.EraseWallet.Failure -> state.copy(eraseWalletState = null) - DetailsAction.EraseWallet.Success -> state.copy(eraseWalletState = null) + DetailsAction.ResetToFactory.Cancel -> state.copy(eraseWalletState = null) + DetailsAction.ResetToFactory.Failure -> state.copy(eraseWalletState = null) + DetailsAction.ResetToFactory.Success -> state.copy(eraseWalletState = null) else -> state } } private fun handleAppCurrencyAction( - action: DetailsAction.AppCurrencyAction, state: DetailsState + action: DetailsAction.AppCurrencyAction, state: DetailsState, ): DetailsState { return when (action) { is DetailsAction.AppCurrencyAction.SetCurrencies -> { @@ -105,7 +116,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 -> { @@ -123,18 +134,18 @@ private fun handleSecurityAction( state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption)) } is DetailsAction.ManageSecurity.OpenSecurity -> { - if (state.scanResponse?.card?.isStart2Coin == true || - state.scanResponse?.isTangemNote() == true - ) { - return state.copy(securityScreenState = state.securityScreenState?.copy( - allowedOptions = EnumSet.of(SecurityOption.LongTap), - selectedOption = state.securityScreenState.currentOption - )) + val allowedSecurityOptions = when { + state.scanResponse?.card?.isStart2Coin == true || + state.scanResponse?.isTangemNote() == true -> { + EnumSet.of(SecurityOption.LongTap) + } + state.scanResponse?.isTangemWallet() == true -> { + EnumSet.of(state.securityScreenState?.currentOption) + } + else -> prepareAllowedSecurityOptions( + state.scanResponse?.card, state.securityScreenState?.currentOption + ) } - - val allowedSecurityOptions = prepareAllowedSecurityOptions( - state.scanResponse?.card, state.securityScreenState?.currentOption - ) state.copy(securityScreenState = state.securityScreenState?.copy( allowedOptions = allowedSecurityOptions, selectedOption = state.securityScreenState.currentOption @@ -171,18 +182,13 @@ private fun handleSecurityAction( } private fun prepareAllowedSecurityOptions( - card: Card?, currentSecurityOption: SecurityOption? + card: Card?, currentSecurityOption: SecurityOption?, ): EnumSet { - val prohibitDefaultPin = card?.settings?.isResettingUserCodesAllowed != true - - val allowedSecurityOptions = EnumSet.noneOf(SecurityOption::class.java) + val allowedSecurityOptions = EnumSet.of(SecurityOption.LongTap) if (card?.isTangemTwin() == true) { allowedSecurityOptions.add(SecurityOption.PassCode) } - if ((currentSecurityOption == SecurityOption.LongTap) || !prohibitDefaultPin) { - allowedSecurityOptions.add(SecurityOption.LongTap) - } if (currentSecurityOption == SecurityOption.AccessCode) { allowedSecurityOptions.add(SecurityOption.AccessCode) } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index cad9493b63..8f55ff5901 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -22,6 +22,7 @@ data class DetailsState( val confirmScreenState: ConfirmScreenState? = null, val securityScreenState: SecurityScreenState? = null, val cardTermsOfUseUrl: Uri? = null, + val createBackupAllowed: Boolean = false, ) : StateType { // if you do not delegate - the application crashes on startup, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt index dff6c35c72..2c1aab1596 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt @@ -1,9 +1,7 @@ package com.tangem.tap.features.details.redux.walletconnect import com.tangem.blockchain.common.* -import com.tangem.common.card.Card import com.tangem.common.extensions.guard -import com.tangem.common.extensions.toHexString import com.tangem.tap.* import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.getFromClipboard @@ -14,6 +12,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.domain.TapWorkarounds.isTestCard import com.tangem.tap.domain.extensions.makeWalletManagerForApp import com.tangem.tap.domain.isMultiwalletAllowed +import com.tangem.tap.domain.tasks.product.ScanResponse import com.tangem.tap.domain.walletconnect.WalletConnectManager import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.wallet.R @@ -128,24 +127,32 @@ class WalletConnectMiddleware { } private fun handleScanCard(wcUri: String) { - store.dispatch(GlobalAction.ScanCard({ scanResponse -> + store.dispatch(GlobalAction.ScanCard(true, { scanResponse -> val card = scanResponse.card if (!card.isMultiwalletAllowed) { store.dispatchOnMain(WalletConnectAction.UnsupportedCard) return@ScanCard } - val walletManager = getWalletManager(card).guard { + val walletManager = getWalletManager(scanResponse).guard { store.dispatchOnMain(WalletConnectAction.UnsupportedCard) return@ScanCard } - val key = walletManager.wallet.publicKey + val wallet = walletManager.wallet + val derivedKey = if (wallet.publicKey.blockchainKey.contentEquals(wallet.publicKey.seedKey)) { + null + } else { + walletManager.wallet.publicKey.blockchainKey + } store.dispatchOnMain(WalletConnectAction.OpenSession( wcUri = wcUri, wallet = WalletForSession( - card.cardId, key.toHexString(), - isTestNet = card.isTestCard + card.cardId, + wallet.publicKey.seedKey, + derivedKey, + wallet.publicKey.derivationPath, + card.isTestCard ), )) }, { @@ -153,26 +160,32 @@ class WalletConnectMiddleware { }, R.string.wallet_connect_scan_card_message)) } - private fun getWalletManager(card: Card): WalletManager? { + private fun getWalletManager(scanResponse: ScanResponse): WalletManager? { + val card = scanResponse.card val factory = store.state.globalState.tapWalletManager.walletManagerFactory + val wcBlockchain = if (scanResponse.card.isTestCard) { + Blockchain.EthereumTestnet + } else { + Blockchain.Ethereum + } + return if (store.state.globalState.scanResponse?.card?.cardId == card.cardId) { - store.state.walletState.getWalletManager(Blockchain.Ethereum) - ?: factory.makeWalletManagerForApp(card, Blockchain.Ethereum) + store.state.walletState.getWalletManager(wcBlockchain) + ?: factory.makeWalletManagerForApp(scanResponse, wcBlockchain) ?.also { walletManager -> store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) } } else { if (currenciesRepository.loadCardCurrencies(card.cardId)?.blockchains?.contains( - Blockchain.Ethereum) == true + wcBlockchain) == true ) { - factory.makeWalletManagerForApp(card, Blockchain.Ethereum) + factory.makeWalletManagerForApp(scanResponse, wcBlockchain) } else { - factory.makeWalletManagerForApp(card, - Blockchain.Ethereum) + factory.makeWalletManagerForApp(scanResponse, wcBlockchain) ?.also { - currenciesRepository.saveAddedBlockchain(card.cardId, Blockchain.Ethereum) + currenciesRepository.saveAddedBlockchain(card.cardId, wcBlockchain) } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt index 38aeaabc84..2757d8cea9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt @@ -1,7 +1,9 @@ package com.tangem.tap.features.details.redux.walletconnect +import com.squareup.moshi.JsonClass import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager +import com.tangem.common.hdWallet.DerivationPath import com.tangem.tap.common.redux.StateDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData @@ -21,9 +23,12 @@ data class WalletConnectSession( val peerMeta: WCPeerMeta, ) +@JsonClass(generateAdapter = true) data class WalletForSession( val cardId: String, - val walletPublicKey: String, + val walletPublicKey: ByteArray, + val derivedPublicKey: ByteArray?, + val derivationPath: DerivationPath?, val isTestNet: Boolean = false, ) { val chainId diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt index 7439dee4ec..8e6dc116d8 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsConfirmFragment.kt @@ -58,13 +58,13 @@ class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm), when (state.confirmScreenState) { ConfirmScreenState.EraseWallet -> { - toolbar.title = getString(R.string.details_row_title_erase_wallet) - tv_warning_description.text = getString(R.string.details_erase_wallet_warning) - btn_confirm.text = getString(R.string.details_row_title_erase_wallet) + toolbar.title = getString(R.string.details_row_title_reset_factory_settings) + tv_warning_description.text = getString(R.string.details_row_title_reset_factory_settings_warning) + btn_confirm.text = getString(R.string.details_row_title_reset_factory_settings) btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds( null, null, getDrawable(R.drawable.ic_send), null ) - btn_confirm.setOnClickListener { store.dispatch(DetailsAction.EraseWallet.Confirm) } + btn_confirm.setOnClickListener { store.dispatch(DetailsAction.ResetToFactory.Confirm) } } ConfirmScreenState.LongTap, ConfirmScreenState.AccessCode, ConfirmScreenState.PassCode -> { diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt index 5a1864ec3f..74d3f40fd9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/DetailsFragment.kt @@ -89,20 +89,24 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber = { dispatch, state -> store.dispatch(GlobalAction.RestoreAppCurrency) store.dispatch(GlobalAction.GetMoonPayStatus) store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted())) + store.dispatch(BackupAction.CheckForUnfinishedBackup) } is HomeAction.ShouldScanCardOnResume -> { if (action.shouldScanCard) { @@ -59,7 +60,7 @@ private fun handleReadCard() { store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer)) } else { changeButtonState(ButtonState.PROGRESS) - store.dispatch(GlobalAction.ScanCard({ scanResponse -> + store.dispatch(GlobalAction.ScanCard(false, { scanResponse -> store.state.globalState.tapWalletManager.updateConfigManager(scanResponse) store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt index 558f5b0e5f..36e9c93cc1 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt @@ -42,7 +42,7 @@ private val onboardingNoteMiddleware: Middleware = { dispatch, state - private fun handleNoteAction(action: Action, dispatch: DispatchFunction) { if (action !is OnboardingNoteAction) return val globalState = store.state.globalState - val onboardingManager = globalState.onboardingManager ?: return + val onboardingManager = globalState.onboardingState.onboardingManager ?: return val scanResponse = onboardingManager.scanResponse val card = onboardingManager.scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt index 4606c539ae..f3419e67fa 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt @@ -37,7 +37,7 @@ private val onboardingOtherCardsMiddleware: Middleware = { dispatch, s private fun handleOtherCardsAction(action: Action, dispatch: DispatchFunction) { if (action !is OnboardingOtherCardsAction) return val globalState = store.state.globalState - val onboardingManager = globalState.onboardingManager ?: return + val onboardingManager = globalState.onboardingState.onboardingManager ?: return val card = onboardingManager.scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt index 818b1974aa..f69e59381f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt @@ -45,7 +45,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) { val action = action as? TwinCardsAction ?: return val globalState = store.state.globalState - val onboardingManager = globalState.onboardingManager + val onboardingManager = globalState.onboardingState.onboardingManager val twinCardsState = store.state.twinCardsState fun getScanResponse(): ScanResponse { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/OnboardingWalletFragment.kt deleted file mode 100644 index 69c66581a6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/OnboardingWalletFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -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.store -import com.tangem.wallet.R -import org.rekotlin.StoreSubscriber - -/** -[REDACTED_AUTHOR] - */ -class OnboardingWalletFragment : BaseStoreFragment(R.layout.fragment_onboarding_wallet), StoreSubscriber { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val inflater = TransitionInflater.from(requireContext()) - enterTransition = inflater.inflateTransition(R.transition.fade) - exitTransition = inflater.inflateTransition(R.transition.fade) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - } - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.onboardingWalletState == newState.onboardingWalletState - }.select { it.onboardingWalletState } - } - storeSubscribersList.add(this) - } - - override fun newState(state: OnboardingWalletState) { - if (activity == null) return - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index 5cfd213d57..b3056d7bdd 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -1,19 +1,67 @@ package com.tangem.tap.features.onboarding.products.wallet.redux +import android.graphics.Bitmap +import com.tangem.tap.features.wallet.redux.Artwork import org.rekotlin.Action -/** -[REDACTED_AUTHOR] - */ sealed class OnboardingWalletAction : Action { object Init : OnboardingWalletAction() + object GetToCreateWalletStep : OnboardingWalletAction() object CreateWallet : OnboardingWalletAction() - object TopUp : OnboardingWalletAction() object Done : OnboardingWalletAction() + object FinishOnboarding : OnboardingWalletAction() object ProceedBackup : OnboardingWalletAction() - sealed class Backup : Action { + object LoadArtwork : OnboardingWalletAction() + class SetArtworkUrl(val artwork: Artwork) : OnboardingWalletAction() + object OnBackPressed : OnboardingWalletAction() +} + +sealed class BackupAction : Action { + + object DetermineBackupStep : BackupAction() + object IntroduceBackup : BackupAction() + object StartBackup : BackupAction() + object DismissBackup : BackupAction() + object ScanPrimaryCard : BackupAction() + + object CheckForUnfinishedBackup : BackupAction() + + object StartAddingBackupCards : BackupAction() + object AddBackupCard : BackupAction() { + object Success : BackupAction() } + + data class LoadBackupCardArtwork( + val cardId: CardId, + val cardPublicKey: ByteArray, + ) : BackupAction() { + data class Success(val cardId: CardId, val artwork: Bitmap) + } + + object GoToShop : BackupAction() + object FinishAddingBackupCards : BackupAction() + + object ShowAccessCodeInfoScreen : BackupAction() + object ShowEnterAccessCodeScreen : BackupAction() + data class CheckAccessCode(val accessCode: String) : BackupAction() + data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction() + data class SaveFirstAccessCode(val accessCode: String) : BackupAction() + object ShowReenterAccessCodeScreen : BackupAction() + data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction() + object OnAccessCodeDialogClosed : BackupAction() + + data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() + data class WriteBackupCard(val cardNumber: Int) : BackupAction() + + object PrepareToWritePrimaryCard : BackupAction() + object WritePrimaryCard: BackupAction() + + object FinishBackup : BackupAction() + object DiscardBackup : BackupAction() + object DiscardSavedBackup : BackupAction() + object ResumeBackup : BackupAction() + } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt index c442090de4..f9465751c3 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt @@ -1,18 +1,282 @@ package com.tangem.tap.features.onboarding.products.wallet.redux +import com.tangem.common.CompletionResult +import com.tangem.common.card.Card +import com.tangem.operations.backup.BackupService +import com.tangem.tap.* +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchOpenUrl +import com.tangem.tap.common.extensions.withMainContext import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.AppScreen +import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.domain.extensions.hasWallets +import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.home.redux.HomeAction +import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL +import kotlinx.coroutines.launch +import org.rekotlin.Action import org.rekotlin.Middleware class OnboardingWalletMiddleware { companion object { val handler = onboardingWalletMiddleware + + const val BUY_WALLET_URL = "https://wallet.tangem.com/" } } private val onboardingWalletMiddleware: Middleware = { dispatch, state -> { next -> { action -> + handleWalletAction(action) next(action) } } +} + +private fun handleWalletAction(action: Action) { + if (action !is OnboardingWalletAction) return + val globalState = store.state.globalState + val onboardingManager = globalState.onboardingState.onboardingManager + + val scanResponse = onboardingManager?.scanResponse + val card = scanResponse?.card + + val walletState = store.state.onboardingWalletState + + when (action) { + OnboardingWalletAction.Init -> { + val action = when { + card == null -> { + OnboardingWalletAction.ProceedBackup + } + card.hasWallets() && card.backupStatus == Card.BackupStatus.NoBackup -> + OnboardingWalletAction.ProceedBackup + card.hasWallets() && card.backupStatus?.isActive == true -> + BackupAction.FinishBackup + else -> OnboardingWalletAction.GetToCreateWalletStep + } + store.dispatch(action) + } + is OnboardingNoteAction.LoadCardArtwork -> { +// scope.launch { +// val artwork = onboardingManager.loadArtwork() +// store.dispatchOnMain(OnboardingWalletAction.SetArtworkUrl(artwork)) +// } + } + is OnboardingWalletAction.CreateWallet -> { + scope.launch { + scanResponse ?: return@launch + val result = tangemSdkManager.createProductWallet(scanResponse) + withMainContext { + when (result) { + is CompletionResult.Success -> { + //here we must use updated scanResponse after createWallet & derivation + val updatedResponse = + globalState.onboardingState.onboardingManager.scanResponse.copy(card = result.data) + onboardingManager.scanResponse = updatedResponse + onboardingManager.activationStarted(updatedResponse.card.cardId) + store.dispatch(OnboardingWalletAction.ProceedBackup) + } + is CompletionResult.Failure -> { +// do nothing + } + } + } + } + } + OnboardingWalletAction.FinishOnboarding -> { + store.dispatch(GlobalAction.Onboarding.Stop) + + if (scanResponse == null) { + store.dispatch(NavigationAction.PopBackTo()) + store.dispatch(HomeAction.ReadCard) + } else { + val backupState = store.state.onboardingWalletState.backupState + val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) + scope.launch { globalState.tapWalletManager.onCardScanned(updatedScanResponse) } + store.dispatchOnMain(NavigationAction.NavigateTo(AppScreen.Wallet)) + } + } + OnboardingWalletAction.ProceedBackup -> { + val newAction = when (val backupState = backupService.currentState) { + BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard + is BackupService.State.FinalizingBackupCard -> + BackupAction.PrepareToWriteBackupCard(backupState.index) + else -> BackupAction.IntroduceBackup + } + store.dispatch(newAction) + } + OnboardingWalletAction.OnBackPressed -> { + when (walletState.backupState.backupStep) { + BackupStep.InitBackup, BackupStep.Finished -> store.dispatch(NavigationAction.PopBackTo()) + BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, + BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, + -> { + store.dispatch(BackupAction.DiscardBackup) + store.dispatch(NavigationAction.PopBackTo()) + } + is BackupStep.WriteBackupCard -> + store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) + } + } + } +} + +private fun updateScanResponseAfterBackup( + scanResponse: ScanResponse, backupState: BackupState, +): ScanResponse { + val card = if (backupState.backupCardsNumber > 0) { + val cardsCount = backupState.backupCardsNumber + scanResponse.card.copy( + backupStatus = Card.BackupStatus.Active(cardCount = cardsCount), + isAccessCodeSet = true + ) + } else { + scanResponse.card + } + return scanResponse.copy(card = card) +} + +class BackupMiddleware { + val backupMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + if (action is BackupAction) handleBackupAction(action) + next(action) + } + } + } +} + +private fun handleBackupAction(action: BackupAction) { + val backupState = store.state.onboardingWalletState.backupState + + val globalState = store.state.globalState + val onboardingManager = globalState.onboardingState.onboardingManager + + val scanResponse = onboardingManager?.scanResponse + val card = scanResponse?.card + + when (action) { + is BackupAction.StartBackup -> { + backupService.discardSavedBackup() + } + is BackupAction.ScanPrimaryCard -> { + backupService.readPrimaryCard(cardId = card?.cardId) { result -> + when (result) { + is CompletionResult.Success -> { + store.dispatchOnMain(BackupAction.StartAddingBackupCards) + } + is CompletionResult.Failure -> { + } + } + } + } + is BackupAction.AddBackupCard -> { + backupService.addBackupCard { result -> + when (result) { + is CompletionResult.Success -> { + store.dispatchOnMain(BackupAction.AddBackupCard.Success) + } + is CompletionResult.Failure -> { + + } + } + } + } + is BackupAction.GoToShop -> { + store.dispatchOpenUrl(BUY_WALLET_URL) + } + is BackupAction.FinishAddingBackupCards -> { + if (backupService.addedBackupCardsCount == 1) { + store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards)) + } + if (backupService.addedBackupCardsCount == 2) { + store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen) + } + } + is BackupAction.CheckAccessCode -> { + if (action.accessCode.length < 4) { + store.dispatch(BackupAction.SetAccessCodeError( + AccessCodeError.CodeTooShort + )) + } else { + store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode)) + } + } + is BackupAction.SaveAccessCodeConfirmation -> { + if (action.accessCodeConfirmation == backupState.accessCode) { + backupService.setAccessCode(action.accessCodeConfirmation) + store.dispatch(BackupAction.PrepareToWritePrimaryCard) + } else { + store.dispatch(BackupAction.SetAccessCodeError( + AccessCodeError.CodesDoNotMatch + )) + } + } + is BackupAction.WritePrimaryCard -> { + backupService.proceedBackup { result -> + when (result) { + is CompletionResult.Success -> { + store.dispatchOnMain( + BackupAction.PrepareToWriteBackupCard(1) + ) + } + is CompletionResult.Failure -> { + } + } + } + } + is BackupAction.WriteBackupCard -> { + backupService.proceedBackup { result -> + when (result) { + is CompletionResult.Success -> { + if (backupService.currentState == BackupService.State.Finished) { + store.dispatchOnMain(BackupAction.FinishBackup) + } else { + store.dispatchOnMain( + BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1) + ) + } + } + is CompletionResult.Failure -> { + + } + } + } + } + is BackupAction.FinishBackup -> { + (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) + .distinct().filterNotNull() + .forEach { cardId -> + preferencesStorage.usedCardsPrefStorage.activationFinished(cardId) + } + } + is BackupAction.DiscardBackup -> { + backupService.discardSavedBackup() + } + is BackupAction.DiscardSavedBackup -> { + backupService.primaryCardId?.let { + preferencesStorage.usedCardsPrefStorage.activationFinished(it) + } + backupService.discardSavedBackup() + } + is BackupAction.CheckForUnfinishedBackup -> { + if (backupService.hasIncompletedBackup) { + store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound)) + } + } + is BackupAction.ResumeBackup -> { + store.dispatch(GlobalAction.Onboarding.Start(null, true)) + store.dispatch(OnboardingWalletAction.ProceedBackup) + store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet)) + } + is BackupAction.DismissBackup -> { + store.dispatch(BackupAction.FinishBackup) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt index 427f44048e..3947cd5d3f 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt @@ -1,20 +1,134 @@ package com.tangem.tap.features.onboarding.products.wallet.redux +import com.tangem.tap.backupService import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction import org.rekotlin.Action class OnboardingWalletReducer { companion object { - fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state) + fun reduce(action: Action, state: AppState): OnboardingWalletState = + internalReduce(action, state) } } private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState { - var state = appState.onboardingWalletState - - when (action) { + val state = appState.onboardingWalletState + return when (action) { + is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState)) + is GlobalAction.Onboarding.Start -> OnboardingWalletState( + backupState = state.backupState.copy(canSkipBackup = action.fromHomeScreen) + ) + is OnboardingWalletAction.GetToCreateWalletStep -> + OnboardingWalletState(step = OnboardingWalletStep.CreateWallet) + is OnboardingWalletAction.ProceedBackup -> state.copy(step = OnboardingWalletStep.Backup) + is OnboardingWalletAction.SetArtworkUrl -> { + state.copy(artwork = action.artwork) + } + is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done) + else -> state } +} - return state + +class BackupReducer { + companion object { + fun reduce( + action: BackupAction, state: BackupState, + ): BackupState { + + return when (action) { + BackupAction.IntroduceBackup -> BackupState( + backupStep = BackupStep.InitBackup, + canSkipBackup = state.canSkipBackup + ) + + BackupAction.StartBackup -> state.copy(backupStep = BackupStep.ScanOriginCard) + + BackupAction.StartAddingBackupCards -> { + state.copy(backupStep = BackupStep.AddBackupCards) + } + BackupAction.AddBackupCard.Success -> { + state.copy(backupCardsNumber = state.backupCardsNumber + 1) + } + BackupAction.FinishAddingBackupCards -> { + state + } + BackupAction.ShowAccessCodeInfoScreen -> { + state.copy(backupStep = BackupStep.SetAccessCode) + } + BackupAction.ShowEnterAccessCodeScreen -> { + state.copy(backupStep = BackupStep.EnterAccessCode) + } + is BackupAction.SaveFirstAccessCode -> { + state.copy( + backupStep = BackupStep.ReenterAccessCode, + accessCode = action.accessCode + ) + } + is BackupAction.SetAccessCodeError -> { + state.copy( + accessCodeError = action.error + ) + } + is BackupAction.PrepareToWritePrimaryCard -> { + if (state.primaryCardId == null) { + state.copy( + primaryCardId = backupService.primaryCardId, + backupCardIds = backupService.backupCardIds, + backupCardsNumber = backupService.backupCardIds.size, + backupStep = BackupStep.WritePrimaryCard + ) + } else { + state.copy( + backupStep = BackupStep.WritePrimaryCard + ) + } + } + is BackupAction.PrepareToWriteBackupCard -> { + if (state.primaryCardId == null) { + state.copy( + primaryCardId = backupService.primaryCardId, + backupCardIds = backupService.backupCardIds, + backupCardsNumber = backupService.backupCardIds.size, + backupStep = BackupStep.WriteBackupCard(action.cardNumber) + ) + } else { + state.copy( + backupStep = BackupStep.WriteBackupCard(action.cardNumber) + ) + } + } + + is BackupAction.FinishBackup -> { + state.copy( + backupStep = BackupStep.Finished + ) + } + + BackupAction.OnAccessCodeDialogClosed -> { + state.copy(backupStep = BackupStep.AddBackupCards) + } + + BackupAction.WritePrimaryCard -> state + is BackupAction.WriteBackupCard -> state + is BackupAction.SaveAccessCodeConfirmation -> state + BackupAction.ScanPrimaryCard -> state + BackupAction.ShowEnterAccessCodeScreen -> state + BackupAction.ShowReenterAccessCodeScreen -> state + BackupAction.AddBackupCard -> state + BackupAction.DismissBackup -> state + is BackupAction.LoadBackupCardArtwork -> state + is BackupAction.CheckAccessCode -> state + BackupAction.GoToShop -> state + BackupAction.DetermineBackupStep -> state + BackupAction.CheckForUnfinishedBackup -> state + BackupAction.DiscardBackup -> state + BackupAction.ResumeBackup -> state + BackupAction.DiscardSavedBackup -> state + } + + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt index 5207a85b3d..d8d0c687b7 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt @@ -1,14 +1,81 @@ package com.tangem.tap.features.onboarding.products.wallet.redux +import android.graphics.Bitmap +import com.tangem.tap.common.redux.StateDialog +import com.tangem.tap.features.wallet.redux.Artwork import org.rekotlin.StateType /** [REDACTED_AUTHOR] */ data class OnboardingWalletState( - val any: String? = null -) : StateType + val step: OnboardingWalletStep = OnboardingWalletStep.None, + val backupState: BackupState = BackupState(), + val artwork: Artwork? = null, + val showConfetti: Boolean = false, +) : StateType { + + fun getProgressStep(): Int { + return when { + step == OnboardingWalletStep.CreateWallet -> { + 1 + } + step == OnboardingWalletStep.Backup -> { + when (backupState.backupStep) { + BackupStep.InitBackup -> 2 + BackupStep.ScanOriginCard -> 3 + + BackupStep.AddBackupCards -> 4 + BackupStep.EnterAccessCode -> 4 + BackupStep.ReenterAccessCode -> 4 + BackupStep.SetAccessCode -> 4 + BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5 + BackupStep.Finished -> 6 + } + } + step == OnboardingWalletStep.Done -> 6 + else -> 1 + } + } +} enum class OnboardingWalletStep { - None, CreateWallet, TopUpWallet, Backup, Done + None, CreateWallet, Backup, Done +} + +data class BackupState( + val primaryCardId: String? = null, + val backupCardsNumber: Int = 0, + val backupCardIds: List = emptyList(), + val backupCardsArtworks: Map = emptyMap(), + val accessCode: String = "", + val accessCodeError: AccessCodeError? = null, + val backupStep: BackupStep = BackupStep.InitBackup, + val maxBackupCards: Int = 2, + val canSkipBackup: Boolean = true +) + +enum class AccessCodeError { + CodeTooShort, CodesDoNotMatch +} + +typealias CardId = String + +sealed class BackupStep { + object InitBackup : BackupStep() + object ScanOriginCard : BackupStep() + object AddBackupCards : BackupStep() + object SetAccessCode : BackupStep() + object EnterAccessCode : BackupStep() + object ReenterAccessCode : BackupStep() + object WritePrimaryCard : BackupStep() + data class WriteBackupCard(val cardNumber: Int) : BackupStep() + object Finished : BackupStep() +} + +sealed class BackupDialog: StateDialog { + object AddMoreBackupCards : StateDialog + object BackupInProgress : StateDialog + object UnfinishedBackupFound : StateDialog + object ConfirmDiscardingBackup : StateDialog } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt new file mode 100644 index 0000000000..d5b5065f36 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupCardsWidget.kt @@ -0,0 +1,234 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.PropertyValuesHolder +import android.view.View +import android.widget.ImageView +import androidx.core.animation.doOnEnd +import com.tangem.tap.common.leapfrogWidget.LeapView +import com.tangem.tap.common.leapfrogWidget.LeapViewState +import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget + +class BackupCardsWidget( + val leapfrogWidget: LeapfrogWidget, + val getTopOfAnchorViewForActivateState: () -> Float, +) { + + fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) { + val animator = createAnimator(animate, onEnd) + animator.playTogether( + createAnimator(BackupCardType.ORIGIN, createWelcomeProperties(BackupCardType.ORIGIN)), + createAnimator(BackupCardType.FIRST_BACKUP, createWelcomeProperties(BackupCardType.FIRST_BACKUP)), + createAnimator(BackupCardType.SECOND_BACKUP, createWelcomeProperties(BackupCardType.SECOND_BACKUP)) + ) + leapfrogWidget.fold { animator.start() } + } + + fun toFolded(animate: Boolean = true, onEnd: () -> Unit = {}) { + val animator = createAnimator(animate, onEnd) + animator.playTogether( + createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), + createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), + createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)) + ) + leapfrogWidget.fold { animator.start() } + } + + fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { + val animator = createAnimator(animate, onEnd) + animator.playTogether( + createAnimator(BackupCardType.ORIGIN, createFanProperties(BackupCardType.ORIGIN)), + createAnimator(BackupCardType.FIRST_BACKUP, createFanProperties(BackupCardType.FIRST_BACKUP)), + createAnimator(BackupCardType.SECOND_BACKUP, createFanProperties(BackupCardType.SECOND_BACKUP)) + ) + leapfrogWidget.fold { animator.start() } + } + + fun toLeapfrog(animate: Boolean = true, onEnd: () -> Unit = {}) { + val animator = createAnimator(animate, onEnd) + animator.playTogether( + createAnimator(BackupCardType.ORIGIN, createLeapfrogProperties(BackupCardType.ORIGIN)), + createAnimator(BackupCardType.FIRST_BACKUP, createLeapfrogProperties(BackupCardType.FIRST_BACKUP)), + createAnimator(BackupCardType.SECOND_BACKUP, createLeapfrogProperties(BackupCardType.SECOND_BACKUP)) + ) + leapfrogWidget.fold { + animator.doOnEnd { + leapfrogWidget.initViews() + leapfrogWidget.unfold() + } + animator.start() + } + } +// +// fun toActivate(animate: Boolean = true, onEnd: () -> Unit = {}) { +// val animator = createAnimator(animate, onEnd) +// animator.playTogether( +// createAnimator(TwinCardNumber.First, createActivateProperties(TwinCardNumber.First)), +// createAnimator(TwinCardNumber.Second, createActivateProperties(TwinCardNumber.Second)) +// ) +// animator.start() +// } + + private fun createAnimator(animate: Boolean, onEnd: () -> Unit): AnimatorSet { + return AnimatorSet().apply { + duration = if (animate) 400 else 0 + doOnEnd { onEnd() } + } + } + + private fun createAnimator( + cardType: BackupCardType, + properties: CardProperties, + ): ObjectAnimator { + val view = getLeapViewByCardNumber(cardType).view + val animator = ObjectAnimator.ofPropertyValuesHolder(view, + *properties.createValuesHolders().toTypedArray()) + view.elevation = properties.elevation + return animator + } + + private fun createWelcomeProperties(cardType: BackupCardType): CardProperties { + return when (cardType) { + BackupCardType.ORIGIN -> CardProperties( + xTranslation = 440f, + yTranslation = 0f, + rotation = 75f, + elevation = 2f, + scale = .9f, + ) + BackupCardType.FIRST_BACKUP -> CardProperties( + xTranslation = 10f, + yTranslation = -100f, + rotation = 100f, + elevation = 1f, + scale = .9f, +) + BackupCardType.SECOND_BACKUP -> CardProperties( + xTranslation = -440f, + yTranslation = -100f, + rotation = 70f, + elevation = 0f, + scale = .9f, + ) + } + } + + private fun createFanProperties(cardType: BackupCardType): CardProperties { + return when (cardType) { + BackupCardType.ORIGIN -> CardProperties( + xTranslation = 0f, + yTranslation = 0f, + rotation = 5f, + elevation = 2f, + scale = 1f, + ) + BackupCardType.FIRST_BACKUP -> CardProperties( + xTranslation = 0f, + yTranslation = -80f, + rotation = -5f, + elevation = 1f, + scale = 0.9f, + ) + BackupCardType.SECOND_BACKUP -> CardProperties( + xTranslation = 10f, + yTranslation = -170f, + rotation = -20f, + elevation = 0f, + scale = 0.8f, + ) + } + } + + private fun createLeapfrogProperties(cardType: BackupCardType): CardProperties { + return when (cardType) { + BackupCardType.ORIGIN -> CardProperties( + xTranslation = 0f, + yTranslation = 0f, + rotation = 0f, + elevation = 2f, + scale = 1f, + ) + BackupCardType.FIRST_BACKUP -> CardProperties( + xTranslation = 0f, + yTranslation = 0f, + rotation = 0f, + elevation = 1f, + scale = 0.9f, + ) + BackupCardType.SECOND_BACKUP -> CardProperties( + xTranslation = 0f, + yTranslation = 0f, + rotation = 0f, + elevation = 0f, + scale = 0.8f, + ) + } + } + + private fun createActivateProperties(cardType: BackupCardType): CardProperties { + val topOfAnchorView = getTopOfAnchorViewForActivateState() + val twinProperties = CardProperties.from(getLeapViewByCardNumber(cardType).state) + return twinProperties.copy( + xTranslation = twinProperties.xTranslation, + yTranslation = twinProperties.yTranslation - topOfAnchorView, + rotation = 0f, + scale = twinProperties.scale - 0.4f, + ) + } + + private fun getLeapViewByCardNumber(cardType: BackupCardType): LeapView { + return when (cardType) { + BackupCardType.ORIGIN -> leapfrogWidget.getViewByPosition(0) + BackupCardType.FIRST_BACKUP -> leapfrogWidget.getViewByPosition(1) + BackupCardType.SECOND_BACKUP -> leapfrogWidget.getViewByPosition(2) + } + } + + fun getOriginCardView(): ImageView { + return getLeapViewByCardNumber(BackupCardType.ORIGIN).view as ImageView + } + + fun getFirstBackupCardView(): ImageView { + return getLeapViewByCardNumber(BackupCardType.FIRST_BACKUP).view as ImageView + } + + fun getSecondBackupCardView(): ImageView { + return getLeapViewByCardNumber(BackupCardType.SECOND_BACKUP).view as ImageView + } +} + + +enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP } + + +private data class CardProperties( + val xTranslation: Float = 0f, + val yTranslation: Float = 0f, + val rotation: Float = 0f, + val elevation: Float = 0f, + val scale: Float = 1.1f, +) { + + fun createValuesHolders(): List { + return listOf( + PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation), + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation), + PropertyValuesHolder.ofFloat(View.ROTATION, rotation), + PropertyValuesHolder.ofFloat(View.SCALE_X, scale), + PropertyValuesHolder.ofFloat(View.SCALE_Y, scale), + ) + } + + companion object { + fun from(leapViewState: LeapViewState): CardProperties { + val leapViewProperties = leapViewState.properties + + return CardProperties( + yTranslation = leapViewProperties.yTranslation, + elevation = leapViewProperties.elevationEnd, + scale = leapViewProperties.scale, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt new file mode 100644 index 0000000000..f7044f9986 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt @@ -0,0 +1,54 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.recyclerview.widget.RecyclerView +import com.tangem.tap.common.extensions.getString +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.item_backup_info_adapter.view.* + +class BackupInfoAdapter : RecyclerView.Adapter() { + + private val data = backupInfoSnippets + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder = + BackupInfoViewHolder(LayoutInflater.from(parent.context) + .inflate(R.layout.item_backup_info_adapter, parent, false)) + + override fun getItemCount(): Int = data.size + + override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) = + holder.itemView.run { + tv_header.text = getString(data[position].header) + tv_body.text = getString(data[position].body) + } +} + +private class BackupInfo( + @StringRes val header: Int, + @StringRes val body: Int, +) + +private val backupInfoSnippets = listOf( + BackupInfo( + R.string.onboarding_wallet_info_title_first, + R.string.onboarding_wallet_info_subtitle_first + ), + BackupInfo( + R.string.onboarding_wallet_info_title_second, + R.string.onboarding_wallet_info_subtitle_second + ), + BackupInfo( + R.string.onboarding_wallet_info_title_third, + R.string.onboarding_wallet_info_subtitle_third + ), + BackupInfo( + R.string.onboarding_wallet_info_title_fourth, + R.string.onboarding_wallet_info_subtitle_fourth + ), + + ) + +class BackupInfoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt new file mode 100644 index 0000000000..f415e176b9 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -0,0 +1,385 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui + +import android.os.Bundle +import android.view.* +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.transition.TransitionInflater +import androidx.transition.TransitionManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.tabs.TabLayoutMediator +import com.tangem.common.CardIdFormatter +import com.tangem.common.core.CardIdDisplayFormat +import com.tangem.tap.common.extensions.dispatchOpenUrl +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget +import com.tangem.tap.common.postUi +import com.tangem.tap.features.FragmentOnBackPressedHandler +import com.tangem.tap.features.addBackPressHandler +import com.tangem.tap.features.onboarding.products.wallet.redux.* +import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware.Companion.BUY_WALLET_URL +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.fragment_onboarding_wallet.* +import kotlinx.android.synthetic.main.layout_onboarding_buttons_add_cards.* +import kotlinx.android.synthetic.main.layout_onboarding_buttons_common.* +import kotlinx.android.synthetic.main.view_confetti.* +import kotlinx.android.synthetic.main.view_onboarding_progress.* +import org.rekotlin.StoreSubscriber + +class OnboardingWalletFragment : Fragment(R.layout.fragment_onboarding_wallet), + StoreSubscriber, FragmentOnBackPressedHandler { + + private var accessCodeDialog: AccessCodeDialog? = null + private lateinit var cardsWidget: BackupCardsWidget + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + postponeEnterTransition() + setHasOptionsMenu(true) + + val inflater = TransitionInflater.from(requireContext()) + enterTransition = inflater.inflateTransition(R.transition.fade) + exitTransition = inflater.inflateTransition(R.transition.fade) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val leapfrog = LeapfrogWidget(fl_cards_container) + cardsWidget = BackupCardsWidget(leapfrog) { 200f } + startPostponedEnterTransition() + + view_pager_backup_info.adapter = BackupInfoAdapter() + TabLayoutMediator(tab_layout_backup_info, view_pager_backup_info) { tab, position -> + //Some implementation + }.attach() + + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) + + store.dispatch(OnboardingWalletAction.Init) + toolbar.setNavigationOnClickListener { activity?.onBackPressed() } + + addBackPressHandler(this) + } + + override fun onStart() { + super.onStart() + store.subscribe(this) { state -> + state.skipRepeats { oldState, newState -> + oldState.onboardingWalletState == newState.onboardingWalletState + }.select { it.onboardingWalletState } + } + } + + override fun onStop() { + super.onStop() + store.unsubscribe(this) + } + + override fun newState(state: OnboardingWalletState) { + if (activity == null) return + + requireActivity().invalidateOptionsMenu() + + state.artwork?.artwork?.let { imv_front_card.setImageBitmap(it) } + pb_state.max = 6 + pb_state.progress = state.getProgressStep() + + when (state.step) { + OnboardingWalletStep.None -> { + } + OnboardingWalletStep.CreateWallet -> setupCreateWalletState() + OnboardingWalletStep.Backup -> setBackupState(state.backupState) + OnboardingWalletStep.Done -> { + + } + } + } + + private fun setupCreateWalletState() { + btn_main_action.setText(R.string.onboarding_create_wallet_button_create_wallet) + btn_main_action.setOnClickListener { store.dispatch(OnboardingWalletAction.CreateWallet) } + btn_alternative_action.hide() + + toolbar.title = getText(R.string.onboarding_getting_started) + + tv_header.setText(R.string.onboarding_create_wallet_header) + tv_body.setText(R.string.onboarding_create_wallet_body) + + cardsWidget.toFolded() + startPostponedEnterTransition() + } + + + private fun setBackupState(state: BackupState) { + when (state.backupStep) { + BackupStep.InitBackup -> showBackupIntro(state) + BackupStep.ScanOriginCard -> showScanOriginCard(state) + BackupStep.AddBackupCards -> showAddBackupCards(state) + BackupStep.SetAccessCode -> showSetAccessCode() + BackupStep.EnterAccessCode -> showEnterAccessCode(state) + BackupStep.ReenterAccessCode -> showReenterAccessCode(state) + is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state) + is BackupStep.WriteBackupCard -> showWriteBackupCard(state) + BackupStep.Finished -> showSuccess(state) + } + } + + private fun showBackupIntro(state: BackupState) { + imv_first_backup_card.show() + imv_second_backup_card.show() + + cardsWidget.toWelcome() + + tv_header.hide() + tv_body.hide() + view_pager_backup_info.show() + tab_layout_backup_info.show() + + btn_main_action.text = getText(R.string.onboarding_button_backup_now) + btn_alternative_action.text = getText(R.string.onboarding_button_skip_backup) + btn_alternative_action.show(state.canSkipBackup) + btn_main_action.setOnClickListener { store.dispatch(BackupAction.StartBackup) } + btn_alternative_action.setOnClickListener { store.dispatch(BackupAction.DismissBackup) } + startPostponedEnterTransition() + } + + private fun showScanOriginCard(state: BackupState) { + + toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) + + cardsWidget.toFolded() + + tv_header.show() + tv_body.show() + view_pager_backup_info.hide() + tab_layout_backup_info.hide() + + imv_first_backup_card.show() + imv_second_backup_card.show() + + tv_header.text = getText(R.string.onboarding_title_scan_origin_card) + tv_body.text = getString( + R.string.onboarding_subtitle_scan_origin_card, + ) + + btn_main_action.text = getString(R.string.onboarding_button_scan_origin_card) + btn_alternative_action.hide() + btn_main_action.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } + } + + private fun showAddBackupCards(state: BackupState) { + imv_first_backup_card.show() + imv_second_backup_card.show() + + accessCodeDialog?.dismiss() + accessCodeDialog = null + + layout_buttons_add_cards.show() + layout_buttons_common.hide() + if (state.backupCardsNumber < state.maxBackupCards) { + btn_add_card.text = getText(R.string.onboarding_button_add_backup_card) + btn_add_card.setOnClickListener { store.dispatch(BackupAction.AddBackupCard) } + } else { + btn_add_card.text = "You hit the maximum" + btn_add_card.isEnabled = false + } + + when (state.backupCardsNumber) { + 0 -> { + cardsWidget.toFan() + tv_header.text = getText(R.string.onboarding_title_no_backup_cards) + tv_body.text = getText(R.string.onboarding_subtitle_no_backup_cards) + cardsWidget.getFirstBackupCardView().alpha = 0.6f + cardsWidget.getSecondBackupCardView().alpha = 0.2f + } + 1 -> { + tv_header.text = getText(R.string.onboarding_title_one_backup_card) + tv_body.text = getText(R.string.onboarding_subtitle_one_backup_card) + + cardsWidget.getFirstBackupCardView().animate().alpha(1f).setDuration(400) + cardsWidget.getSecondBackupCardView().alpha = 0.2f + } + 2 -> { + tv_header.text = getText(R.string.onboarding_title_two_backup_cards) + tv_body.text = getText(R.string.onboarding_subtitle_two_backup_cards) + cardsWidget.getFirstBackupCardView().alpha = 1f + cardsWidget.getSecondBackupCardView().animate().alpha(1f).setDuration(400) + } + } + + btn_continue.text = getText(R.string.onboarding_button_finalize_backup) + btn_continue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } + btn_continue.isEnabled = state.backupCardsNumber != 0 + } + + private fun showSetAccessCode() { + accessCodeDialog = AccessCodeDialog(requireContext()).apply { + dismissWithAnimation = true + create() + setOnCancelListener { + store.dispatch(BackupAction.OnAccessCodeDialogClosed) + } + show() + showInfoScreen() + val view = + delegate.findViewById(com.google.android.material.R.id.design_bottom_sheet) + (view?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) } + behavior.state = BottomSheetBehavior.STATE_EXPANDED + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + view?.let { it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT } + } + } + + private fun showEnterAccessCode(state: BackupState) { + accessCodeDialog?.showEnterAccessCode() + accessCodeDialog?.showError(state.accessCodeError) + postUi(2000) { store.dispatch(BackupAction.SetAccessCodeError(null)) } + + } + + private fun showReenterAccessCode(state: BackupState) { + accessCodeDialog?.showReenterAccessCode() + accessCodeDialog?.showError(state.accessCodeError) + postUi(2000) { store.dispatch(BackupAction.SetAccessCodeError(null)) } + + } + + private fun showWritePrimaryCard(state: BackupState) { + accessCodeDialog?.dismiss() + + prepareViewForFinalizeStep() + cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) + + cardsWidget.toLeapfrog() { + cardsWidget.getFirstBackupCardView().alpha = 0.6f + cardsWidget.getSecondBackupCardView().alpha = 0.2f + } + + val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) + tv_header.text = getText(R.string.onboarding_title_prepare_origin) + tv_body.text = getString( + R.string.onboarding_subtitle_scan_primary_card_format, + state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) } + ) + btn_main_action.text = getText(R.string.onboarding_button_backup_origin) + btn_main_action.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } + + } + + private fun prepareViewForFinalizeStep() { + layout_buttons_add_cards.hide() + layout_buttons_common.show() + + imv_first_backup_card.show() + imv_second_backup_card.show() + + toolbar.title = getText(R.string.onboarding_button_finalize_backup) + + imv_card_background.hide() + + cardsWidget.toLeapfrog() + +// imv_front_card.alpha = 1f +// imv_first_backup_card.alpha = 1f +// imv_second_backup_card.alpha = 1f + + btn_alternative_action.hide() + } + + private fun showWriteBackupCard(state: BackupState) { + prepareViewForFinalizeStep() + + cardsWidget.getSecondBackupCardView().show(state.backupCardsNumber == 2) + + val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1 + when (cardNumber) { + 1 -> cardsWidget.leapfrogWidget.leap() { + cardsWidget.getOriginCardView().alpha = 0.4f + cardsWidget.getFirstBackupCardView().alpha = 0.2f + cardsWidget.getSecondBackupCardView().alpha = 1f + } + 2 -> cardsWidget.leapfrogWidget.leap() { + cardsWidget.getOriginCardView().alpha = 0.4f + cardsWidget.getFirstBackupCardView().alpha = 0.2f + cardsWidget.getSecondBackupCardView().alpha = 1f + } + } + + val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) + tv_header.text = + getString(R.string.onboarding_title_backup_card_format, cardNumber) + tv_body.text = getString( + R.string.onboarding_subtitle_scan_primary_card_format, + cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]) + ) + btn_main_action.text = getString( + R.string.onboarding_button_backup_card_format, + cardNumber + ) + + btn_main_action.setOnClickListener { store.dispatch(BackupAction.WriteBackupCard(cardNumber)) } + } + + private fun showSuccess(state: BackupState) { + tv_header.text = getText(R.string.onboarding_done_header) + val text = when (state.backupCardsNumber) { + 1 -> getText(R.string.onboarding_subtitle_success_backup_one_card) + 2 -> getText(R.string.onboarding_subtitle_success_backup) + else -> getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) + } + + tv_header.show() + tv_body.show() + view_pager_backup_info.hide() + tab_layout_backup_info.hide() + + tv_body.text = text + btn_main_action.text = getText(R.string.common_continue) + btn_alternative_action.hide() + btn_main_action.setOnClickListener { + lav_confetti.cancelAnimation() + lav_confetti.hide() + store.dispatch(OnboardingWalletAction.FinishOnboarding) + } + + cardsWidget.leapfrogWidget.fold { + fl_cards_container?.hide() + imv_card_background?.hide() + lav_confetti?.show() + lav_confetti?.playAnimation() + imv_success?.alpha = 0f + imv_success?.show() + + imv_success?.animate() + ?.alpha(1f) + ?.setDuration(400) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.shop_menu -> { + store.dispatchOpenUrl(BUY_WALLET_URL) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.shop, menu) + + val backupStep = store.state.onboardingWalletState.backupState.backupStep + val shopMenuShouldBeVisible = + backupStep == BackupStep.ScanOriginCard || backupStep == BackupStep.AddBackupCards + menu.getItem(0).isVisible = shopMenuShouldBeVisible + } + + override fun handleOnBackPressed() { + store.dispatch(OnboardingWalletAction.OnBackPressed) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt new file mode 100644 index 0000000000..9433d4e274 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.tangem.tap.common.extensions.hide +import com.tangem.tap.common.extensions.show +import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction +import com.tangem.tap.store +import com.tangem.wallet.R +import kotlinx.android.synthetic.main.layout_backup_access_code.* +import kotlinx.android.synthetic.main.layout_backup_access_code_info.* +import kotlinx.android.synthetic.main.layout_backup_access_code_submit.* + +class AccessCodeDialog(context: Context) : BottomSheetDialog(context) { + + + init { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + val dialogView = LayoutInflater.from(context).inflate(R.layout.layout_backup_access_code, null) + setContentView(dialogView) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + override fun setContentView(view: View) { + super.setContentView(view) + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + + + fun showInfoScreen() { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title) + layout_backup_access_code_info.show() + layout_backup_access_code_submit.hide() + btn_access_code_create.setOnClickListener { + store.dispatch(BackupAction.ShowEnterAccessCodeScreen) + } + } + + fun showEnterAccessCode() { + layout_backup_access_code_info.hide() + layout_backup_access_code_submit.show() + access_code_title.text = context.getText(R.string.onboarding_access_code_intro_title) + btn_access_code_submit.text = context.getText(R.string.common_continue) + btn_access_code_submit.setOnClickListener { + store.dispatch(BackupAction.CheckAccessCode(et_access_code.text.toString())) + } + } + + fun showReenterAccessCode() { + layout_backup_access_code_info.hide() + layout_backup_access_code_submit.show() + access_code_title.text = context.getText(R.string.onboarding_access_code_repeat_code_title) + et_access_code.setText("") + btn_access_code_submit.text = context.getText(R.string.common_submit) + btn_access_code_submit.setOnClickListener { + store.dispatch(BackupAction.SaveAccessCodeConfirmation(et_access_code.text.toString())) + } + } + + fun showError(error: AccessCodeError?) { + when (error) { + AccessCodeError.CodeTooShort -> + til_access_code.error = context.getString(R.string.onboarding_access_code_too_short) + AccessCodeError.CodesDoNotMatch -> + til_access_code.error = context.getString(R.string.onboarding_access_codes_doesnt_match) + null -> til_access_code.error = null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt new file mode 100644 index 0000000000..883ce390dc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt @@ -0,0 +1,28 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction +import com.tangem.tap.store +import com.tangem.wallet.R + +class AddMoreBackupCardsDialog { + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.alert_title) + setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added) + setPositiveButton(R.string.common_continue) { _, _ -> + store.dispatch(BackupAction.ShowAccessCodeInfoScreen) + } + setNegativeButton(R.string.onboarding_button_add_more_cards) { _, _ -> + + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt new file mode 100644 index 0000000000..9b619eef7d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt @@ -0,0 +1,24 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.store +import com.tangem.wallet.R + +class BackupInProgressDialog { + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.alert_title) + setMessage(R.string.onboarding_backup_exit_warning) + setPositiveButton(R.string.warning_button_ok) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt new file mode 100644 index 0000000000..8b88890be8 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ConfirmDiscardingBackupDialog.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction +import com.tangem.tap.store +import com.tangem.wallet.R + +class ConfirmDiscardingBackupDialog { + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.welcome_interrupted_backup_discard_title) + setMessage(R.string.welcome_interrupted_backup_discard_message) + setPositiveButton(R.string.welcome_interrupted_backup_discard_resume) { _, _ -> + store.dispatch(BackupAction.ResumeBackup) + } + setNegativeButton(R.string.welcome_interrupted_backup_discard_discard) { _, _ -> + store.dispatch(BackupAction.DiscardSavedBackup) + } + setOnDismissListener { + store.dispatch(GlobalAction.HideDialog) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt new file mode 100644 index 0000000000..41ccf7083b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs + +import android.content.Context +import androidx.appcompat.app.AlertDialog +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.store +import com.tangem.wallet.R + +class UnfinishedBackupFoundDialog { + companion object { + fun create(context: Context): AlertDialog { + return AlertDialog.Builder(context).apply { + setTitle(R.string.alert_title) + setMessage(R.string.welcome_interrupted_backup_alert_message) + setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + store.dispatch(BackupAction.ResumeBackup) + } + setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> + store.dispatch(GlobalAction.HideDialog) + store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup)) + } + setCancelable(false) + }.create() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt b/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt new file mode 100644 index 0000000000..db7f77b99c --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/service/OnboardingWalletService.kt @@ -0,0 +1,13 @@ +package com.tangem.tap.features.onboarding.service + +//class OnboardingWalletService( +// override val fromScreen: AppScreen, +// override var scanResponse: ScanResponse, +// cardInfoStorage: UsedCardsPrefStorage, +// walletManagerFactory: WalletManagerFactory, +//) : ProductOnboardingService(cardInfoStorage, walletManagerFactory) { +// +// override fun navigateToScreen(): AppScreen { +// return AppScreen.OnboardingWallet +// } +//} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt index 6701ac636d..ff45a74e19 100644 --- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt @@ -15,6 +15,7 @@ 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.NavigationAction +import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE import com.tangem.tap.domain.TangemSigner import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapWorkarounds.isStart2Coin @@ -152,21 +153,32 @@ private fun sendTransaction( if (card.isStart2Coin) { tangemSdk.config.linkedTerminal = false } - val signer = TangemSigner( - tangemSdk = tangemSdk, initialMessage = action.messageForSigner - ) { signResponse -> + + val signer = TangemSigner(tangemSdk, action.messageForSigner) { signResponse -> store.dispatch( GlobalAction.UpdateWalletSignedHashes( walletSignedHashes = signResponse.totalSignedHashes, - walletPublicKey = walletManager.wallet.publicKey, + walletPublicKey = walletManager.wallet.publicKey.seedKey, remainingSignatures = signResponse.remainingSignatures ) ) } - val result = (walletManager as TransactionSender).send(txData, signer) + + val sendResult = try { + (walletManager as TransactionSender).send(txData, signer) + } catch (ex: Exception) { + FirebaseCrashlytics.getInstance().recordException(ex) + delay(DELAY_SDK_DIALOG_CLOSE) + withMainContext { + dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) + store.dispatchErrorNotification(TapError.CustomError(ex.localizedMessage ?: "Unknown error")) + } + return@launch + } + withContext(Dispatchers.Main) { tangemSdk.config.linkedTerminal = isLinkedTerminal - when (result) { + when (sendResult) { is SimpleResult.Success -> { FirebaseAnalyticsHandler.triggerEvent( event = AnalyticsEvent.TRANSACTION_IS_SENT, @@ -191,20 +203,18 @@ private fun sendTransaction( } } is SimpleResult.Failure -> { - when (result.error) { + when (sendResult.error) { is CreateAccountUnderfunded -> { - val error = result.error as CreateAccountUnderfunded + val error = sendResult.error as CreateAccountUnderfunded val reserve = error.minReserve.value?.stripZeroPlainString() ?: "0" val symbol = error.minReserve.currencySymbol dispatch(SendAction.SendError(TapError.CreateAccountUnderfunded(listOf(reserve, symbol)))) } is SendException -> { - result.error?.let { - FirebaseCrashlytics.getInstance().recordException(it) - } + sendResult.error?.let { FirebaseCrashlytics.getInstance().recordException(it) } } is Throwable -> { - val throwable = result.error as Throwable + val throwable = sendResult.error as Throwable val message = throwable.message val infoHolder = store.state.globalState.feedbackManager?.infoHolder when { @@ -228,13 +238,13 @@ private fun sendTransaction( dispatch(SendAction.SendError(TapError.XmlError.AssetAccountNotCreated)) } else -> { - (result.error as? TangemSdkError)?.let { error -> + (sendResult.error as? TangemSdkError)?.let { error -> FirebaseAnalyticsHandler.logCardSdkError( error, FirebaseAnalyticsHandler.ActionToLog.SendTransaction, mapOf( FirebaseAnalyticsHandler.AnalyticsParam.BLOCKCHAIN - to walletManager.wallet.blockchain.currency), + to walletManager.wallet.blockchain.currency), card = card, ) } @@ -303,7 +313,7 @@ fun createValidateTransactionError(errorList: EnumSet, walletM private fun setIfSendingToPayIdEnabled(appState: AppState?, dispatch: (Action) -> Unit) { val isSendingToPayIdEnabled = - appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false + appState?.globalState?.configManager?.config?.isSendingToPayIdEnabled ?: false dispatch(AddressPayIdActionUi.ChangePayIdState(isSendingToPayIdEnabled)) } diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt index 1f91fb98cb..3938f9720e 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/SendFragment.kt @@ -285,8 +285,12 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) { } } - override fun onDestroy() { + override fun onDestroyView() { keyboardObserver.unregisterListener() + super.onDestroyView() + } + + override fun onDestroy() { store.dispatch(ReleaseSendState) super.onDestroy() } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt index 98efd84540..bcf0bf5644 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/CurrencyListItem.kt @@ -6,8 +6,12 @@ import com.tangem.blockchain.common.Token import com.tangem.wallet.R sealed class CurrencyListItem { + var isAdded: Boolean = false + var isLock: Boolean = false + data class TokenListItem(val token: Token) : CurrencyListItem() data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem() + data class TitleListItem( @StringRes val titleResId: Int, var isContentShown: Boolean = true, diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt index 1be9e7c701..d2b911d8b7 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensAction.kt @@ -1,22 +1,20 @@ package com.tangem.tap.features.tokens.redux import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token import com.tangem.tap.features.wallet.redux.WalletData import org.rekotlin.Action sealed class TokensAction : Action { + object ResetState : TokensAction() + object LoadCurrencies : TokensAction() { data class Success(val currencies: List) : TokensAction() } - object LoadCardTokens : TokensAction() { - data class Success(val tokens: List) : TokensAction() - } - data class SetAddedCurrencies(val wallets: List) : TokensAction() data class ToggleShowTokensForBlockchain(val isShown: Boolean, val blockchain: Blockchain) : TokensAction() + data class SaveChanges(val addedItems: List) : TokensAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt index 6ecff3f595..2176e4ea51 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensMiddleware.kt @@ -1,9 +1,25 @@ package com.tangem.tap.features.tokens.redux +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.CompletionResult +import com.tangem.common.card.EllipticCurve +import com.tangem.tap.common.extensions.dispatchErrorNotification +import com.tangem.tap.common.extensions.toMapKey import com.tangem.tap.common.redux.AppState +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.common.redux.navigation.NavigationAction import com.tangem.tap.currenciesRepository +import com.tangem.tap.domain.DELAY_SDK_DIALOG_CLOSE +import com.tangem.tap.domain.TapError import com.tangem.tap.domain.TapWorkarounds.isTestCard +import com.tangem.tap.domain.tasks.product.ScanResponse +import com.tangem.tap.features.wallet.redux.WalletAction +import com.tangem.tap.scope import com.tangem.tap.store +import com.tangem.tap.tangemSdkManager +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.rekotlin.Middleware class TokensMiddleware { @@ -12,22 +28,111 @@ class TokensMiddleware { { next -> { action -> when (action) { - is TokensAction.LoadCurrencies -> { - val card = state()?.globalState?.scanResponse?.card - val isTestcard = card?.isTestCard ?: false - val tokens = currenciesRepository.getPopularTokens(isTestcard) - val blockchains = currenciesRepository.getBlockchains( - cardFirmware = card?.firmwareVersion, - isTestNet = isTestcard - ) - val currencies = CurrencyListItem.createListOfCurrencies( - blockchains, tokens - ) - store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) - } + is TokensAction.LoadCurrencies -> handleLoadCurrencies(action) + is TokensAction.SaveChanges -> handleSaveChanges(action) } next(action) } } } + + private fun handleLoadCurrencies(action: TokensAction.LoadCurrencies) { + val scanResponse = store.state.globalState.scanResponse ?: return + val isTestcard = scanResponse.card.isTestCard + + val tokens = currenciesRepository.getPopularTokens(isTestcard) + val blockchains = currenciesRepository.getBlockchains( + cardFirmware = scanResponse.card.firmwareVersion, + isTestNet = isTestcard + ) + val currencies = CurrencyListItem.createListOfCurrencies(blockchains, tokens).toMutableList() + if (scanResponse.isTangemWallet()) { + currencies.forEach { + when (it) { + is CurrencyListItem.TitleListItem -> { + } + is CurrencyListItem.BlockchainListItem -> { + val firstCurve = it.blockchain.getSupportedCurves()?.firstOrNull() + if (firstCurve == EllipticCurve.Ed25519) { + it.isLock = true + } + } + is CurrencyListItem.TokenListItem -> { + val firstCurve = it.token.blockchain.getSupportedCurves()?.firstOrNull() + if (firstCurve == EllipticCurve.Ed25519) { + it.isLock = true + } + } + } + } + } + store.dispatch(TokensAction.LoadCurrencies.Success(currencies)) + } + + private fun handleSaveChanges(action: TokensAction.SaveChanges) { + val scanResponse = store.state.globalState.scanResponse ?: return + + val candidatesToAdd = action.addedItems + if (candidatesToAdd.isEmpty()) return + + val blockchains = candidatesToAdd.filterIsInstance() + .map { it.blockchain } + val tokens = candidatesToAdd.filterIsInstance() + .map { it.token } + if (blockchains.isEmpty() && tokens.isEmpty()) return + + if (scanResponse.isTangemWallet()) { + deriveMissingBlockchains(scanResponse, blockchains, tokens) + } else { + submitAdd(blockchains, tokens) + store.dispatch(NavigationAction.PopBackTo()) + } + } + + private fun deriveMissingBlockchains( + scanResponse: ScanResponse, + blockchains: List, + tokens: List + ) { + val wallet = scanResponse.card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return + + val tokenBlockchains = tokens.map { it.blockchain } + val derivationPathsCandidates = (blockchains + tokenBlockchains).distinct() + .mapNotNull { it.derivationPath() } + + val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey() + val alreadyDerivedKeys = scanResponse.derivedKeys[mapKeyOfWalletPublicKey]?.toMutableList() ?: mutableListOf() + val alreadyDerivedPaths = alreadyDerivedKeys.map { it.derivationPath } + + val toDerive = derivationPathsCandidates.filterNot { alreadyDerivedPaths.contains(it) } + + scope.launch { + val result = tangemSdkManager.derivePublicKeys(scanResponse.card.cardId, wallet.publicKey, toDerive) + when (result) { + is CompletionResult.Success -> { + val newDerivedKeys = result.data + alreadyDerivedKeys.addAll(newDerivedKeys) + val updatedScanResponse = scanResponse.copy( + derivedKeys = mapOf(mapKeyOfWalletPublicKey to alreadyDerivedKeys.toList()) + ) + store.dispatch(GlobalAction.SaveScanNoteResponse(updatedScanResponse)) + submitAdd(blockchains, tokens) + + delay(DELAY_SDK_DIALOG_CLOSE) + store.dispatch(NavigationAction.PopBackTo()) + } + is CompletionResult.Failure -> { + store.dispatchErrorNotification(TapError.CustomError("Error adding tokens")) + } + } + } + } + + private fun submitAdd(blockchains: List, tokens: List) { + (blockchains.map { + WalletAction.MultiWallet.AddBlockchain(it) + } + tokens.map { + WalletAction.MultiWallet.AddToken(it) + }).forEach { store.dispatch(it) } + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt index f221424d67..2fb4c5dec8 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/redux/TokensReducer.kt @@ -17,28 +17,23 @@ private fun internalReduce(action: Action, state: AppState): TokensState { val tokensState = state.tokensState return when (action) { + is TokensAction.ResetState -> TokensState() is TokensAction.LoadCurrencies.Success -> { tokensState.copy(currencies = action.currencies, shownCurrencies = action.currencies) } is TokensAction.SetAddedCurrencies -> { tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies()) } - is TokensAction.LoadCardTokens.Success -> { - tokensState.copy(addedTokens = LinkedHashSet( - action.tokens.map { TokenWithAmount(it, null) } - )) - } - is TokensAction.ToggleShowTokensForBlockchain -> { if (action.isShown) { val shownCurrencies = tokensState.shownCurrencies .removeTokensForBlockchain(action.blockchain) - shownCurrencies.toggleHeaderContentShownValue(action.blockchain) + shownCurrencies.toggleHeaderContentShownValue(action.blockchain) tokensState.copy(shownCurrencies = shownCurrencies) } else { val shownCurrencies = tokensState.shownCurrencies .addTokensForBlockchain(action.blockchain, tokensState.currencies) - shownCurrencies.toggleHeaderContentShownValue(action.blockchain) + shownCurrencies.toggleHeaderContentShownValue(action.blockchain) tokensState.copy(shownCurrencies = shownCurrencies) } } diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt index b5d09b7074..38de45981b 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/AddTokensFragment.kt @@ -12,6 +12,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import com.tangem.tap.common.redux.navigation.NavigationAction +import com.tangem.tap.features.tokens.redux.TokensAction import com.tangem.tap.features.tokens.redux.TokensState import com.tangem.tap.features.tokens.ui.adapters.CurrenciesAdapter import com.tangem.tap.mainScope @@ -57,30 +58,35 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + (activity as? AppCompatActivity)?.setSupportActionBar(toolbar) toolbar.setNavigationOnClickListener { activity?.onBackPressed() } setupPopularTokensRecyclerView() + btn_tokens_save_changes.setOnClickListener { + store.dispatch(TokensAction.SaveChanges(viewAdapter.getAddedItems())) + } } - private fun setupPopularTokensRecyclerView() { viewAdapter = CurrenciesAdapter() + viewAdapter.setOnItemAddListener { + btn_tokens_save_changes.isEnabled = viewAdapter.getAddedItems().isNotEmpty() + } + rv_popular_tokens.layoutManager = LinearLayoutManager(context) rv_popular_tokens.adapter = viewAdapter } - override fun newState(state: TokensState) { if (activity == null) return + viewAdapter.addedCurrencies = state.addedCurrencies viewAdapter.submitUnfilteredList(state.shownCurrencies) } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { - R.id.menu_search -> { - true - } + R.id.menu_search -> true else -> super.onOptionsItemSelected(item) } } @@ -101,6 +107,11 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens), .launchIn(mainScope) return super.onCreateOptionsMenu(menu, inflater); } + + override fun onDestroy() { + store.dispatch(TokensAction.ResetState) + super.onDestroy() + } } fun SearchView.inputtedTextAsFlow(): Flow = callbackFlow { diff --git a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt index 85a8462b81..b3d0ecdc04 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/ui/adapters/CurrenciesAdapter.kt @@ -7,6 +7,9 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.Token +import com.tangem.common.extensions.VoidCallback import com.tangem.tap.common.extensions.getString import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.loadCurrenciesIcon @@ -14,7 +17,6 @@ import com.tangem.tap.common.extensions.show import com.tangem.tap.domain.tokens.CardCurrencies import com.tangem.tap.features.tokens.redux.CurrencyListItem import com.tangem.tap.features.tokens.redux.TokensAction -import com.tangem.tap.features.wallet.redux.WalletAction import com.tangem.tap.store import com.tangem.wallet.R import kotlinx.android.synthetic.main.item_currency_subtitle.view.* @@ -27,11 +29,28 @@ class CurrenciesAdapter : ListAdapter private var unfilteredList = listOf() + private val currentlyAddedItems = mutableListOf() + + private var vhOnItemAddListener: ((CurrencyListItem) -> Unit) = { + currentlyAddedItems.add(it) + onItemAddListener?.invoke() + } + private var onItemAddListener: VoidCallback? = null + fun submitUnfilteredList(list: List) { unfilteredList = list submitList(list) } + fun setOnItemAddListener(itemAddListener: VoidCallback) { + onItemAddListener = itemAddListener + if (currentlyAddedItems.isNotEmpty()) itemAddListener() + } + + fun getAddedItems(): List { + return currentlyAddedItems.toList() + } + override fun getItemViewType(position: Int): Int { return when (currentList[position]) { is CurrencyListItem.TitleListItem -> 0 @@ -47,11 +66,13 @@ class CurrenciesAdapter : ListAdapter ) 1 -> CurrenciesViewHolder( LayoutInflater.from(parent.context) - .inflate(R.layout.item_popular_token, parent, false) + .inflate(R.layout.item_popular_token, parent, false), + vhOnItemAddListener ) else -> CurrenciesViewHolder( LayoutInflater.from(parent.context) - .inflate(R.layout.item_popular_token, parent, false) + .inflate(R.layout.item_popular_token, parent, false), + vhOnItemAddListener ) } } @@ -61,7 +82,7 @@ class CurrenciesAdapter : ListAdapter if (holder is TitleViewHolder && listItem is CurrencyListItem.TitleListItem) { holder.bind(listItem) } else if (holder is CurrenciesViewHolder) { - holder.bind(listItem, addedCurrencies) + holder.bind(listItem, addedCurrencies, currentlyAddedItems) } } @@ -86,15 +107,15 @@ class CurrenciesAdapter : ListAdapter is CurrencyListItem.BlockchainListItem -> { element.blockchain.currency.toLowerCase(Locale.US) .contains(queryNormalized) || - element.blockchain.fullName.toLowerCase(Locale.US) - .contains(queryNormalized) + element.blockchain.fullName.toLowerCase(Locale.US) + .contains(queryNormalized) } is CurrencyListItem.TokenListItem -> { element.token.name.toLowerCase(Locale.US) .contains(queryNormalized) || - element.token.symbol.toLowerCase(Locale.US) - .contains(queryNormalized) + element.token.symbol.toLowerCase(Locale.US) + .contains(queryNormalized) } is CurrencyListItem.TitleListItem -> true } @@ -105,56 +126,85 @@ class CurrenciesAdapter : ListAdapter submitList(list) } - class CurrenciesViewHolder(val view: View) : - RecyclerView.ViewHolder(view) { - fun bind(currency: CurrencyListItem, addedCurrencies: CardCurrencies?) { + class CurrenciesViewHolder( + private val view: View, + private val onItemAddListener: ((CurrencyListItem) -> Unit), + ) : RecyclerView.ViewHolder(view) { + + fun bind( + currency: CurrencyListItem, + addedCurrencies: CardCurrencies?, + currentlyAddedItems: MutableList + ) { when (currency) { is CurrencyListItem.BlockchainListItem -> { - val blockchain = currency.blockchain - view.tv_currency_name.text = blockchain.fullName - view.tv_currency_symbol.text = blockchain.currency - val isAdded = addedCurrencies?.blockchains?.contains(blockchain) == true - view.btn_add_token.show(!isAdded) - view.btn_token_added.show(isAdded) - - Picasso.get().loadCurrenciesIcon( - imageView = view.iv_currency, - textView = view.tv_token_letter, - blockchain = blockchain, token = null - ) - - view.btn_add_token.setOnClickListener { - store.dispatch(WalletAction.MultiWallet.AddBlockchain(blockchain)) - view.btn_add_token.hide() - view.btn_token_added.show() - } + val addedBlockchains = addedCurrencies?.blockchains ?: listOf() + val currentlyAdded = currentlyAddedItems.filterIsInstance() + bindBlockchain(currency, addedBlockchains, currentlyAdded) } is CurrencyListItem.TokenListItem -> { - val token = currency.token - view.tv_currency_name.text = token.name - view.tv_currency_symbol.text = token.symbol - - val isAdded = addedCurrencies?.tokens - ?.any { - it.symbol == token.symbol && it.contractAddress == token.contractAddress - } == true - - view.btn_add_token.show(!isAdded) - view.btn_token_added.show(isAdded) - - Picasso.get().loadCurrenciesIcon( - imageView = view.iv_currency, - textView = view.tv_token_letter, - token = token, blockchain = token.blockchain - ) - view.btn_add_token.setOnClickListener { - store.dispatch(WalletAction.MultiWallet.AddToken(token)) - view.btn_add_token.hide() - view.btn_token_added.show() - } + val addedTokens = addedCurrencies?.tokens ?: listOf() + val currentlyAdded = currentlyAddedItems.filterIsInstance() + bindToken(currency, addedTokens, currentlyAdded) } } } + + private fun bindBlockchain( + currency: CurrencyListItem.BlockchainListItem, + addedBlockchains: List, + currentlyAdded: List + ) { + val blockchain = currency.blockchain + Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, null, blockchain) + + view.tv_currency_name.text = blockchain.fullName + view.tv_currency_symbol.text = blockchain.currency + view.btn_add_token.setOnClickListener { + onItemAddListener.invoke(currency) + currency.isAdded = true + modifyAddTokenButton(currency) + } + + val isAddedBefore = addedBlockchains.contains(blockchain) + val isCurrentlyAdded = currentlyAdded.any { it.blockchain == blockchain } + currency.isAdded = isAddedBefore || isCurrentlyAdded + modifyAddTokenButton(currency) + } + + private fun bindToken( + currency: CurrencyListItem.TokenListItem, + addedTokens: List, + currentlyAdded: List + ) { + val token = currency.token + Picasso.get().loadCurrenciesIcon(view.iv_currency, view.tv_token_letter, token, token.blockchain) + + view.tv_currency_name.text = token.name + view.tv_currency_symbol.text = token.symbol + view.btn_add_token.setOnClickListener { + onItemAddListener.invoke(currency) + currency.isAdded = true + modifyAddTokenButton(currency) + } + + val isAddedBefore = addedTokens.any { it == token } + val isCurrentlyAdded = currentlyAdded.any { it.token == token } + currency.isAdded = isAddedBefore || isCurrentlyAdded + + modifyAddTokenButton(currency) + } + + private fun modifyAddTokenButton(currency: CurrencyListItem) { + if (currency.isLock) { + view.btn_add_token.setText(R.string.common_add) + view.btn_add_token.isEnabled = false + } else { + val text = if (currency.isAdded) R.string.add_token_added else R.string.common_add + view.btn_add_token.setText(text) + view.btn_add_token.isEnabled = !currency.isAdded + } + } } class TitleViewHolder(val view: View) : diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt index 149550727b..1081a3303c 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt @@ -50,8 +50,7 @@ sealed class WalletAction : Action { data class AddToken(val token: Token) : MultiWallet() data class SaveCurrencies(val cardCurrencies: CardCurrencies) : MultiWallet() object FindTokensInUse : MultiWallet() - data class FindBlockchainsInUse(val card: Card, val factory: WalletManagerFactory) : - MultiWallet() + object FindBlockchainsInUse : MultiWallet() data class TokenLoaded(val amount: Amount, val token: Token) : MultiWallet() data class SelectWallet(val walletData: WalletData?) : MultiWallet() diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt index 8397f148e0..11d7232cd3 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/MultiWalletMiddleware.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.common.extensions.isZero import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.safeUpdate import com.tangem.tap.common.redux.global.GlobalState import com.tangem.tap.common.redux.navigation.AppScreen import com.tangem.tap.common.redux.navigation.NavigationAction @@ -24,10 +25,12 @@ class MultiWalletMiddleware { fun handle( action: WalletAction.MultiWallet, walletState: WalletState?, globalState: GlobalState?, ) { + val globalState = globalState ?: return + val tapWalletManager = globalState.tapWalletManager + when (action) { is WalletAction.MultiWallet.AddWalletManagers -> { - store.state.globalState.feedbackManager?.infoHolder - ?.setWalletsInfo(action.walletManagers) + globalState.feedbackManager?.infoHolder?.setWalletsInfo(action.walletManagers) } is WalletAction.MultiWallet.SelectWallet -> { if (action.walletData != null) { @@ -35,7 +38,7 @@ class MultiWalletMiddleware { } } is WalletAction.MultiWallet.AddToken -> { - globalState?.scanResponse?.card?.cardId?.let { + globalState.scanResponse?.card?.cardId?.let { currenciesRepository.saveAddedToken(it, action.token) } addToken(action.token, walletState, globalState) @@ -44,35 +47,33 @@ class MultiWalletMiddleware { addTokens(action.tokens, walletState, globalState) } is WalletAction.MultiWallet.AddBlockchain -> { - globalState?.scanResponse?.card?.let { card -> - currenciesRepository.saveAddedBlockchain(card.cardId, action.blockchain) + globalState.scanResponse?.let { + currenciesRepository.saveAddedBlockchain(it.card.cardId, action.blockchain) if (walletState?.blockchains?.contains(action.blockchain) != true) { - globalState.tapWalletManager.walletManagerFactory - .makeWalletManagerForApp(card, action.blockchain)?.let { - store.dispatch(WalletAction.MultiWallet.AddWalletManagers(it)) + tapWalletManager.walletManagerFactory + .makeWalletManagerForApp(it, action.blockchain)?.let { walletManager -> + store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) } } } store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain))) store.dispatch(WalletAction.LoadWallet( - moonpayStatus = globalState?.moonpayStatus, + moonpayStatus = globalState.moonpayStatus, blockchain = action.blockchain - ) + ) ) } is WalletAction.MultiWallet.SaveCurrencies -> { - val cardId = globalState?.scanResponse?.card?.cardId - cardId?.let { currenciesRepository.saveCardCurrencies(it, action.cardCurrencies) } + globalState.scanResponse?.card?.cardId?.let { + currenciesRepository.saveCardCurrencies(it, action.cardCurrencies) + } } is WalletAction.MultiWallet.RemoveWallet -> { - val cardId = globalState?.scanResponse?.card?.cardId + val cardId = globalState.scanResponse?.card?.cardId when (val currency = action.walletData.currency) { is Currency.Blockchain -> { cardId?.let { - currenciesRepository.removeBlockchain( - it, - currency.blockchain - ) + currenciesRepository.removeBlockchain(it, currency.blockchain) } } is Currency.Token -> { @@ -83,11 +84,14 @@ class MultiWalletMiddleware { } } is WalletAction.MultiWallet.FindBlockchainsInUse -> { - val cardFirmware = globalState?.scanResponse?.card?.firmwareVersion + val scanResponse = globalState.scanResponse ?: return + if (scanResponse.isTangemWallet()) return + + val cardFirmware = scanResponse.card.firmwareVersion val blockchains = currenciesRepository.getBlockchains(cardFirmware) .filterNot { walletState?.blockchains?.contains(it) == true } val walletManagers = - action.factory.makeWalletManagersForApp(action.card, blockchains) + tapWalletManager.walletManagerFactory.makeWalletManagersForApp(scanResponse, blockchains) scope.launch { walletManagers.map { walletManager -> @@ -120,12 +124,14 @@ class MultiWalletMiddleware { } } is WalletAction.MultiWallet.FindTokensInUse -> { - val card = globalState?.scanResponse?.card ?: return + val scanResponse = globalState.scanResponse ?: return + if (scanResponse.isTangemWallet()) return + + val walletFactory = tapWalletManager.walletManagerFactory + val card = scanResponse.card val walletManager = walletState?.getWalletManager(Blockchain.Ethereum) - ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( - card = card, - blockchain = Blockchain.Ethereum - ) + ?: walletFactory.makeWalletManagerForApp(scanResponse, Blockchain.Ethereum) + val tokenFinder = walletManager as TokenFinder scope.launch { val result = tokenFinder.findTokens() @@ -159,26 +165,33 @@ class MultiWalletMiddleware { } private fun addToken(token: Token, walletState: WalletState?, globalState: GlobalState?) { - val card = globalState?.scanResponse?.card ?: return + val scanResponse = globalState?.scanResponse ?: return + val walletManager = walletState?.getWalletManager(token) ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( - card = card, - blockchain = token.blockchain + scanResponse, + token.blockchain )?.also { walletManager -> store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) } - store.dispatch( - WalletAction.LoadFiatRate(currency = Currency.Token(token)) - ) + store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Token(token))) scope.launch { when (val result = walletManager?.addToken(token)) { is Result.Success -> { - store.dispatchOnMain( - WalletAction.MultiWallet.TokenLoaded(amount = result.data, token = token) - ) + store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, token)) + } + is Result.Failure -> { + when (val result = walletManager.safeUpdate()) { + is com.tangem.common.services.Result.Success -> { + val tokenAmount = result.data.getTokenAmount(token) ?: return@launch + store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, token)) + } + is com.tangem.common.services.Result.Failure -> { + } + } } } } @@ -189,12 +202,13 @@ class MultiWalletMiddleware { walletState: WalletState?, globalState: GlobalState?, ) { - val card = globalState?.scanResponse?.card ?: return + val scanResponse = globalState?.scanResponse ?: return + val tokensWithManagers = tokens.map { token -> val walletManager = walletState?.getWalletManager(token) ?: globalState.tapWalletManager.walletManagerFactory.makeWalletManagerForApp( - card = card, - blockchain = token.blockchain + scanResponse, + token.blockchain )?.also { walletManager -> store.dispatch(WalletAction.MultiWallet.AddWalletManagers(walletManager)) store.dispatch(WalletAction.MultiWallet.AddBlockchain(walletManager.wallet.blockchain)) @@ -208,11 +222,17 @@ class MultiWalletMiddleware { tokensWithManagers.forEach { when (val result = it.walletManager?.addToken(it.token)) { is Result.Success -> { - store.dispatchOnMain( - WalletAction.MultiWallet.TokenLoaded( - amount = result.data, token = it.token - ) - ) + store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(result.data, it.token)) + } + is Result.Failure -> { + when (val result = it.walletManager.safeUpdate()) { + is com.tangem.common.services.Result.Success -> { + val tokenAmount = result.data.getTokenAmount(it.token) ?: return@launch + store.dispatchOnMain(WalletAction.MultiWallet.TokenLoaded(tokenAmount, it.token)) + } + is com.tangem.common.services.Result.Failure -> { + } + } } } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt index aa0aaee36c..8eeef0afdc 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/MultiWalletView.kt @@ -2,6 +2,7 @@ package com.tangem.tap.features.wallet.ui.wallet import android.app.Dialog import androidx.recyclerview.widget.LinearLayoutManager +import com.tangem.common.card.Card import com.tangem.tap.common.extensions.hide import com.tangem.tap.common.extensions.show import com.tangem.tap.common.redux.StateDialog @@ -49,8 +50,22 @@ class MultiWalletView : WalletView { btn_scan_multiwallet?.show() rv_multiwallet.show() btn_add_token.show() + setupWalletCardNumber(fragment) } + private fun setupWalletCardNumber(fragment: WalletFragment) = with(fragment) { + val card = store.state.globalState.scanResponse?.card + if (card?.backupStatus is Card.BackupStatus.Active) { + val cardCount = (card.backupStatus as Card.BackupStatus.Active).cardCount + 1 + tv_twin_card_number.show() + tv_twin_card_number.text = + getString(R.string.wallet_twins_chip_format, 1, cardCount) + } else { + tv_twin_card_number.hide() + } + } + + private fun setupButtons(fragment: WalletFragment) = with(fragment) { btn_scan_multiwallet?.setOnClickListener { store.dispatch(WalletAction.Scan) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt index cb58832ebb..d8ab4644df 100644 --- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt +++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt @@ -96,11 +96,11 @@ class SingleWalletView : WalletView { twinCardsState?.cardNumber?.let { cardNumber -> tv_twin_card_number.show() val number = when (cardNumber) { - TwinCardNumber.First -> "1" - TwinCardNumber.Second -> "2" + TwinCardNumber.First -> 1 + TwinCardNumber.Second -> 2 } tv_twin_card_number.text = - this.getString(R.string.wallet_twins_chip_format, number) + this.getString(R.string.wallet_twins_chip_format, number, 2) } if (twinCardsState?.cardNumber == null) { tv_twin_card_number.hide() diff --git a/app/src/main/java/com/tangem/tap/network/Retrofit.kt b/app/src/main/java/com/tangem/tap/network/Retrofit.kt index dd16923173..a2188e6d64 100644 --- a/app/src/main/java/com/tangem/tap/network/Retrofit.kt +++ b/app/src/main/java/com/tangem/tap/network/Retrofit.kt @@ -4,6 +4,7 @@ import com.squareup.moshi.FromJson import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import com.tangem.common.json.TangemSdkAdapter import com.tangem.wallet.BuildConfig import okhttp3.Interceptor import okhttp3.OkHttpClient @@ -41,7 +42,10 @@ fun createMoshiConverterFactory(): Converter.Factory = MoshiConverterFactory.cre fun createMoshi(): Moshi = Moshi.Builder() .add(BigDecimalAdapter) - .add(KotlinJsonAdapterFactory()).build() + .add(KotlinJsonAdapterFactory()) + .add(TangemSdkAdapter.DerivationPathAdapter()) + .add(TangemSdkAdapter.DerivationNodeAdapter()) + .build() private fun createHttpLoggingInterceptor(): HttpLoggingInterceptor { val logging = HttpLoggingInterceptor() diff --git a/app/src/main/res/color/selector_btn_gray_text_color.xml b/app/src/main/res/color/selector_btn_gray_text_color.xml new file mode 100644 index 0000000000..8e5c0ab355 --- /dev/null +++ b/app/src/main/res/color/selector_btn_gray_text_color.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/selector_edit_text.xml b/app/src/main/res/color/selector_edit_text.xml new file mode 100644 index 0000000000..b6c0697011 --- /dev/null +++ b/app/src/main/res/color/selector_edit_text.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_new.png b/app/src/main/res/drawable-hdpi/card_placeholder_new.png new file mode 100644 index 0000000000..4c9cff9062 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable-ldpi/card_placeholder_new.png b/app/src/main/res/drawable-ldpi/card_placeholder_new.png new file mode 100644 index 0000000000..e9986ae27f Binary files /dev/null and b/app/src/main/res/drawable-ldpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable-mdpi/card_placeholder_new.png b/app/src/main/res/drawable-mdpi/card_placeholder_new.png new file mode 100644 index 0000000000..b15e1cf3bb Binary files /dev/null and b/app/src/main/res/drawable-mdpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable-xhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xhdpi/card_placeholder_new.png new file mode 100644 index 0000000000..a9f3c750e0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png new file mode 100644 index 0000000000..0caf7a0d35 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png b/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png new file mode 100644 index 0000000000..56a6e580f0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/card_placeholder_new.png differ diff --git a/app/src/main/res/drawable/ic_access_code.xml b/app/src/main/res/drawable/ic_access_code.xml new file mode 100644 index 0000000000..d24768aa15 --- /dev/null +++ b/app/src/main/res/drawable/ic_access_code.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml new file mode 100644 index 0000000000..321894f58a --- /dev/null +++ b/app/src/main/res/drawable/ic_dot.xml @@ -0,0 +1,15 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dot_not_selected.xml b/app/src/main/res/drawable/ic_dot_not_selected.xml new file mode 100644 index 0000000000..a6d0f14af2 --- /dev/null +++ b/app/src/main/res/drawable/ic_dot_not_selected.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_feature_1.xml b/app/src/main/res/drawable/ic_feature_1.xml new file mode 100644 index 0000000000..ed4eba060f --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_1.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_feature_2.xml b/app/src/main/res/drawable/ic_feature_2.xml new file mode 100644 index 0000000000..6c1b54564b --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_2.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_feature_3.xml b/app/src/main/res/drawable/ic_feature_3.xml new file mode 100644 index 0000000000..cf8a08f7d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_feature_3.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_hide_password.xml b/app/src/main/res/drawable/ic_hide_password.xml new file mode 100644 index 0000000000..e0ace3f008 --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_password.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_selected_dot.xml b/app/src/main/res/drawable/ic_selected_dot.xml new file mode 100644 index 0000000000..e847725fc2 --- /dev/null +++ b/app/src/main/res/drawable/ic_selected_dot.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_show_password.xml b/app/src/main/res/drawable/ic_show_password.xml new file mode 100644 index 0000000000..4bbf87adb5 --- /dev/null +++ b/app/src/main/res/drawable/ic_show_password.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/selector_password_toggle.xml b/app/src/main/res/drawable/selector_password_toggle.xml new file mode 100644 index 0000000000..bdff307019 --- /dev/null +++ b/app/src/main/res/drawable/selector_password_toggle.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tabs_dots.xml b/app/src/main/res/drawable/selector_tabs_dots.xml new file mode 100644 index 0000000000..8b1678434a --- /dev/null +++ b/app/src/main/res/drawable/selector_tabs_dots.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chip.xml b/app/src/main/res/drawable/shape_chip.xml index 9544d9916d..658e94c9a7 100644 --- a/app/src/main/res/drawable/shape_chip.xml +++ b/app/src/main/res/drawable/shape_chip.xml @@ -1,7 +1,7 @@ - + + android:fitsSystemWindows="true"> + + + diff --git a/app/src/main/res/layout/fragment_details.xml b/app/src/main/res/layout/fragment_details.xml index 90df722e29..b6475a8ac3 100644 --- a/app/src/main/res/layout/fragment_details.xml +++ b/app/src/main/res/layout/fragment_details.xml @@ -160,15 +160,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes" /> - + + + app:layout_constraintTop_toBottomOf="@id/tv_reset_to_factory" /> - + android:layout_height="match_parent" + android:background="@color/backgroundLightGray" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="vertical"> - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_backup_info_adapter.xml b/app/src/main/res/layout/item_backup_info_adapter.xml new file mode 100644 index 0000000000..ecbfdbdb1b --- /dev/null +++ b/app/src/main/res/layout/item_backup_info_adapter.xml @@ -0,0 +1,36 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_popular_token.xml b/app/src/main/res/layout/item_popular_token.xml index 976047b14b..dc388c1dfa 100644 --- a/app/src/main/res/layout/item_popular_token.xml +++ b/app/src/main/res/layout/item_popular_token.xml @@ -74,33 +74,17 @@ app:layout_constraintVertical_bias="0" tools:text="NODLE" /> - - - @@ -110,7 +94,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:barrierDirection="start" - app:constraint_referenced_ids="btn_add_token, btn_token_added" /> - + app:constraint_referenced_ids="btn_add_token" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_backup_access_code.xml b/app/src/main/res/layout/layout_backup_access_code.xml new file mode 100644 index 0000000000..de45562ba3 --- /dev/null +++ b/app/src/main/res/layout/layout_backup_access_code.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_backup_access_code_info.xml b/app/src/main/res/layout/layout_backup_access_code_info.xml new file mode 100644 index 0000000000..fab2ddf46d --- /dev/null +++ b/app/src/main/res/layout/layout_backup_access_code_info.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_backup_access_code_submit.xml b/app/src/main/res/layout/layout_backup_access_code_submit.xml new file mode 100644 index 0000000000..eca2c05bbd --- /dev/null +++ b/app/src/main/res/layout/layout_backup_access_code_submit.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml new file mode 100644 index 0000000000..1de5055e97 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_buttons_common.xml b/app/src/main/res/layout/layout_onboarding_buttons_common.xml new file mode 100644 index 0000000000..ff0472d121 --- /dev/null +++ b/app/src/main/res/layout/layout_onboarding_buttons_common.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/test_leapfrog_widget.xml b/app/src/main/res/layout/test_leapfrog_widget.xml index c4fddb3183..63834a3de2 100644 --- a/app/src/main/res/layout/test_leapfrog_widget.xml +++ b/app/src/main/res/layout/test_leapfrog_widget.xml @@ -19,4 +19,10 @@ android:layout_height="wrap_content" android:src="@drawable/card_placeholder_black" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_onboarding_progress.xml b/app/src/main/res/layout/view_onboarding_progress.xml index 6dbac9141f..1c7c3b53fc 100644 --- a/app/src/main/res/layout/view_onboarding_progress.xml +++ b/app/src/main/res/layout/view_onboarding_progress.xml @@ -1,6 +1,5 @@ - + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5d0fe290b1..93cbbcca62 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -10,7 +10,7 @@ @color/accent #661ACE80 @color/darkGray6 - #661C1C1E + #6A6A6A #5AC461 #C4291C @@ -55,4 +55,7 @@ #FFB71B #CA0F03 + #EFEFF0 + + #18C077 \ No newline at end of file diff --git a/app/src/main/res/values/strings_untranslated.xml b/app/src/main/res/values/strings_untranslated.xml index c28a51e88b..e81e6a07bf 100644 --- a/app/src/main/res/values/strings_untranslated.xml +++ b/app/src/main/res/values/strings_untranslated.xml @@ -16,7 +16,7 @@ One wallet. Two cards. This one that you are holding in your hands and the other one with number #%s. - Card %s of 2 + Card %1d of %2d Re-create twin wallet Card terms of use @@ -208,4 +208,89 @@ Ok, Got it! + Success! + Getting started + + Backup now + Get a backup card + Skip for later + Finalize the backup process + + Add backup card + Scan the card #%d + Scan primary card + Add more cards + + Backup your wallet + No backup cards + Prepare your card + One backup card added + Two backup cards added + Backup card #%d + + You can backup your keys up to two other blank wallet cards + To start the backup process add up to two cards + Prepare the primary card with number %s + Prepare the primary card + You can add one more card or finalize the backup process + Max number of cards added. Finalize the backup process + If the process of backup gets interrupted in any way you’ll have to start over + + You’ve added one backup card. When backup process is finished you can’t add more backup cards. If you have one more card, add it in backup, otherwise you can buy it in shop. Do you like to continue backup process? + + Create Access Code + Re-enter your Access Code + Protect + Personalize + Restore + You have to setup a single access code to protect all yor wallets. + You can set an individual access code on each card later. + The access code can be restored with a linked card, don’t keep all cards at one place. + Choose any word, phrase, or number you want as your access code. + Access code must be at least 4 characters long + Entered access code didn\'t match initial access code + + Create + Submit + You have created 2 backup cards, and now these cards are ready for use. + Creating a backup + The backup process is partly complete. You can’t exit it now. + + Backup wallet + Identical cards + Access code + Access code restore + + You can backup your keys up to two other blank Tangem Wallet cards. + All the backup cards can be used as full-functoinal with the identical keys. + You will be able to set an access code to protect your wallets. + Access code can be restored with one of backup cards. + + Your Tangem Wallet card is configured and ready for use. + You have created 1 backup card, and now this card are ready for use. + Primary Card + I understand + + Backup status + Active. Backup cards count: %d + Linked. Backup cards count: %d + No backup + Create backup + You have an interrupted backup. Do you want to resume? + Yes, resume + Discard + This is an irreversible action + If you will discard the backup now, then you will have to reset the cards to factory settings to start over again + Resume backup + Discard + Primary card + Scan the primary card + Continue to my wallet + Card %1s of %2s + Sending %s + Prepare the backup card with number %s + Prepare the primary card with number %s + Reset to factory settings + This action is irreversible. If, after resetting the card, someone sends funds to it, then you will not be able to withdraw them. + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 286cddae59..ba05fd093a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -9,6 +9,8 @@ true true @null + @color/menu_accent_color + + + + + + + + + + + + + + + +