From f89f2ea32e3be9e6b2ea883d87e4471f1bcbb30f Mon Sep 17 00:00:00 2001 From: polstianka Date: Tue, 8 Oct 2024 16:37:23 -0700 Subject: [PATCH] fix lockscreen passcode --- .../wallet/data/passcode/LockScreen.kt | 62 +++++++++++++++++ .../wallet/data/passcode/PasscodeManager.kt | 23 +++++++ .../tonapps/tonkeeper/extensions/Context.kt | 2 +- .../com/tonapps/tonkeeper/koin/KoinModule.kt | 2 +- .../ui/component/MainSwipeRefreshLayout.kt | 2 +- .../ui/screen/events/EventsViewModel.kt | 3 +- .../tonkeeper/ui/screen/root/RootActivity.kt | 69 +++++++++---------- .../tonkeeper/ui/screen/root/RootViewModel.kt | 42 ----------- .../ui/screen/wallet/main/WalletViewModel.kt | 30 ++++++++ 9 files changed, 153 insertions(+), 82 deletions(-) create mode 100644 apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt new file mode 100644 index 000000000..98f6390bc --- /dev/null +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/LockScreen.kt @@ -0,0 +1,62 @@ +package com.tonapps.wallet.data.passcode + +import android.content.Context +import androidx.biometric.BiometricPrompt +import com.tonapps.wallet.data.settings.SettingsRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.withContext + +class LockScreen( + private val passcodeManager: PasscodeManager, + private val settingsRepository: SettingsRepository +) { + + sealed class State { + data object None: State() + data object Input: State() + data object Biometric: State() + data object Error: State() + } + + private val _stateFlow = MutableStateFlow(null) + val stateFlow = _stateFlow.asStateFlow().filterNotNull() + + fun init() { + if (!settingsRepository.lockScreen) { + hide() + } else if (settingsRepository.biometric) { + _stateFlow.value = State.Biometric + } else { + _stateFlow.value = State.Input + } + } + + private fun hide() { + _stateFlow.value = State.None + } + + private fun error() { + _stateFlow.value = State.Error + } + + suspend fun check(context: Context, code: String) = withContext(Dispatchers.IO) { + val valid = passcodeManager.isValid(context, code) + if (valid) { + hide() + } else { + error() + } + } + + fun biometric(result: BiometricPrompt.AuthenticationResult) { + hide() + } + + fun reset() { + + } + +} \ No newline at end of file diff --git a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt index 409d6067d..b783d101c 100644 --- a/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt +++ b/apps/wallet/data/passcode/src/main/java/com/tonapps/wallet/data/passcode/PasscodeManager.kt @@ -1,6 +1,7 @@ package com.tonapps.wallet.data.passcode import android.content.Context +import androidx.biometric.BiometricPrompt import com.tonapps.extensions.logError import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.passcode.dialog.PasscodeDialog @@ -8,10 +9,12 @@ import com.tonapps.wallet.data.rn.RNLegacy import com.tonapps.wallet.data.settings.SettingsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uikit.navigation.Navigation +import java.util.concurrent.atomic.AtomicBoolean class PasscodeManager( private val accountRepository: AccountRepository, @@ -21,14 +24,34 @@ class PasscodeManager( private val scope: CoroutineScope ) { + private val lockscreen = LockScreen(this, settingsRepository) + + val lockscreenFlow: Flow + get() = lockscreen.stateFlow + init { scope.launch(Dispatchers.IO) { if (rnLegacy.isRequestMigration()) { helper.reset() } + lockscreen.init() } } + fun lockscreenBiometric(result: BiometricPrompt.AuthenticationResult) { + lockscreen.biometric(result) + } + + fun deleteAll() { + scope.launch { + reset() + } + } + + fun lockscreenCheck(context: Context, code: String) { + scope.launch { lockscreen.check(context, code) } + } + suspend fun hasPinCode(): Boolean = withContext(Dispatchers.IO) { if (helper.hasPinCode) { true diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt index 094d995ab..90a88b90e 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/extensions/Context.kt @@ -49,7 +49,7 @@ fun Context.safeExternalOpenUri(uri: Uri) { val intent = Intent(Intent.ACTION_VIEW, uri) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(intent) - } catch (e: Exception) { + } catch (e: Throwable) { debugToast(e) } } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt index 8b336bce7..b381248bd 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/koin/KoinModule.kt @@ -35,9 +35,9 @@ import com.tonapps.wallet.data.settings.SettingsRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.factoryOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val koinModel = module { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainSwipeRefreshLayout.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainSwipeRefreshLayout.kt index 77a20879e..f865366a1 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainSwipeRefreshLayout.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/component/MainSwipeRefreshLayout.kt @@ -32,6 +32,6 @@ class MainSwipeRefreshLayout @JvmOverloads constructor( override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) - setProgressViewOffset(true, topPadding, topPadding + 42.dp) + setProgressViewOffset(true, topPadding, topPadding + 40.dp) } } \ No newline at end of file diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt index 8a92fcb72..4b6b5abca 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/events/EventsViewModel.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.concurrent.atomic.AtomicBoolean @@ -69,7 +70,7 @@ class EventsViewModel( } autoRefreshJob = viewModelScope.launch(Dispatchers.IO) { - while (true) { + while (isActive) { checkAutoRefresh() delay(35.seconds) } diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt index 29cc6e31a..ed10e543c 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootActivity.kt @@ -2,19 +2,15 @@ package com.tonapps.tonkeeper.ui.screen.root import android.content.Intent import android.content.res.Configuration -import android.content.res.Resources import android.net.Uri import android.os.Bundle import android.os.Handler -import android.util.Log import android.view.View -import androidx.appcompat.app.AppCompatDelegate import androidx.biometric.BiometricPrompt import androidx.core.app.ActivityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding -import androidx.lifecycle.lifecycleScope import com.tonapps.extensions.toUriOrNull import com.tonapps.tonkeeper.App import com.tonapps.tonkeeper.extensions.isDarkMode @@ -35,7 +31,9 @@ import com.tonapps.tonkeeperx.R import com.tonapps.wallet.api.entity.TokenEntity import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.core.Theme +import com.tonapps.wallet.data.passcode.LockScreen import com.tonapps.wallet.data.passcode.PasscodeBiometric +import com.tonapps.wallet.data.passcode.PasscodeManager import com.tonapps.wallet.data.passcode.ui.PasscodeView import com.tonapps.wallet.data.rn.RNLegacy import com.tonapps.wallet.data.settings.SettingsRepository @@ -59,6 +57,7 @@ class RootActivity: BaseWalletActivity() { private val legacyRN: RNLegacy by inject() private val settingsRepository by inject() + private val passcodeManager by inject() private lateinit var uiHandler: Handler @@ -84,7 +83,9 @@ class RootActivity: BaseWalletActivity() { lockView = findViewById(R.id.lock) lockPasscodeView = findViewById(R.id.lock_passcode) - lockPasscodeView.doOnCheck = ::checkPasscode + lockPasscodeView.doOnCheck = { + passcodeManager.lockscreenCheck(this, it) + } lockSignOut = findViewById(R.id.lock_sign_out) lockSignOut.setOnClickListener { signOutAll() } @@ -98,11 +99,36 @@ class RootActivity: BaseWalletActivity() { collectFlow(viewModel.hasWalletFlow) { init(it) } collectFlow(viewModel.eventFlow) { event(it) } - collectFlow(viewModel.passcodeFlow, ::passcodeFlow) + collectFlow(passcodeManager.lockscreenFlow, ::pinState) App.applyConfiguration(resources.configuration) } + private fun pinState(state: LockScreen.State) { + if (state == LockScreen.State.None) { + lockView.visibility = View.GONE + lockPasscodeView.setSuccess() + } else if (state == LockScreen.State.Error) { + lockPasscodeView.setError() + } else { + lockView.visibility = View.VISIBLE + if (state is LockScreen.State.Biometric) { + PasscodeBiometric.showPrompt(this, getString(Localization.app_name), object : BiometricPrompt.AuthenticationCallback() { + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + passcodeManager.lockscreenBiometric(result) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + toast(Localization.authorization_required) + } + }) + } + } + } + private fun createOrGetViewModel(): RootViewModel { if (cachedRootViewModel == null) { cachedRootViewModel = viewModel() @@ -148,36 +174,6 @@ class RootActivity: BaseWalletActivity() { } } - private fun passcodeFlow(config: RootViewModel.Passcode) { - if (!config.show) { - lockView.visibility = View.GONE - return - } - lockView.visibility = View.VISIBLE - if (config.biometric) { - PasscodeBiometric.showPrompt(this, getString(Localization.app_name), object : BiometricPrompt.AuthenticationCallback() { - - override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { - super.onAuthenticationSucceeded(result) - lockView.visibility = View.GONE - } - - override fun onAuthenticationFailed() { - super.onAuthenticationFailed() - toast(Localization.authorization_required) - } - }) - } - } - - private fun checkPasscode(code: String) { - viewModel.checkPasscode(this, code).catch { - lockPasscodeView.setError() - }.onEach { - lockPasscodeView.setSuccess() - }.launchIn(lifecycleScope) - } - override fun setContentView(layoutResID: Int) { super.setContentView(R.layout.activity_root) } @@ -233,6 +229,7 @@ class RootActivity: BaseWalletActivity() { builder.setMessage(Localization.sign_out_all_description) builder.setNegativeButton(Localization.sign_out) { viewModel.signOut() + passcodeManager.deleteAll() setIntroFragment() } builder.setPositiveButton(Localization.cancel) diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt index b08ac1983..871b29370 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/root/RootViewModel.kt @@ -1,10 +1,8 @@ package com.tonapps.tonkeeper.ui.screen.root import android.app.Application -import android.content.Context import android.net.Uri import android.os.Bundle -import android.util.Log import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.net.toUri @@ -62,24 +60,18 @@ import com.tonapps.wallet.data.account.entities.WalletEntity import com.tonapps.wallet.data.account.AccountRepository import com.tonapps.wallet.data.browser.BrowserRepository import com.tonapps.wallet.data.core.ScreenCacheSource -import com.tonapps.wallet.data.core.Theme import com.tonapps.wallet.data.core.entity.SignRequestEntity import com.tonapps.wallet.data.dapps.entities.AppConnectEntity -import com.tonapps.wallet.data.passcode.PasscodeManager import com.tonapps.wallet.data.purchase.PurchaseRepository import com.tonapps.wallet.data.settings.SettingsRepository import com.tonapps.wallet.localization.Localization import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -90,7 +82,6 @@ import kotlinx.coroutines.withContext class RootViewModel( app: Application, - private val passcodeManager: PasscodeManager, private val settingsRepository: SettingsRepository, private val accountRepository: AccountRepository, private val api: API, @@ -103,11 +94,6 @@ class RootViewModel( savedStateHandle: SavedStateHandle, ): BaseWalletVM(app) { - data class Passcode( - val show: Boolean, - val biometric: Boolean, - ) - private val savedState = RootModelState(savedStateHandle) private val selectedWalletFlow: Flow = accountRepository.selectedWalletFlow @@ -118,9 +104,6 @@ class RootViewModel( private val _eventFlow = MutableEffectFlow() val eventFlow = _eventFlow.asSharedFlow().filterNotNull() - private val _passcodeFlow = MutableStateFlow(null) - val passcodeFlow = _passcodeFlow.asStateFlow().filterNotNull() - init { tonConnectManager.transactionRequestFlow.collectFlow(::sendTransaction) @@ -128,15 +111,6 @@ class RootViewModel( context.setLocales(settingsRepository.localeList) } - combine( - settingsRepository.biometricFlow.take(1), - settingsRepository.lockscreenFlow.take(1) - ) { biometric, lockscreen -> - Passcode(lockscreen, biometric) - }.collectFlow { - _passcodeFlow.value = it - } - combine( accountRepository.selectedStateFlow.filter { it !is AccountRepository.SelectedState.Initialization }, api.configFlow, @@ -270,29 +244,13 @@ class RootViewModel( fun signOut() { viewModelScope.launch { accountRepository.logout() - passcodeManager.reset() - hidePasscode() } } - private fun hidePasscode() { - _passcodeFlow.value = Passcode(show = false, biometric = false) - } - fun connectLedger(connectData: LedgerConnectData, accounts: List) { _eventFlow.tryEmit(RootEvent.Ledger(connectData, accounts)) } - fun checkPasscode(context: Context, code: String): Flow = flow { - val valid = passcodeManager.isValid(context, code) - if (valid) { - hidePasscode() - emit(Unit) - } else { - throw Exception("invalid passcode") - } - }.take(1) - fun processIntentExtras(bundle: Bundle) { val pushType = bundle.getString("type") ?: return hasWalletFlow.take(1).collectFlow { diff --git a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/wallet/main/WalletViewModel.kt b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/wallet/main/WalletViewModel.kt index adaf0d779..e9cbb8cc7 100644 --- a/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/wallet/main/WalletViewModel.kt +++ b/apps/wallet/instance/app/src/main/java/com/tonapps/tonkeeper/ui/screen/wallet/main/WalletViewModel.kt @@ -25,6 +25,7 @@ import com.tonapps.wallet.data.core.WalletCurrency import com.tonapps.wallet.data.rates.RatesRepository import com.tonapps.wallet.data.settings.SettingsRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -33,9 +34,12 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import uikit.extensions.collectFlow +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds class WalletViewModel( app: Application, @@ -53,6 +57,7 @@ class WalletViewModel( private val assetsManager: AssetsManager, ): BaseWalletVM(app) { + private var autoRefreshJob: Job? = null private val alertNotificationsFlow = MutableStateFlow>(emptyList()) private val _uiLabelFlow = MutableStateFlow(null) @@ -227,6 +232,13 @@ class WalletViewModel( }.launchIn(viewModelScope) loadAlertNotifications() + + autoRefreshJob = viewModelScope.launch(Dispatchers.IO) { + while (isActive) { + checkAutoRefresh() + delay(2.minutes) + } + } } fun refresh() { @@ -234,6 +246,18 @@ class WalletViewModel( _lastLtFlow.value += 1 } + private suspend fun checkAutoRefresh() { + if (hasPendingTransaction()) { + withContext(Dispatchers.Main) { + refresh() + } + } + } + + private fun hasPendingTransaction(): Boolean { + return _statusFlow.value == Status.SendingTransaction + } + private fun loadAlertNotifications() { viewModelScope.launch(Dispatchers.IO) { alertNotificationsFlow.value = api.getAlertNotifications() @@ -294,6 +318,12 @@ class WalletViewModel( screenCacheSource.set(CACHE_NAME, wallet.id, items) } + override fun onCleared() { + super.onCleared() + autoRefreshJob?.cancel() + autoRefreshJob = null + } + companion object { private const val CACHE_NAME = "wallet"