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 829fdd5601..a13d000c44 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -130,6 +130,11 @@ class DialogManager : StoreSubscriber { preparedData = state.dialog.data, context = context, ) + is WalletConnectDialog.PairConnectErrorDialog -> SimpleAlertDialog.create( + titleRes = R.string.wallet_connect_title, + message = state.dialog.error.message, + context = context, + ) is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context) is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 0b0af1a01b..80d9f694c8 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -6,6 +6,7 @@ import com.tangem.domain.redux.LegacyAction import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.stripZeroPlainString +import com.tangem.tap.common.feedback.FeedbackEmail import com.tangem.tap.common.feedback.RateCanBeBetterEmail import com.tangem.tap.common.feedback.SendTransactionFailedEmail import com.tangem.tap.common.redux.AppState @@ -27,6 +28,12 @@ internal object LegacyMiddleware { scanResponse = action.scanResponse, ) } + is LegacyAction.SendEmailSupport -> { + store.state.globalState.feedbackManager?.sendEmail( + feedbackData = FeedbackEmail(), + scanResponse = action.scanResponse, + ) + } is LegacyAction.StartOnboardingProcess -> { store.dispatch( GlobalAction.Onboarding.Start(action.scanResponse, canSkipBackup = action.canSkipBackup), diff --git a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt index 966f9613e9..1e322aeaba 100644 --- a/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt +++ b/app/src/main/java/com/tangem/tap/domain/card/DefaultResetCardUseCase.kt @@ -11,8 +11,8 @@ import com.tangem.common.core.TangemError import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.card.models.ResetCardError -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWalletId import com.tangem.tap.domain.sdk.TangemSdkManager @@ -20,24 +20,25 @@ internal class DefaultResetCardUseCase( private val tangemSdkManager: TangemSdkManager, ) : ResetCardUseCase { - override suspend fun invoke(card: CardDTO): Either = resourceScope { - either { - withUserCodeRequestPolicy(card) + override suspend fun invoke(cardId: String, params: ResetCardUserCodeParams): Either = + resourceScope { + either { + withUserCodeRequestPolicy(params) - tangemSdkManager.resetToFactorySettings( - cardId = card.cardId, - allowsRequestAccessCodeFromRepository = true, - ).bind(raise = this) + tangemSdkManager.resetToFactorySettings( + cardId = cardId, + allowsRequestAccessCodeFromRepository = true, + ).bind(raise = this) + } } - } override suspend fun invoke( cardNumber: Int, - card: CardDTO, + params: ResetCardUserCodeParams, userWalletId: UserWalletId, - ): Either = resourceScope { + ): Either = resourceScope { either { - withUserCodeRequestPolicy(card) + withUserCodeRequestPolicy(params) tangemSdkManager.resetBackupCard( cardNumber = cardNumber, @@ -46,11 +47,11 @@ internal class DefaultResetCardUseCase( } } - private suspend fun ResourceScope.withUserCodeRequestPolicy(card: CardDTO) { + private suspend fun ResourceScope.withUserCodeRequestPolicy(params: ResetCardUserCodeParams) { install( acquire = { val policyBeforeReset = tangemSdkManager.userCodeRequestPolicy - requestMandatoryAccessCodeEntry(card) + requestMandatoryAccessCodeEntry(params) policyBeforeReset }, @@ -60,10 +61,10 @@ internal class DefaultResetCardUseCase( ) } - private fun requestMandatoryAccessCodeEntry(card: CardDTO) { - val type = if (card.isAccessCodeSet) { + private fun requestMandatoryAccessCodeEntry(params: ResetCardUserCodeParams) { + val type = if (params.isAccessCodeSet) { UserCodeType.AccessCode - } else if (card.isPasscodeSet == true) { + } else if (params.isPasscodeSet == true) { UserCodeType.Passcode } else { null @@ -74,15 +75,14 @@ internal class DefaultResetCardUseCase( } } - private fun CompletionResult<*>.bind(raise: Raise) { + private fun CompletionResult.bind(raise: Raise): Boolean { return when (this) { is CompletionResult.Failure -> { val domainError = error.mapToDomainError() raise.raise(domainError) } - is CompletionResult.Success -> { /* no-op */ - } + is CompletionResult.Success -> data } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt index 0f5e408f5e..2dbfaec5b7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/TangemSdkManager.kt @@ -70,9 +70,9 @@ interface TangemSdkManager { suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult + ): CompletionResult - suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult + suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 00bacdbe26..835a2fee69 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -202,7 +202,7 @@ class DefaultTangemSdkManager( override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult { + ): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ResetToFactorySettingsTask( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, @@ -210,10 +210,9 @@ class DefaultTangemSdkManager( cardId = cardId, initialMessage = Message(resources.getString(R.string.card_settings_reset_card_to_factory)), ) - .map { CardDTO(it) } } - override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { return runTaskAsyncReturnOnMain( runnable = ResetBackupCardTask(userWalletId), initialMessage = Message( diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index dcc850f798..0dffc198a7 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -87,12 +87,12 @@ class MockTangemSdkManager( override suspend fun resetToFactorySettings( cardId: String, allowsRequestAccessCodeFromRepository: Boolean, - ): CompletionResult { - return MockProvider.getCardDto() + ): CompletionResult { + return CompletionResult.Success(true) } - override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { - return CompletionResult.Success(Unit) + override suspend fun resetBackupCard(cardNumber: Int, userWalletId: UserWalletId): CompletionResult { + return CompletionResult.Success(true) } override suspend fun saveAccessCode(accessCode: String, cardsIds: Set): CompletionResult { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index 7de7ceb374..288ca22a9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -19,11 +19,11 @@ import com.tangem.tap.domain.tasks.UserWalletIdPreflightReadFilter */ internal class ResetBackupCardTask( private val userWalletId: UserWalletId, -) : CardSessionRunnable { +) : CardSessionRunnable { override val allowsRequestAccessCodeFromRepository: Boolean = false - override fun run(session: CardSession, callback: CompletionCallback) { + override fun run(session: CardSession, callback: CompletionCallback) { PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), @@ -35,10 +35,10 @@ internal class ResetBackupCardTask( } } - private fun resetCard(session: CardSession, callback: CompletionCallback) { + private fun resetCard(session: CardSession, callback: CompletionCallback) { ResetToFactorySettingsTask(allowsRequestAccessCodeFromRepository).run(session) { result -> when (result) { - is CompletionResult.Success -> callback(CompletionResult.Success(Unit)) + is CompletionResult.Success -> callback(CompletionResult.Success(result.data)) is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } } 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 index f06f2f801a..51ff0b2d80 100644 --- 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 @@ -10,13 +10,15 @@ import com.tangem.operations.wallet.PurgeWalletCommand class ResetToFactorySettingsTask( override val allowsRequestAccessCodeFromRepository: Boolean, -) : CardSessionRunnable { +) : CardSessionRunnable { - override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private var isResetCompleted = false + + override fun run(session: CardSession, callback: (result: CompletionResult) -> Unit) { deleteWallets(session, callback) } - private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private fun deleteWallets(session: CardSession, callback: (result: CompletionResult) -> Unit) { val wallet = session.environment.card?.wallets?.lastOrNull().guard { resetBackup(session, callback) return @@ -25,6 +27,7 @@ class ResetToFactorySettingsTask( PurgeWalletCommand(wallet.publicKey).run(session) { result -> when (result) { is CompletionResult.Success -> { + isResetCompleted = true deleteWallets(session, callback) } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) @@ -32,18 +35,18 @@ class ResetToFactorySettingsTask( } } - private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { + private fun resetBackup(session: CardSession, callback: (result: CompletionResult) -> Unit) { if (session.environment.card?.backupStatus == null || session.environment.card?.backupStatus == Card.BackupStatus.NoBackup ) { - callback(CompletionResult.Success(session.environment.card!!)) + callback(CompletionResult.Success(isResetCompleted)) return } ResetBackupCommand().run(session) { result -> when (result) { is CompletionResult.Success -> { - callback(CompletionResult.Success(session.environment.card!!)) + callback(CompletionResult.Success(true)) } is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error)) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt index 4565c111ed..2901b7120b 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/GeneralUserWalletsListManager.kt @@ -48,21 +48,33 @@ internal class GeneralUserWalletsListManager( get() = requireImplementation.isLockable override val userWallets: Flow> - get() = implementation.transformLatest { impl -> - if (impl != null && impl.hasUserWallets) { - emitAll(impl.userWallets) + get() = implementation + .transformLatest { impl -> + if (impl != null) { + emitAll(impl.userWallets) + } } - } + // To avoid returning empty flow to subscriber while implementation and userWallets are null + // Flow is called first time when implementation is null and then when its assigned with implementation + // that may have not user wallets (null or empty). + // As a result subscription occurs on empty flow, than will not change if user wallets are available + .filter { requireImplementation.hasUserWallets } override val userWalletsSync: List get() = requireImplementation.userWalletsSync override val selectedUserWallet: Flow - get() = implementation.transformLatest { impl -> - if (impl != null && impl.hasUserWallets) { - emitAll(impl.selectedUserWallet) + get() = implementation + .transformLatest { impl -> + if (impl != null) { + emitAll(impl.selectedUserWallet) + } } - } + // To avoid returning empty flow to subscriber while implementation and userWallets are null + // Flow is called first time when implementation is null and then when its assigned with implementation + // that may have not user wallets (null or empty). + // As a result subscription occurs on empty flow, than will not change if user wallets are available + .filter { requireImplementation.hasUserWallets } override val selectedUserWalletSync: UserWallet? get() = requireImplementation.selectedUserWalletSync diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt index 909f4dafb7..073fcb8f0d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/app/WalletConnectEventsHandlerImpl.kt @@ -46,4 +46,8 @@ internal class WalletConnectEventsHandlerImpl : WalletConnectEventsHandler { override fun onUnsupportedRequest() { store.dispatchOnMain(WalletConnectAction.RejectUnsupportedRequest) } + + override fun onPairConnectError(error: Throwable) { + store.dispatchOnMain(WalletConnectAction.PairConnectErrorAction(error)) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 04a4940928..4266b2fa73 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -246,7 +246,20 @@ internal class DefaultLegacyWalletConnectRepository( } override fun pair(uri: String) { - Web3Wallet.pair(Wallet.Params.Pair(uri)) + Web3Wallet.pair( + params = Wallet.Params.Pair(uri), + onSuccess = { + Timber.i("Paired successfully: $it") + }, + onError = { + Timber.e("Error while pairing: $it") + scope.launch { + _events.emit( + WalletConnectEvents.PairConnectError(it.throwable), + ) + } + }, + ) } override fun approve(userNamespaces: Map>) { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index d4fef91bab..cc4a3e16c4 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -14,26 +14,24 @@ import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WalletConnectSessionsRepository import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @Module -@InstallIn(ActivityComponent::class) +@InstallIn(SingletonComponent::class) internal object WalletConnectInteractorModule { @Provides - @ActivityScoped + @Singleton fun provideWalletConnectInteractor( wcRepository: LegacyWalletConnectRepository, wcSessionsRepository: WalletConnectSessionsRepository, @@ -41,6 +39,7 @@ internal object WalletConnectInteractorModule { currenciesRepository: CurrenciesRepository, walletManagersFacade: WalletManagersFacade, userWalletsListManager: UserWalletsListManager, + coroutineDispatcherProvider: CoroutineDispatcherProvider, ): WalletConnectInteractor { return WalletConnectInteractor( handler = WalletConnectEventsHandlerImpl(), @@ -51,7 +50,7 @@ internal object WalletConnectInteractorModule { currenciesRepository = currenciesRepository, walletManagersFacade = walletManagersFacade, userWalletsListManager = userWalletsListManager, - dispatchers = AppCoroutineDispatcherProvider(), + dispatchers = coroutineDispatcherProvider, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt index 30c73a0642..b9414d247e 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectEventsHandler.kt @@ -16,4 +16,6 @@ interface WalletConnectEventsHandler { fun onSessionRequest(request: WcPreparedRequest) fun onUnsupportedRequest() + + fun onPairConnectError(error: Throwable) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt index b63a1d0005..66f8b5d16c 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/WalletConnectInteractor.kt @@ -9,18 +9,18 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase +import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.filterNotNull import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.domain.models.* +import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction import com.tangem.tap.features.details.ui.walletconnect.WcSessionForScreen +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancelChildren +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber +import java.util.Stack @Suppress("LargeClass", "LongParameterList") class WalletConnectInteractor( @@ -35,18 +35,27 @@ class WalletConnectInteractor( val blockchainHelper: WcBlockchainHelper, ) { + var isWalletConnectReadyForDeepLinks = false + private val getSelectedWalletUseCase by lazy(LazyThreadSafetyMode.NONE) { GetSelectedWalletUseCase(userWalletsListManager) } private val wcScope = CoroutineScope( - Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("wcScope"), + SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> + Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") + }, ) private val listenerScope = CoroutineScope( - Job() + dispatchers.io + FeatureCoroutineExceptionHandler.create("listenScope"), + SupervisorJob() + dispatchers.io + CoroutineExceptionHandler { _, throwable -> + Timber.e("CoroutineException: from: LISTENER SCOPE, exception: $throwable") + }, ) + /** Stack of deeplinks to handle if user wallet is not selected or cryptocurrency statuses are not available */ + private val deeplinkStack: Stack = Stack() + private val events = walletConnectRepository.events private val sessions = walletConnectRepository.activeSessions @@ -96,6 +105,7 @@ class WalletConnectInteractor( networks = currencies.map { it.network }, ) setUserChains(accounts) + handleDeeplinkStack(accounts) } private suspend fun startListeningWc(userWalletId: String, cardId: String?) { @@ -118,6 +128,17 @@ class WalletConnectInteractor( walletConnectRepository.setUserNamespaces(userNamespaces) } + private fun handleDeeplinkStack(accounts: List) { + runCatching { + if (accounts.isEmpty()) return + isWalletConnectReadyForDeepLinks = true + val lastDeeplink = deeplinkStack.pop() + store.dispatchOnMain(WalletConnectAction.OpenSession(lastDeeplink)) + }.onFailure { + Timber.e("WC deeplink handling failed. $it") + } + } + private suspend fun subscribeToEvents() { events .onEach { wcEvent -> @@ -167,6 +188,9 @@ class WalletConnectInteractor( is WalletConnectEvents.SessionRequest -> { handleRequest(wcEvent) } + is WalletConnectEvents.PairConnectError -> { + handler.onPairConnectError(wcEvent.error) + } } } .flowOn(dispatchers.io) @@ -329,6 +353,21 @@ class WalletConnectInteractor( return uri.lowercase().startsWith(WC_SCHEME) } + /** + * Handles Wallet Connect deep links. + * If wallet connect is able to handle the deeplink, session is started with deeplink. + * Otherwise, deeplink is stored until wallet connect is ready to handle it. + * + * @param deeplink deeplink to handle + */ + fun addDeeplink(deeplink: String) { + if (isWalletConnectReadyForDeepLinks) { + store.dispatchOnMain(WalletConnectAction.OpenSession(deeplink)) + } else { + deeplinkStack.push(deeplink) + } + } + private fun getCardId(userWallet: UserWallet): String? { return if (userWallet.scanResponse.card.backupStatus?.isActive != true) { userWallet.cardId diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt index d47c296a42..85e6e06f5b 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/models/WalletConnectEvents.kt @@ -26,4 +26,6 @@ sealed interface WalletConnectEvents { val metaUrl: String, val method: String, ) : WalletConnectEvents + + data class PairConnectError(val error: Throwable) : WalletConnectEvents } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt index e96974c3b4..a6c5dabb19 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt @@ -22,17 +22,19 @@ sealed class WalletConnectAction : Action { data class ShowClipboardOrScanQrDialog(val wcUri: String) : WalletConnectAction() //region WalletConnect 2.0 - object ApproveProposal : WalletConnectAction() - object RejectProposal : WalletConnectAction() + data object ApproveProposal : WalletConnectAction() + data object RejectProposal : WalletConnectAction() - object SessionEstablished : WalletConnectAction() + data object SessionEstablished : WalletConnectAction() data class SessionRejected(val error: WalletConnectError) : WalletConnectAction() data class SessionListUpdated(val sessions: List) : WalletConnectAction() data class ShowSessionRequest(val sessionRequest: WcPreparedRequest) : WalletConnectAction() - object RejectUnsupportedRequest : WalletConnectAction() + data object RejectUnsupportedRequest : WalletConnectAction() data class PerformRequestedAction(val sessionRequest: WcPreparedRequest) : WalletConnectAction() + + data class PairConnectErrorAction(val throwable: Throwable) : WalletConnectAction() //endregion WalletConnect 2.0 } \ No newline at end of file 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 f55288968a..6b0e0898ec 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 @@ -47,8 +47,11 @@ class WalletConnectMiddleware { when (action) { is WalletConnectAction.HandleDeepLink -> { - if (!action.wcUri.isNullOrBlank()) { - store.dispatchOnMain(WalletConnectAction.OpenSession(action.wcUri)) + val wsUrl = action.wcUri + Timber.i("WC deeplink: $wsUrl") + if (!wsUrl.isNullOrBlank()) { + Timber.i("WC deeplink added to stack: $wsUrl") + walletConnectInteractor.addDeeplink(wsUrl) } } is WalletConnectAction.DisconnectSession -> { @@ -172,6 +175,9 @@ class WalletConnectMiddleware { is WalletConnectAction.RejectUnsupportedRequest -> { store.dispatchOnMain(GlobalAction.ShowDialog(WalletConnectDialog.UnsupportedNetwork())) } + is WalletConnectAction.PairConnectErrorAction -> { + store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.PairConnectErrorDialog(action.throwable))) + } } } diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt index 20217386f2..a7640ed758 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectReducer.kt @@ -14,6 +14,7 @@ object WalletConnectReducer { is WalletConnectAction.RejectProposal, is WalletConnectAction.SessionEstablished, is WalletConnectAction.SessionRejected, + is WalletConnectAction.PairConnectErrorAction, -> state.copy(loading = false) is WalletConnectAction.SessionListUpdated -> state.copy( wc2Sessions = action.sessions, 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 65d11fc520..d1a9368288 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 @@ -117,6 +117,8 @@ sealed class WalletConnectDialog : StateDialog { data class SignTransactionDialog( val data: WcPreparedRequest.SignTransaction, ) : WalletConnectDialog() + + data class PairConnectErrorDialog(val error: Throwable) : WalletConnectDialog() } data class WcTransactionData( diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt index 4715114d15..97d4a464d9 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import arrow.core.getOrElse import com.tangem.common.CompletionResult import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute @@ -14,10 +13,9 @@ import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchDialogShow @@ -38,7 +36,6 @@ import javax.inject.Inject @HiltViewModel internal class CardSettingsViewModel @Inject constructor( private val scanCardProcessor: ScanCardProcessor, - private val getUserWalletUseCase: GetUserWalletUseCase, private val tangemSdkManager: TangemSdkManager, savedStateHandle: SavedStateHandle, ) : ViewModel() { @@ -47,6 +44,8 @@ internal class CardSettingsViewModel @Inject constructor( ?.unbundle(UserWalletId.serializer()) ?: error("User wallet ID is required for CardSettingsViewModel") + private val scannedScanResponse = MutableStateFlow(value = null) + val screenState: MutableStateFlow = MutableStateFlow(getInitialState()) private fun getInitialState() = CardSettingsScreenState( @@ -58,13 +57,11 @@ internal class CardSettingsViewModel @Inject constructor( private fun scanCard() = viewModelScope.launch { scanCardProcessor.scan(allowsRequestAccessCodeFromRepository = true) .doOnSuccess { scanResponse -> + scannedScanResponse.value = scanResponse + val scannedUserWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() - val isCorrectUserWalletScanned = scannedUserWalletId == userWalletId - - if (isCorrectUserWalletScanned) { - val userWallet = getUserWallet() - - updateCardDetails(userWallet) + if (userWalletId == scannedUserWalletId || scannedUserWalletId == null) { + updateCardDetails(scanResponse) } else { store.dispatchDialogShow( AppDialog.SimpleOkDialogRes( @@ -76,9 +73,9 @@ internal class CardSettingsViewModel @Inject constructor( } } - private fun updateCardDetails(userWallet: UserWallet) { - val card = userWallet.scanResponse.card - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + private fun updateCardDetails(scanResponse: ScanResponse) { + val card = scanResponse.card + val cardTypesResolver = scanResponse.cardTypesResolver val cardId = cardTypesResolver.getCardId() val currentSecurityOption = getCurrentSecurityOption(card) @@ -112,7 +109,7 @@ internal class CardSettingsViewModel @Inject constructor( if (isResetCardAllowed) { CardInfo.ResetToFactorySettings( - description = getResetToFactoryDescription(card, cardTypesResolver), + description = getResetToFactoryDescription(card.backupStatus, cardTypesResolver), ).let(::add) } } @@ -129,9 +126,21 @@ internal class CardSettingsViewModel @Inject constructor( changeAccessCode() } is CardInfo.ResetToFactorySettings -> { + val card = requireNotNull(scannedScanResponse.value) { + "Impossible to reset card if ScanResponse is null" + }.card + Analytics.send(Settings.CardSettings.ButtonFactoryReset()) store.dispatchNavigationAction { - push(route = AppRoute.ResetToFactory(userWalletId)) + push( + route = AppRoute.ResetToFactory( + userWalletId = userWalletId, + cardSpecificInfo = AppRoute.ResetToFactory.CardSpecificInfo( + cardId = card.cardId, + backupStatus = card.backupStatus, + ), + ), + ) } } is CardInfo.SecurityMode -> { @@ -150,9 +159,9 @@ internal class CardSettingsViewModel @Inject constructor( } private fun changeAccessCode() = viewModelScope.launch { - val card = getUserWallet().scanResponse.card + val scanResponse = requireNotNull(scannedScanResponse.value) { "Scan response is null" } - when (val result = tangemSdkManager.setAccessCode(card.cardId)) { + when (val result = tangemSdkManager.setAccessCode(scanResponse.card.cardId)) { is CompletionResult.Success -> Analytics.send(Settings.CardSettings.UserCodeChanged()) is CompletionResult.Failure -> { Timber.e("Failed to change access code: ${result.error}") @@ -160,12 +169,6 @@ internal class CardSettingsViewModel @Inject constructor( } } - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - } - private fun isResetToFactoryAllowedByCard(card: CardDTO, cardTypesResolver: CardTypesResolver): Boolean { val hasPermanentWallet = card.wallets.any { it.settings.isPermanent } val isNotAllowed = hasPermanentWallet || cardTypesResolver.isStart2Coin() diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt index b576e8193e..5be7b9eb88 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/utils/ResetToFactory.kt @@ -5,8 +5,11 @@ import com.tangem.domain.models.scan.CardDTO import com.tangem.tap.features.details.ui.cardsettings.TextReference import com.tangem.wallet.R -internal fun getResetToFactoryDescription(card: CardDTO, typesResolver: CardTypesResolver): TextReference { - return if (card.backupStatus?.isActive != true || typesResolver.isTangemTwins()) { +internal fun getResetToFactoryDescription( + backupStatus: CardDTO.BackupStatus?, + typesResolver: CardTypesResolver, +): TextReference { + return if (backupStatus?.isActive != true || typesResolver.isTangemTwins()) { TextReference.Res(R.string.reset_card_without_backup_to_factory_message) } else { TextReference.Res(R.string.reset_card_with_backup_to_factory_message) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt index d578f0875f..2131cd42e2 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/resetcard/ResetCardViewModel.kt @@ -11,12 +11,11 @@ import com.tangem.common.routing.utils.popTo import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.DeleteSavedAccessCodesUseCase import com.tangem.domain.card.ResetCardUseCase +import com.tangem.domain.card.ResetCardUserCodeParams import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -39,7 +38,7 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel internal class ResetCardViewModel @Inject constructor( - private val getUserWalletUseCase: GetUserWalletUseCase, + getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val resetCardUseCase: ResetCardUseCase, private val deleteSavedAccessCodesUseCase: DeleteSavedAccessCodesUseCase, @@ -49,9 +48,26 @@ internal class ResetCardViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel() { - private val userWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID) + // region Card-set specific data. All cards from single set have the same userWalletId and cardTypesResolver + private val currentUserWalletId = savedStateHandle.get(AppRoute.ResetToFactory.USER_WALLET_ID) ?.unbundle(UserWalletId.serializer()) - ?: error("User wallet ID must be provided for ResetCardViewModel") + ?: error("UserWalletId must be provided for ResetCardViewModel") + + // Use only for card-specific data + private val userWallet = getUserWalletUseCase(userWalletId = currentUserWalletId) + .getOrElse { error("Failed to get user wallet: $it") } + + private val currentCardTypesResolver = userWallet.cardTypesResolver + private val currentUserCodeParams = ResetCardUserCodeParams( + isAccessCodeSet = userWallet.scanResponse.card.isAccessCodeSet, + isPasscodeSet = userWallet.scanResponse.card.isPasscodeSet, + ) + // endregion + + // region Data of card that was scanned on CardSettings + private val primaryCardId: String + private val primaryBackupStatus: CardDTO.BackupStatus? + // endregion // TODO: move logic to separate domain entity private var resetBackupCardCount = 0 @@ -60,29 +76,33 @@ internal class ResetCardViewModel @Inject constructor( value = getInitialState(), ) - private fun getInitialState(): ResetCardScreenState { - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - val card = userWallet.scanResponse.card - val cardTypesResolver = userWallet.scanResponse.cardTypesResolver - val descriptionText = getResetToFactoryDescription(card, cardTypesResolver) - val isTangemWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() - val showResetPasswordButton = isTangemWallet && card.backupStatus is CardDTO.BackupStatus.Active + init { + val cardSpecificInfo = savedStateHandle.get(AppRoute.ResetToFactory.CARD_SPECIFIC_DATA) + ?.unbundle(AppRoute.ResetToFactory.CardSpecificInfo.serializer()) + ?: error("CardSpecificData must be provided for ResetCardViewModel") + primaryCardId = cardSpecificInfo.cardId + primaryBackupStatus = cardSpecificInfo.backupStatus + } + + private fun getInitialState(): ResetCardScreenState { + val shouldShowResetPasswordButton = shouldShowResetPasswordButton() val warningsToShow = buildList { add(ResetCardScreenState.WarningsToReset.LOST_WALLET_ACCESS) - if (showResetPasswordButton) { + if (shouldShowResetPasswordButton) { add(ResetCardScreenState.WarningsToReset.LOST_PASSWORD_RESTORE) } } return ResetCardScreenState( resetButtonEnabled = false, - descriptionText = descriptionText, + descriptionText = getResetToFactoryDescription( + backupStatus = primaryBackupStatus, + typesResolver = currentCardTypesResolver, + ), warningsToShow = warningsToShow, - showResetPasswordButton = showResetPasswordButton, + showResetPasswordButton = shouldShowResetPasswordButton, acceptCondition1Checked = false, acceptCondition2Checked = false, onAcceptCondition1ToggleClick = ::toggleFirstCondition, @@ -92,6 +112,12 @@ internal class ResetCardViewModel @Inject constructor( ) } + private fun shouldShowResetPasswordButton(): Boolean { + val isTangemWallet = currentCardTypesResolver.isTangemWallet() || currentCardTypesResolver.isWallet2() + + return isTangemWallet && primaryBackupStatus is CardDTO.BackupStatus.Active + } + private fun toggleFirstCondition(isAccepted: Boolean) { screenState.update { prevState -> val resetButtonEnabled = if (prevState.showResetPasswordButton) { @@ -154,12 +180,9 @@ internal class ResetCardViewModel @Inject constructor( private fun makeFullReset() { viewModelScope.launch { - val userWallet = getUserWallet() - val scanResponse = userWallet.scanResponse - - resetCardUseCase(card = scanResponse.card).onRight { - deleteSavedAccessCodesUseCase(scanResponse.card.cardId) - val hasUserWallets = deleteWalletUseCase(userWalletId).getOrElse { + resetCardUseCase(cardId = primaryCardId, params = currentUserCodeParams).onRight { + deleteSavedAccessCodesUseCase(cardId = primaryCardId) + val hasUserWallets = deleteWalletUseCase(userWalletId = currentUserWalletId).getOrElse { Timber.e("Unable to delete user wallet: $it") return@launch } @@ -183,14 +206,15 @@ internal class ResetCardViewModel @Inject constructor( dismissDialog() viewModelScope.launch { - val userWallet = getUserWallet() resetCardUseCase( cardNumber = resetBackupCardCount + 1, - card = userWallet.scanResponse.card, - userWalletId = userWalletId, + params = currentUserCodeParams, + userWalletId = currentUserWalletId, ) - .onRight { - resetBackupCardCount++ + .onRight { isResetCompleted -> + if (isResetCompleted) { + resetBackupCardCount++ + } delay(DELAY_SDK_DIALOG_CLOSE) @@ -213,7 +237,7 @@ internal class ResetCardViewModel @Inject constructor( } private fun checkRemainingBackupCards() { - val backupCardsCount = getUserWallet().scanResponse.getBackupCardsCount() + val backupCardsCount = getBackupCardsCount() when { backupCardsCount > resetBackupCardCount -> showDialog(ResetCardDialog.ContinueResetDialog) @@ -227,12 +251,6 @@ internal class ResetCardViewModel @Inject constructor( } } - private fun getUserWallet(): UserWallet { - return getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } - } - private fun dismissAndFinishFullReset() { dismissDialog() @@ -249,7 +267,7 @@ internal class ResetCardViewModel @Inject constructor( if (isLocked && userWalletsListManager.hasUserWallets) { store.dispatchNavigationAction { popTo() } } else { - store.dispatchNavigationAction { popTo() } + store.dispatchNavigationAction { replaceAll(AppRoute.Home) } } } } @@ -262,10 +280,10 @@ internal class ResetCardViewModel @Inject constructor( screenState.update { it.copy(dialog = null) } } - private fun ScanResponse.getBackupCardsCount(): Int { - if (!cardTypesResolver.isMultiwalletAllowed()) return 0 + private fun getBackupCardsCount(): Int { + if (!currentCardTypesResolver.isMultiwalletAllowed()) return 0 - return when (val status = card.backupStatus) { + return when (val status = primaryBackupStatus) { is CardDTO.BackupStatus.Active -> status.cardCount is CardDTO.BackupStatus.CardLinked, is CardDTO.BackupStatus.NoBackup, 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 efa987c847..c86ed79d1d 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 @@ -480,7 +480,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) when (result) { is CompletionResult.Success -> { val backupValidator = BackupValidator() - if (!backupValidator.isValid(CardDTO(result.data))) { + if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) { store.dispatchOnMain(BackupAction.ErrorInBackupCard) } if (backupService.currentState == BackupService.State.Finished) { diff --git a/common/routing/build.gradle.kts b/common/routing/build.gradle.kts index ae4552a1f0..a492ede7aa 100644 --- a/common/routing/build.gradle.kts +++ b/common/routing/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { /* Domain */ implementation(projects.domain.qrScanning.models) + implementation(projects.domain.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.staking.models) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index bdd1ea3030..8f9796de2b 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -5,6 +5,7 @@ import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.tokens.model.CryptoCurrency @@ -143,15 +144,26 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object AppSettings : AppRoute(path = "/app_settings") + /** + * Reset to factory route + * + * @property userWalletId user wallet id + * @property cardSpecificInfo info about card that was scanned on CardSettings + */ @Serializable data class ResetToFactory( val userWalletId: UserWalletId, - ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}"), RouteBundleParams { + val cardSpecificInfo: CardSpecificInfo, + ) : AppRoute(path = "/reset_to_factory/${userWalletId.stringValue}/$cardSpecificInfo"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) + @Serializable + data class CardSpecificInfo(val cardId: String, val backupStatus: CardDTO.BackupStatus?) + companion object { const val USER_WALLET_ID = "userWalletId" + const val CARD_SPECIFIC_DATA = "cardSpecificInfo" } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index e7c941c536..778d4f08d8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -93,7 +93,7 @@ object BigDecimalFormatter { maximumFractionDigits = cryptoCurrency.decimals minimumFractionDigits = 2 isGroupingUsed = true - roundingMode = RoundingMode.DOWN + roundingMode = RoundingMode.HALF_UP } return formatter.format(cryptoAmount).let { diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt index 7bdd4f2293..7a71009602 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ResetCardUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.card import arrow.core.Either import com.tangem.domain.card.models.ResetCardError -import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.wallets.models.UserWalletId /** @@ -12,13 +11,18 @@ import com.tangem.domain.wallets.models.UserWalletId */ interface ResetCardUseCase { - /** Reset card [card] to factory settings */ - suspend operator fun invoke(card: CardDTO): Either + /** Reset card [cardId] to factory settings */ + suspend operator fun invoke(cardId: String, params: ResetCardUserCodeParams): Either - /** Reset backup card [cardNumber] with expected [UserWalletId] using [card] of reset card */ + /** Reset backup card [cardNumber] with expected [UserWalletId] using [params] of reset card */ suspend operator fun invoke( cardNumber: Int, - card: CardDTO, + params: ResetCardUserCodeParams, userWalletId: UserWalletId, - ): Either -} \ No newline at end of file + ): Either +} + +data class ResetCardUserCodeParams( + val isAccessCodeSet: Boolean, + val isPasscodeSet: Boolean?, +) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index 68c6988923..ab0183d09e 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -8,6 +8,8 @@ import java.math.BigDecimal sealed interface LegacyAction : Action { + data class SendEmailSupport(val scanResponse: ScanResponse) : LegacyAction + data class SendEmailRateCanBeBetter(val scanResponse: ScanResponse) : LegacyAction /** diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index fbccb4120d..bf25c0f8b6 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -1,9 +1,11 @@ plugins { alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) id("configuration") } dependencies { implementation(deps.tangem.card.core) implementation(deps.moshi.kotlin) + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt index ff2b5be1d0..91ceeb0049 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt @@ -8,6 +8,8 @@ import com.tangem.common.card.EncryptionMode import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.operations.attestation.Attestation +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import java.util.Date import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion @@ -298,11 +300,19 @@ data class CardDTO( } } + @Serializable sealed class BackupStatus { + + @Serializable + @SerialName("card_linked") data class CardLinked(val cardCount: Int) : BackupStatus() + @Serializable + @SerialName("active") data class Active(val cardCount: Int) : BackupStatus() + @Serializable + @SerialName("no_backup") data object NoBackup : BackupStatus() val isActive: Boolean diff --git a/features/disclaimer/impl/build.gradle.kts b/features/disclaimer/impl/build.gradle.kts index bb7ee06e94..d60dcd1885 100644 --- a/features/disclaimer/impl/build.gradle.kts +++ b/features/disclaimer/impl/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.accompanist.systemUiController) implementation(deps.compose.accompanist.permission) + implementation(deps.compose.accompanist.webView) implementation(deps.compose.material3) implementation(deps.compose.material) @@ -41,6 +42,9 @@ dependencies { implementation(projects.features.disclaimer.api) implementation(projects.features.pushNotifications.api) + /** Other dependencies */ + implementation(deps.arrow.core) + /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt new file mode 100644 index 0000000000..1d60540d5e --- /dev/null +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/local/LocalTermOfServices.kt @@ -0,0 +1,233 @@ +package com.tangem.features.disclaimer.impl.local + +internal val localTermsOfServices = """ + + + + + + + + Legal Disclaimer + + + + +

TANGEM WALLET AND TANGEM MOBILE APPLICATION

+

Terms of Service

+ +

PLEASE READ THESE TERMS OF SERVICE CAREFULLY. BY CLICKING TO ACCEPT, OR BY ACCESSING OR USING OUR SERVICES, YOU AGREE THAT YOU HAVE READ, UNDERSTOOD, AND ACCEPT ALL OF THE TERMS AND CONDITIONS CONTAINED HEREIN. BY PURCHASE OF TANGEM WALLET (CARD) OR BY USING TANGEM WALLET (CARD) OR BY USING TANGEM MOBILE APPLICATION, YOU DEMONSTRATE YOUR AGREEMENT TO THESE TERMS AND CONDITIONS CONTAINED HEREIN.

+ +

1. DEFINITIONS

+

“Cardholder” refers to an individual who owns a Tangem Card that is used to access the Tangem Wallet and Tangem Mobile Application.

+

“Blockchain Asset” refers to digital assets, including but not limited to cryptocurrencies, that can be managed through the Tangem Wallet.

+

“Blockchain Address” means a unique identifier that serves as a virtual location of the Blockchain Asset in the blockchain.

+

“Card Transaction” means transfer of Blockchain Asset from the Blockchain Address associated with the Public Key stored on the Tangem Wallet (Card).

+

“Official Mobile Application” means an application developed and distributed by Tangem, providing interoperability between Tangem Wallet (Card) and blockchain, and working on NFC-capable smartphones and tablets using Google Android and Apple iOS operation systems.

+

“Private Key” means a secret cryptographic key which provides full control over a Blockchain Asset.

+

“Tangem” means Tangem AG, a company incorporated under the laws of Switzerland (CHE-390.112.525), with a registered address at Baarerstrasse 10, CH-6300 Zug.

+

“Public Key” means a cryptographic key, which provides access to information about a Blockchain Asset, including but not limited to Blockchain Address

+

“Services” means the purchase and/or use of Tangem Wallet (Card) and the services or any other features, technologies or functionalities linked to the Tangem Wallet (Card) provided or operated by Tangem via the website or Official Mobile Applications.

+

“Tangem Wallet (Card) / Tangem Card / Card” means physical card which stores Private Key and Public Key and is used as a backup card.

+ +

2. DISCLAIMER

+

2.1. Bitcoin and other cryptocurrencies are virtual currencies, digital representations of value that are neither issued by a central bank of any state or public authority attached to a conventional currency, but may be used by any natural or legal persons as a means of exchange and can be transferred, stored or traded electronically.

+

2.2. Blockchain technologies and related services are subject to continuous regulatory changes and scrutiny around the world, including but not limited to anti-money laundering and financial regulations. You acknowledge that certain Services, including their availability, could be impacted by one or more regulatory requirements.

+

2.3. No advice. No part of the information herein should be considered to be business, legal, financial or tax advice regarding the Products or Services. You should consult your own legal, financial, tax or other professional advisor regarding the matter. By using the Services, you represent that Tangem is responsible neither for obtaining the information about tax or similar obligations arising in relation to usage of the Services nor for fulfillment of such tax (or similar) obligations.

+ +

3. GENERAL PROVISIONS

+

3.1. These Terms of Service (the “Terms”) govern the use of Tangem Wallet and/or Official Mobile Application provided by Tangem (referred to as "Tangem", "we" or "us" in this document) and the related services or any other features, technologies or functionalities linked to Tangem Wallet (Card). Tangem is a company incorporated under the laws of Switzerland (CHE-390.112.525), with a registered address at Baarerstrasse 10, CH-6300 Zug.

+

3.2. Tangem Cards may be used for storage of Private Key and Public Key to Cardholder’s Blockchain Assets and authentication of Card Transactions with the purpose of “person-to-person” transfer of Blockchain Assets to another blockchain address.

+

3.3. Official Mobile Application is intended for usage only with Tangem Wallet (Card), providing interoperability between the cards and blockchain via NFC interface. Official Mobile Application DOES NOT:

+

3.3.1. Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain wallets holding Blockchain assets.

+

3.3.2. Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases that can be used to restore or to copy private (secret) keys to blockchain wallets holding Blockchain assets.

+

3.3.3 Provide exchange, trading, investment services on behalf of Tangem.

+ +

4. RIGHTS AND OBLIGATIONS

+

4.1. Cardholder agrees that these Terms are binding.

+

4.2. Cardholder shall be the only person having physical access to Tangem Wallet.

+

4.3. Cardholder acknowledges and agrees that Tangem does not provide backup or recovery of Private Key and Public Key stored by Tangem Wallet (Card).

+

4.4. Tangem does not keep records of Cardholder’s personal data, the amount of Blockchain Asset stored on the Card, the Private Key, or personalized history of Card usage.

+ +

5. COSTS

+

5.1. Costs, fees and commission (the “Costs”) may be charged in connection with the use of Card. These Costs are disclosed in Official Mobile Applications to Cardholder.

+

5.2. Amendments to Costs due to changing expenses or market conditions may be made at any time via adjustments to the fee schedules. Such amendments shall be communicated to Cardholder in an appropriate manner. Upon notification and in the event of the objection, Cardholder may cancel the Card with immediate effect.

+ +

6. CARDHOLDER’S DUTIES OF CARE

+

6.1. In particular, Cardholder shall exercise the following duties of care:

+

6.1.1. Upon receiving the Card, Cardholder should download Official Mobile Applications in order to create the backup and if applicable determine the amount of Blockchain Asset stored through Official Mobile Applications.

+

6.1.2. Cardholder shall keep the means of access and Tangem Wallet (Card) with care and all Cards separate from each other.

+

6.1.3. Cardholder must always know where Tangem Wallet (Card) is and regularly ensure that it is still in his/her possession. He/she shall avoid even temporary possession of Tangem Wallet (Card) by any other person.

+

6.1.4. Cardholder shall treat Tangem Wallet in the same manner as physical money (cash) and keep it safe. If any of the Card is lost, stolen or destroyed, control over the corresponding Blockchain Asset may be permanently lost.

+

6.1.5. Before using the Card with Official Mobile Applications, Cardholder shall locate Official Mobile Applications in Google Play or Apple app store and install it as instructed in the Card box.

+

6.1.6. Card shall be used only with Official Mobile Applications and as instructed in the Card box.

+

6.1.7. Official Mobile Application shall be the only source of information about the Blockchain Address of the Blockchain Asset and corresponding Public Key stored on Tangem Wallet (Card).

+

6.1.8. Cardholder shall only use Near-Field Devices (the “NFC”) devices that are capable of running Official Mobile Applications. He/she shall avoid leaving Tangem Wallet (Card) in the proximity of the NFC devices of other persons.

+

6.1.9. Tangem Wallet (Card) shall be used only for physically tapping and holding near Cardholder’s NFC device when Official Mobile Application requests it.

+

6.1.10. Cardholder shall keep Tangem Wallet (Card) with care and protect Tangem Wallet (Card) from mechanical damage, high temperatures, strong electromagnetic fields, and other harmful factors.

+

6.2. No retrieval of Private Keys. Tangem operates non-custodial services, which means that we do not store, nor do we have access to your Blockchain Assets nor your Private Keys. Tangem does not have access to or store passwords, 24-word Recovery Phrase, Private Keys, passphrases, transaction history, PIN, or other credentials associated with your use of the Services. You are solely responsible for remembering, storing, and keeping your credentials in a secure location, away from prying eyes. Any third party with knowledge of one or more of your 24-word Recovery Phrase can gain control of the Private Keys associated with your Tangem Wallet (Card) or of the 24-word Recovery Phrase, and therefore steal your Blockchain Assets, without any possibility for you or Tangem to retrieve them.

+ +

7. RIGHTS AND RESPONSIBILITIES OF CARDHOLDER

+

7.1. Cardholder is liable for all liabilities arising from the use of Tangem Wallet (Card) and/or Tangem Official Mobile Application. Any disputes in relation to discrepancies and complaints about goods or services and any resulting claims must be settled directly by Cardholder with the respective Reseller.

+

7.2. As a matter of principle, Cardholder is liable for any risks resulting from the misuse of Tangem Wallet (Card) and/or Official Mobile Application. In any case, Cardholder is solely liable for all transactions authorized using a means of access.

+

7.3. Any loss or damage resulting from the forwarding of Tangem Wallet (Card) and/or means of access shall be borne by Cardholder.

+

7.4. Loss or damage incurred by Cardholder in connection with the possession or use of Tangem Wallet (Card) and/or Official Mobile Application shall be borne solely by Cardholder. Tangem assumes no liability if Tangem Wallet (Card) and/or Official Mobile Application cannot be used due to a technical defect or because it has been canceled, blocked or the spending limit has been adjusted.

+

7.5. Cardholder is only permitted to use Tangem Wallet (Card) and Official Mobile Application for his personal, non-commercial use. Cardholder is not allowed to resell Tangem Wallet (Card).

+

7.6. Cardholder is solely responsible to determinate what, if any, taxes apply to Card Transactions. Tangem or contributors to Official Mobile Applications are NOT responsible for determining the taxes that apply to Card Transactions.

+

7.7. Before Cardholder engages in transactions using an electronic system, Cardholder should carefully review the rules and regulations of the exchanges offering the system and/or listing the instruments Cardholder intends to trade. Online trading has inherent risk due to system response and access times that may vary due to market conditions, system performance, and other factors. Cardholder should understand, fully accept and take on these and additional risks before trading.

+

7.8. There is considerable exposure to risk in the Blockchain Asset exchange transaction. Any transaction involving the Blockchain Asset involves risks including, but not limited to, the potential for changing economic conditions that may substantially affect the price or liquidity of the Blockchain Asset. Investments in the Blockchain Asset exchange speculation may also be susceptible to sharp rises and falls as the relevant market values fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.

+

7.9. Before initiating any transactions through third-party resources via widgets or links within the application, Cardholder is expressly advised to meticulously review and comprehend the terms, rules, and regulations governing such resources. It is imperative for Cardholder to be cognizant of the inherent risks associated with online trading, including variations in system response times, access delays influenced by market conditions, system performance, and other pertinent factors.

+

7.10. Cardholder unequivocally assumes sole responsibility for all actions undertaken, encompassing but not limited to swap transactions, on-ramp, and off-ramp activities, when transitioning to third-party resources through widgets or links within the application. This responsibility extends to compliance with the terms and conditions of the relevant third-party resources and adherence to applicable laws and regulations.

+

7.11. Cardholder acknowledges and accepts that the use of third-party resources involves inherent risks, and Tangem shall bear no liability for the consequences arising from Cardholder's independent actions on these external platforms.

+

7.12. Cardholder is responsible for implementing adequate security measures and precautions when interacting with third-party resources to safeguard personal information, financial assets, and to mitigate potential risks associated with such engagements.

+

7.13. Cardholder agrees to indemnify and hold Tangem, its affiliates, and service providers harmless from any claims, losses, or damages incurred as a result of their actions on third-party platforms, as outlined in the Terms of Services.

+

7.14. Tangem explicitly disclaims any affiliation, endorsement, or responsibility for the content, policies, or transactions on third-party resources, and Cardholder interactions with such resources are entirely at their own risk.

+ +

8. THIRD-PARTY SERVICES

+

8.1. We may incorporate, reference and/or provide access to Third Party Services. For instance, buy, sell and crypto to crypto exchange (“swap”) services are Third Party Services. You agree that your use of Third-Party Services is subject to separate terms and conditions between you and the third-party identified in Tangem.

+

8.2. Tangem is not responsible for the content, accuracy, security, availability, any performance, or failure to perform of the Third-Party Services or any issue in relation with the use of Third-Party Services. Tangem does not provide any guarantees that access to Third-Party Services will not be interrupted or that there will be no delays, failures, errors, omissions, corruption or loss of transmitted information, data or funds, and Tangem shall not be liable for any such Third-Party Services. You agree to use the Third-Party Services at your own risk. It is your responsibility to review the third party’s terms and policies before using a Third-Party Service. Third-Party Services may not be available in all languages and may not be appropriate or available for use in any particular location. To the extent you choose to use such Third-Party Services, you are solely responsible for compliance with any applicable laws in relation to such use. In addition, Tangem reserves the right to block access to these Third-Party Services through Tangem Live in particular, but not exclusively, in the event of non-compliance with the applicable regulations by the Third-Party partner. We retain the exclusive right to suspend, remove, or cancel the availability of any such Third-Party Service for any reason and without prior notice.

+ +

9. RESPONSIBILITIES AND LIABILITIES OF TANGEM

+

9.1. Tangem does not warrant or make any representations regarding the use, the inability to use or operate, or the results of the use or operation of Tangem Wallet (Card) and/or Official Mobile Application.

+

9.2. Tangem does not keep any records of Cardholder information, the amount of Blockchain Asset stored on Tangem Wallet (Card), the Private Key, or personalized history of cards usage.

+

9.3. Tangem does not provide any backup or recovery of Private Key and Public Key stored on Tangem Wallet (Card).

+

9.4. Tangem shall not be held liable for any failure to be able to use Tangem Wallet (Card) and/or Official Mobile Application, for any reason whatsoever, nor will Tangem be held liable for the loss of the Blockchain Asset resulting from a malfunction or inoperability of the blockchain network hosting Blockchain Asset, as well as the inaccessibility of its public servers and services.

+

9.5. Tangem does not guarantee that the operation of Tangem Wallet (Card) and/or Official Mobile Application will be secure, accurate, complete, uninterrupted, without error or free of viruses, worms, other harmful components or other program limitations. Tangem may, at its sole discretion and without obligation to do so, correct, modify, amend, enhance, improve and make any other changes to Tangem Wallet and/or Official Mobile Application, change, update or suspend the Services, temporarily or indefinitely, so as to carry out works including, but not limited to: firmware and software updates, maintenance operations, amendments to the servers, bug fixes, etc. We will make reasonable efforts to give you prior notice of any significant disruption of the Services. Tangem does not guarantee the correct functioning of the Services in the event of the installation or use of programs or applications that do not conform to Service specifications and technical standards.

+

9.6. Tangem shall not be held liable for the loss of profits, income, value or any indirect, extraordinary, consequential, exemplary or punitive damages.

+

9.7. Tangem shall not be held liable for loss or breakdown of Tangem Wallet (Card).

+

9.8. Tangem shall not be held liable for any loss of Blockchain Asset in the event of loss or total breakdown of Tangem Wallet (Card).

+ +

10. GUARANTEES

+

10.1. Under the condition that Cardholder exercises the duties of care as stated in Clause 5, Tangem guarantees that Tangem Wallet (Card) will function properly and without restriction for a period of 2 (two) years. In the event of a breakdown of Tangem Wallet (Card) without it being the fault of Cardholder due to reasons mentioned in Clause 5 of these Terms, Tangem will replace Tangem Wallet with a new one. Cardholder shall inform Tangem on the event of a breakdown by sending an e-mail to support@tangem.com. If failed to resolve with support team of Tangem, Cardholder shall wait for instructions on safe shipping of Tangem Wallet (Card).

+

10.2. Tangem guarantees that Tangem Wallet (Card) prevents duplication of the Private Key and that Cardholder has exclusive control over the Blockchain Asset unless the contrary is imposed by specific blockchain network rules, e.g. two or more private keys can be used to control the same Blockchain Asset.

+ +

11. LIMITATION OF LIABILITY

+

11.1. Tangem Wallet (Card), including without limitation any content, data and information related thereto, is provided on an “as is” basis and “as available” basis, without any warranties of any kind, express or implied warranties of use, merchantability or suitability for a certain purpose or use, including without limitation, the quality of products and services provided by users, third-party services, and/or exchanges (except for the guarantees set forth in Section 9).

+

11.2. Official Mobile Application is provided on an “as is” basis and “as available” basis without any warranties of any kind regarding Official Mobile Application and/or any content, data, materials and/or services provided on Official Mobile Application.

+

11.3. Tangem and its affiliates, including any of their officers, directors, shareholders, employees, sub-contractors, agents, parent companies, subsidiaries and other affiliates (collectively, the “Tangem Affiliates”), jointly and severally, disclaim and make no representations or warranties as to the usability, accuracy, quality, availability, reliability, suitability, completeness, truthfulness, usefulness or effectiveness of any content, data, results or other information obtained or generated by Tangem and/or any user related to you or any other user of Tangem Wallet (Card), and Official Mobile Applications.

+

11.4. In no event shall Tangem and/or any of Tangem Affiliates be liable for any damages whatsoever, including direct, indirect, extraordinary, incidental or consequential damages of any kind, but not limited to, resulting from or arising out of the use of Tangem Wallet (Card) and/or Official Mobile Applications or inability to use Tangem Wallet (Card) and/or Official Mobile Applications, failure of Tangem Wallet (Card) and/or Official Mobile Applications to perform as represented or expected, loss of goodwill or profits, or loss of data arising out of or in any way connected with the use of Tangem Wallet (Card) and/or Official Mobile Applications. In no event shall Tangem and/or any of Tangem Affiliates be liable for the performance or failure of Tangem Wallet (Card) and/ or Official Mobile Applications to perform under these Terms of Use and any other act or omission by Tangem by any cause whatsoever including without limitation damages arising from the conduct of any users, third party services and/or exchanges. In no way Tangem or contributors to Official Mobile Application are responsible for the actions, decisions, or other behavior taken or not taken by Cardholder in reliance upon Tangem Official Mobile Application.

+

11.5. You hereby acknowledge and agree that these limitations of liability are agreed allocations of risk constituting in part the consideration for using Tangem Wallet (Card) and Official Mobile Applications and such limitations will apply notwithstanding the failure of essential purpose of any limited remedy, and even if Tangem and/or any Tangem Affiliates has been advised of the possibility of such liabilities and/or damages.

+

11.6. Tangem will not be responsible for any losses, damages or claims arising from events falling within the scope of the following five categories:

+

11.6.1. Mistakes made by Cardholder, e.g., forgotten passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Tangem Wallet (Card).

+

11.6.2. Problems of Official Mobile Application and/or any blockchain- or cryptocurrency- related software or service, e.g., corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.

+

11.6.3. Technical failures in the hardware of Cardholder, including cards, of any blockchain- or cryptocurrency- related software or service, e.g., data loss due to a faulty or damaged storage device.

+

11.6.4. Security problems experienced by Cardholder, e.g., unauthorized access to Cardholders' wallets and/or accounts.

+

11.6.5. Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy of service providers, information security attacks on service providers, and fraud conducted by third parties.

+ +

12. GOVERNING LAW AND DISPUTE RESOLUTION

+

12.1. Unless otherwise required by a mandatory law of a member state of the European Union or any other jurisdiction these Terms of Service and any separate agreements whereby we provide you Services shall be governed by the laws of Switzerland without regard to its conflict of laws principles.

+

12.2. You can submit a claim in written form regarding the operation of the Services to us via email at store@tangem.com. You may also reach us in writing at the following address: Tangem AG, Baarerstrasse 10, Zug, CH-6300 Switzerland. In case of failure to resolve disputes and disagreements by way of negotiations the settlement shall be in accordance with claim procedure. Claims shall be reviewed within 30 calendar days.

+

12.3. Subject to compulsory legal provisions, any use of the Services and all legal disputes arising out of or in connection therewith shall be submitted to the exclusive jurisdiction of the courts of the Canton of Zug.

+ +

13. COMMUNICATION

+

13.1. In the event when under the Terms Tangem provides the User with any information that relates to the Services provided hereunder, this information may be given to the Client through the Website without sending said information directly to the User’s address and / or using other secure means.

+

13.2. Tangem shall respond to requests from the User promptly and within 7 calendar days following the date of receipt of the request. The response time may in some cases may exceed 7 calendar days.

+ +

14. MISCELLANEOUS

+

14.1. Entire agreement. These Terms and any policies or operating rules posted by us on the Website or in respect to the Services constitutes the entire agreement and understanding between you and us and govern your use of the Services, superseding any prior or contemporaneous agreements, communications and proposals, whether oral or written, between you and us (including, but not limited to, any prior versions of the Terms).

+

14.2. Severability. In the event that any provision of these Terms is determined to be unlawful, void or unenforceable, such provision will nonetheless be enforceable to the fullest extent permitted by applicable law, and the unenforceable portion will be deemed to be severed from these Terms of Service, such determination will not affect the validity and enforceability of any other remaining provisions.

+

14.3. Assignment. You may not assign your rights or obligations under these Terms in whole or in part to any third party. You acknowledge and agree that Tangem may assign its rights and obligations under these Terms, including rights and obligations concerning insurance, and, in such context, share or transfer information provided by you while using the Services to a third party.

+

14.4. No waiver. The failure of us to exercise or enforce any right or provision of these Terms will not constitute a waiver of such right or provision.

+

14.5. Any ambiguities in the interpretation of these Terms will not be construed against the drafting party.

+

14.6. Errors, Inaccuracies, And Omissions. Occasionally there may be information in the Services that contains typographical errors, inaccuracies or omissions that may relate to product descriptions, pricing, promotions, offers, product shipping charges, transit times and availability. We reserve the right to correct any errors, inaccuracies or omissions, and to change or update information or cancel orders if any information in the Services or on any related website is inaccurate at any time without prior notice (including after you have submitted your order).

+

14.7. Terms concerning Recovery Phrase apply to Tangem Wallet (Card) supporting this feature.

+

14.8. We undertake no obligation to update, amend or clarify information in the Services, including without limitation, pricing information, except as required by law. No specified update or refresh date applied in the Services or on any related website, should be taken to indicate that all information in the Services has been modified or updated.

+

14.9. These Terms may be drawn up in different languages. In case of any inconsistency the English version of the Terms shall prevail.

+ +

Last amended on: March 1st, 2024

+ + + +""".trimIndent() \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt index e3007b5a5a..f1be4240e0 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/model/DisclaimerModel.kt @@ -6,8 +6,11 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.domain.card.repository.CardRepository +import com.tangem.domain.settings.NeverRequestPermissionUseCase +import com.tangem.domain.settings.NeverToInitiallyAskPermissionUseCase import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.disclaimer.impl.entity.DisclaimerUM +import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -18,6 +21,8 @@ internal class DisclaimerModel @Inject constructor( private val cardRepository: CardRepository, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, + private val neverToInitiallyAskPermissionUseCase: NeverToInitiallyAskPermissionUseCase, + private val neverRequestPermissionUseCase: NeverRequestPermissionUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -38,6 +43,8 @@ internal class DisclaimerModel @Inject constructor( if (shouldAskPushPermission) { router.push(AppRoute.PushNotification) } else { + neverToInitiallyAskPermissionUseCase(PUSH_PERMISSION) + neverRequestPermissionUseCase(PUSH_PERMISSION) router.push(AppRoute.Home) } } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index fddb1a255b..b3740659cc 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -2,26 +2,28 @@ package com.tangem.features.disclaimer.impl.ui import android.annotation.SuppressLint import android.content.res.Configuration -import android.view.View -import android.view.ViewGroup -import android.webkit.WebView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +import com.google.accompanist.web.rememberWebViewStateWithHTMLData import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.NavigationBar3ButtonsScrim import com.tangem.core.ui.components.PrimaryButton @@ -35,6 +37,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.disclaimer.impl.R import com.tangem.features.disclaimer.impl.entity.DisclaimerUM import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer +import com.tangem.features.disclaimer.impl.local.localTermsOfServices import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull @Composable @@ -63,6 +66,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { iconRes = R.drawable.ic_back_24, onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, + titleAlignment = Alignment.CenterHorizontally, textColor = textColor, iconTint = iconColor, ) @@ -81,52 +85,51 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { @SuppressLint("SetJavaScriptEnabled") @Composable private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { - val progressState = remember { mutableStateOf(ProgressState.Loading) } - val webClient = remember { DisclaimerWebViewClient(progressState) } - val transparent = Color.Transparent val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 - Box( - modifier = Modifier, - ) { - AndroidView( - factory = { - WebView(it).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - setBackgroundColor(transparent.toArgb()) - settings.allowFileAccess = false - // to inject css style to display only in dark theme - settings.javaScriptEnabled = !isTosAccepted - overScrollMode = View.OVER_SCROLL_NEVER - webViewClient = webClient - clearHistory() - clearFormData() - clearCache(true) + val webViewStateUrl = rememberWebViewState(url) + val webViewStateData = + rememberWebViewStateWithHTMLData(data = localTermsOfServices, mimeType = "text/html", encoding = "UTF-8") - loadUrl(url) - } + val webViewState by remember { + derivedStateOf { + if (webViewStateUrl.errorsForCurrentRequest.isNotEmpty()) { + webViewStateData + } else { + webViewStateUrl + } + } + } + + Box { + WebView( + state = webViewState, + captureBackPresses = false, + onCreated = { + it.settings.javaScriptEnabled = !isTosAccepted + it.setBackgroundColor(backgroundColor.toArgb()) }, + client = remember { DisclaimerWebViewClient() }, ) - when (progressState.value) { - ProgressState.Loading -> { - Box( + AnimatedVisibility( + visible = webViewState.isLoading, + label = "Loading state change animation", + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(backgroundColor), + ) { + CircularProgressIndicator( + color = TangemTheme.colors.icon.informative, modifier = Modifier - .fillMaxSize() - .background(backgroundColor), - ) { - CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.Center) - .padding(TangemTheme.dimens.spacing8), - ) - } + .align(Alignment.Center) + .padding(TangemTheme.dimens.spacing8), + ) } - else -> Unit } } } diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt index 5ad8aedb8f..c3046d0855 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt @@ -1,8 +1,8 @@ package com.tangem.features.disclaimer.impl.ui import android.graphics.Bitmap -import android.webkit.* -import androidx.compose.runtime.MutableState +import android.webkit.WebView +import com.google.accompanist.web.AccompanistWebViewClient internal enum class ProgressState { Loading, @@ -26,57 +26,20 @@ private fun WebView.injectCSS() { evaluateJavascript(code, null) } -internal class DisclaimerWebViewClient(private val progressState: MutableState) : WebViewClient() { - - private var loadingUrl: String? = null - private var loadedUrl: String? = null - - fun reset() { - loadingUrl = null - loadedUrl = null - progressState.value = ProgressState.Loading - } +internal class DisclaimerWebViewClient : AccompanistWebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { view?.injectCSS() super.onPageStarted(view, url, favicon) + } - if (loadingUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Loading - loadingUrl = url + override fun onPageCommitVisible(view: WebView?, url: String?) { + view?.injectCSS() + super.onPageCommitVisible(view, url) } override fun onPageFinished(view: WebView?, url: String?) { view?.injectCSS() super.onPageFinished(view, url) - - if (loadedUrl != url && progressState.value != ProgressState.Error) progressState.value = ProgressState.Done - loadedUrl = url - } - - override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) { - view?.injectCSS() - super.onReceivedError(view, resourceRequest, error) - error?.let { progressState.value = ProgressState.Error } - } - - override fun onReceivedHttpError( - view: WebView?, - resourceRequest: WebResourceRequest?, - errorResponse: WebResourceResponse?, - ) { - view?.injectCSS() - super.onReceivedHttpError(view, resourceRequest, errorResponse) - - if (resourceRequest != null && errorResponse != null) { - val isDifferentUrl = resourceRequest.url?.toString() != loadingUrl - val isSuccessCode = errorResponse.statusCode < RESPONSE_USER_ERROR_STATUS_CODE - val isNotDone = progressState.value != ProgressState.Done - if (isDifferentUrl || isSuccessCode || isNotDone) return - progressState.value = ProgressState.Error - } - } - - companion object { - private const val RESPONSE_USER_ERROR_STATUS_CODE = 400 } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt index 4153d69e68..d83e759503 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/BackupValidator.kt @@ -7,10 +7,14 @@ import javax.inject.Inject class BackupValidator @Inject constructor() { - fun isValid(cardDTO: CardDTO): Boolean { + fun isValidFull(cardDTO: CardDTO): Boolean { return validateBackupStatus(cardDTO) && validateCurves(cardDTO) } + fun isValidBackupStatus(cardDTO: CardDTO): Boolean { + return validateBackupStatus(cardDTO) + } + private fun validateCurves(cardDTO: CardDTO): Boolean { val config = CardConfig.createConfig(cardDTO) // / Since the curve `bls12381_G2_AUG` was added later into first generation of wallets, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 25ef6de005..031c843c16 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -53,7 +53,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( buildList { addSwapPromoNotification(shouldShowPromo, promoBanner, clickIntents) - addCriticalNotifications(userWallet) + addCriticalNotifications(userWallet, clickIntents) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -87,11 +87,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( ) } - private fun MutableList.addCriticalNotifications(userWallet: UserWallet) { + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + clickIntents: WalletClickIntents, + ) { val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( - element = WalletNotification.Critical.BackupError, - condition = !backupValidator.isValid(userWallet.scanResponse.card) || userWallet.hasBackupError, + element = WalletNotification.Critical.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, ) addIf( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index 0ed8bda419..c878757dbc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -19,11 +19,16 @@ import org.joda.time.DateTime @Immutable sealed class WalletNotification(val config: NotificationConfig) { - sealed class Critical(title: TextReference, subtitle: TextReference) : WalletNotification( + sealed class Critical( + title: TextReference, + subtitle: TextReference, + buttonsState: NotificationConfig.ButtonsState? = null, + ) : WalletNotification( config = NotificationConfig( title = title, subtitle = subtitle, iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, ), ) { @@ -37,9 +42,13 @@ sealed class WalletNotification(val config: NotificationConfig) { subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), ) - data object BackupError : Critical( + data class BackupError(val onSupportClick: () -> Unit) : Critical( title = resourceReference(R.string.warning_backup_errors_title), subtitle = resourceReference(R.string.warning_backup_errors_message), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(id = R.string.details_row_title_contact_to_support), + onClick = onSupportClick, + ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index e90a55faac..b8ee6c5ef5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -62,6 +62,8 @@ internal interface WalletWarningsClickIntents { fun onTravalaPromoClick(link: String?) fun onCloseTravalaPromoClick() + + fun onSupportClick() } @Suppress("LongParameterList") @@ -262,6 +264,15 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } + override fun onSupportClick() { + reduxStateHolder.dispatch( + LegacyAction.SendEmailSupport( + scanResponse = getSelectedUserWallet()?.scanResponse + ?: error("ScanResponse must be not null"), + ), + ) + } + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index ce194116f9..d6bb419a92 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-702" +tangemBlockchainSdk = "develop-703" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-375" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/version.properties b/version.properties index f8c3184558..16ef03c0ac 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -versionName=5.13.0 \ No newline at end of file +versionName=5.14.0 \ No newline at end of file