From de799c3a1286145acc8f93b1a1521f8c6b978d6e Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Sun, 14 Nov 2021 23:19:25 +0200 Subject: [PATCH 01/14] Reformat code and minor refactor in PR #125 --- .../java/com/ivy/wallet/ui/IvyActivity.kt | 59 ++++++++++++------- .../main/java/com/ivy/wallet/ui/IvyContext.kt | 7 +-- 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt index f0a0201f60..1230b76006 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt @@ -202,10 +202,11 @@ class IvyActivity : AppCompatActivity() { AddTransactionWidget.updateBroadcast(this) setContent { - val viewModel : IvyViewModel = viewModel() + val viewModel: IvyViewModel = viewModel() val isSystemInDarkTheme = isSystemInDarkTheme() val appLocked by viewModel.appLocked.observeAsState(false) + appLockedEnabled = appLocked val isUserInactive = ivyContext.isUserInactive LaunchedEffect(isSystemInDarkTheme) { @@ -217,28 +218,42 @@ class IvyActivity : AppCompatActivity() { ivyContext = ivyContext, ) { if (appLocked) { - appLockedEnabled = true ivyContext.navigateTo( - Screen.AppLock({ - authenticateWithOSBiometricsModal( - viewModel.handleBiometricAuthenticationResult(onAuthSuccess = { - viewModel.unlockAuthenticated(intent) - }) - ) - }, { viewModel.unlockAuthenticated(intent) }), false + Screen.AppLock( + onShowOSBiometricsModal = { + authenticateWithOSBiometricsModal( + viewModel.handleBiometricAuthenticationResult( + onAuthSuccess = { + viewModel.unlockAuthenticated(intent) + } + ) + ) + }, + onContinueWithoutAuthentication = { + viewModel.unlockAuthenticated(intent) + } + ), + allowBackStackStore = false ) } if (appLockedEnabled && isUserInactive.value) { ivyContext.resetUserInActiveTimer() ivyContext.navigateTo( - Screen.AppLock({ - authenticateWithOSBiometricsModal( - viewModel.handleBiometricAuthenticationResult(onAuthSuccess = { - ivyContext.back() - }) - ) - }, { ivyContext.back() }) + Screen.AppLock( + onShowOSBiometricsModal = { + authenticateWithOSBiometricsModal( + viewModel.handleBiometricAuthenticationResult( + onAuthSuccess = { + ivyContext.back() + } + ) + ) + }, + onContinueWithoutAuthentication = { + ivyContext.back() + } + ) ) } @@ -272,7 +287,8 @@ class IvyActivity : AppCompatActivity() { }, onContinueWithoutAuthentication = { screen.onContinueWithoutAuthentication() - }) + } + ) } null -> { } @@ -294,14 +310,16 @@ class IvyActivity : AppCompatActivity() { override fun onResume() { super.onResume() - if (appLockedEnabled) + if (appLockedEnabled) { ivyContext.checkUserInactiveTimeStatus() + } } override fun onPause() { super.onPause() - if (appLockedEnabled) + if (appLockedEnabled) { ivyContext.startUserInactiveTimeCounter() + } } @Composable @@ -373,8 +391,7 @@ class IvyActivity : AppCompatActivity() { Spacer(Modifier.height(24.dp)) //To automatically launch the biometric screen on load of this composable - LaunchedEffect(true) - { + LaunchedEffect(true) { if (hasLockScreen(context)) { onShowOSBiometricsModal() } else { diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt index 0c70423a5d..fc6589bb40 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt @@ -223,7 +223,7 @@ class IvyContext { // UserInactivity ------------------------------------------------------------------------------ private val _isUserInactive = mutableStateOf(false) - val isUserInactive : State = _isUserInactive + val isUserInactive: State = _isUserInactive private val userInactiveTime = AtomicLong(0) private var userInactiveJob: Job? = null @@ -234,11 +234,10 @@ class IvyContext { } fun startUserInactiveTimeCounter() { - if (userInactiveJob != null && userInactiveJob!!.isActive) - return + if (userInactiveJob != null && userInactiveJob!!.isActive) return userInactiveJob = GlobalScope.launch(Dispatchers.IO) { - while (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT && userInactiveJob != null && !userInactiveJob?.isCancelled!!) { + while (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT && userInactiveJob != null && !userInactiveJob?.isCancelled!!) { delay(1000) userInactiveTime.incrementAndGet() } From ec3347c65ba589d1fec42e003394b1c1988d6693 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 10:56:04 +0200 Subject: [PATCH 02/14] Extract AppLockedScreen.kt to another file + minor refactor (& fix bug caused by the previous refactor) --- .../java/com/ivy/wallet/ui/IvyActivity.kt | 142 ++---------------- .../java/com/ivy/wallet/ui/IvyViewModel.kt | 8 +- .../wallet/ui/applocked/AppLockedScreen.kt | 116 ++++++++++++++ 3 files changed, 136 insertions(+), 130 deletions(-) create mode 100644 app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt index 1230b76006..dd8fd82dd0 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt @@ -20,27 +20,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricPrompt import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* -import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel -import com.google.accompanist.insets.systemBarsPadding import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -50,11 +36,11 @@ import com.google.android.gms.tasks.Task import com.google.android.play.core.review.ReviewManagerFactory import com.ivy.wallet.BuildConfig import com.ivy.wallet.Constants -import com.ivy.wallet.R import com.ivy.wallet.base.* import com.ivy.wallet.logic.CustomerJourneyLogic import com.ivy.wallet.model.TransactionType import com.ivy.wallet.ui.analytics.AnalyticsReport +import com.ivy.wallet.ui.applocked.AppLockedScreen import com.ivy.wallet.ui.balance.BalanceScreen import com.ivy.wallet.ui.bankintegrations.ConnectBankScreen import com.ivy.wallet.ui.budget.BudgetScreen @@ -72,7 +58,6 @@ import com.ivy.wallet.ui.statistic.level1.PieChartStatisticScreen import com.ivy.wallet.ui.statistic.level2.ItemStatisticScreen import com.ivy.wallet.ui.test.TestScreen import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.IvyButton import com.ivy.wallet.ui.webView.WebViewScreen import com.ivy.wallet.widget.AddTransactionWidget import dagger.hilt.android.AndroidEntryPoint @@ -205,9 +190,8 @@ class IvyActivity : AppCompatActivity() { val viewModel: IvyViewModel = viewModel() val isSystemInDarkTheme = isSystemInDarkTheme() - val appLocked by viewModel.appLocked.observeAsState(false) - appLockedEnabled = appLocked - val isUserInactive = ivyContext.isUserInactive + val appLockedEnabled by viewModel.appLockedEnabled.observeAsState(false) + val isUserInactive by ivyContext.isUserInactive LaunchedEffect(isSystemInDarkTheme) { viewModel.start(isSystemInDarkTheme, intent) @@ -217,7 +201,12 @@ class IvyActivity : AppCompatActivity() { IvyApp( ivyContext = ivyContext, ) { - if (appLocked) { + if (appLockedEnabled) { + //update this.appLockedEnabled here + // because only dependant Compose code on appLockedEnabled state will be updated + //when appLockedEnabled state is changed + this@IvyActivity.appLockedEnabled = true + ivyContext.navigateTo( Screen.AppLock( onShowOSBiometricsModal = { @@ -237,7 +226,7 @@ class IvyActivity : AppCompatActivity() { ) } - if (appLockedEnabled && isUserInactive.value) { + if (appLockedEnabled && isUserInactive) { ivyContext.resetUserInActiveTimer() ivyContext.navigateTo( Screen.AppLock( @@ -245,6 +234,7 @@ class IvyActivity : AppCompatActivity() { authenticateWithOSBiometricsModal( viewModel.handleBiometricAuthenticationResult( onAuthSuccess = { + //go back to previous screen ivyContext.back() } ) @@ -280,16 +270,7 @@ class IvyActivity : AppCompatActivity() { is Screen.Report -> ReportScreen(screen = screen) is Screen.Budget -> BudgetScreen(screen = screen) is Screen.WebView -> WebViewScreen(screen = screen) - is Screen.AppLock -> { - AppLockedScreen( - onShowOSBiometricsModal = { - screen.onShowOSBiometricsModal() - }, - onContinueWithoutAuthentication = { - screen.onContinueWithoutAuthentication() - } - ) - } + is Screen.AppLock -> AppLockedScreen(screen = screen) null -> { } } @@ -304,101 +285,21 @@ class IvyActivity : AppCompatActivity() { WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE ) - } else + } else { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } } override fun onResume() { super.onResume() - if (appLockedEnabled) { + if (appLockedEnabled) ivyContext.checkUserInactiveTimeStatus() - } } override fun onPause() { super.onPause() - if (appLockedEnabled) { + if (appLockedEnabled) ivyContext.startUserInactiveTimeCounter() - } - } - - @Composable - private fun AppLockedScreen( - onShowOSBiometricsModal: () -> Unit, - onContinueWithoutAuthentication: () -> Unit, - ) { - Column( - modifier = Modifier - .fillMaxSize() - .systemBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(Modifier.height(32.dp)) - - Text( - modifier = Modifier - .background(IvyTheme.colors.medium, Shapes.roundedFull) - .padding(vertical = 12.dp) - .padding(horizontal = 32.dp), - text = "APP LOCKED", - style = Typo.body2.style( - fontWeight = FontWeight.ExtraBold, - ) - ) - - Spacer(Modifier.weight(1f)) - - Image( - modifier = Modifier - .size(width = 96.dp, height = 138.dp), - painter = painterResource(id = R.drawable.ic_fingerprint), - colorFilter = ColorFilter.tint(IvyTheme.colors.medium), - contentScale = ContentScale.FillBounds, - contentDescription = "unlock icon" - ) - - Spacer(Modifier.weight(1f)) - - Text( - text = "Authenticate to enter the app", - style = Typo.body2.style( - fontWeight = FontWeight.SemiBold, - color = Gray - ) - ) - - Spacer(Modifier.height(24.dp)) - - val context = LocalContext.current - IvyButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - text = "Unlock", - textStyle = Typo.body2.style( - color = White, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center - ), - wrapContentMode = false - ) { - if (hasLockScreen(context)) { - onShowOSBiometricsModal() - } else { - onContinueWithoutAuthentication() - } - } - Spacer(Modifier.height(24.dp)) - - //To automatically launch the biometric screen on load of this composable - LaunchedEffect(true) { - if (hasLockScreen(context)) { - onShowOSBiometricsModal() - } else { - onContinueWithoutAuthentication() - } - } - } } private fun authenticateWithOSBiometricsModal( @@ -619,15 +520,4 @@ class IvyActivity : AppCompatActivity() { val addTransactionWidget = ComponentName(this, AddTransactionWidget::class.java) appWidgetManager.requestPinAppWidget(addTransactionWidget, null, null) } - - @Preview - @Composable - private fun Preview_Locked() { - IvyAppPreview { - AppLockedScreen( - onContinueWithoutAuthentication = {}, - onShowOSBiometricsModal = {} - ) - } - } } diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt index a7799a3371..40b13eaf57 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt @@ -40,8 +40,8 @@ class IvyViewModel @Inject constructor( const val EXTRA_ADD_TRANSACTION_TYPE = "add_transaction_type_extra" } - private val _appLocked = MutableLiveData() - val appLocked = _appLocked.asLiveData() + private val _appLockedEnabled = MutableLiveData() + val appLockedEnabled = _appLockedEnabled.asLiveData() fun start(systemDarkMode: Boolean, intent: Intent) { viewModelScope.launch { @@ -62,7 +62,7 @@ class IvyViewModel @Inject constructor( if (onboardingCompleted()) { val appLocked = sharedPrefs.getBoolean(SharedPrefs.LOCK_APP, false) uiThread { - _appLocked.value = appLocked + _appLockedEnabled.value = appLocked } if (!appLocked) { @@ -110,7 +110,7 @@ class IvyViewModel @Inject constructor( } fun unlockAuthenticated(intent: Intent) { - _appLocked.value = false + _appLockedEnabled.value = false continueNavigation(intent) } diff --git a/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt new file mode 100644 index 0000000000..4b48d22690 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt @@ -0,0 +1,116 @@ +package com.ivy.wallet.ui.applocked + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.google.accompanist.insets.systemBarsPadding +import com.ivy.wallet.R +import com.ivy.wallet.base.hasLockScreen +import com.ivy.wallet.ui.IvyAppPreview +import com.ivy.wallet.ui.Screen +import com.ivy.wallet.ui.theme.* +import com.ivy.wallet.ui.theme.components.IvyButton + +@Composable +fun AppLockedScreen( + screen: Screen.AppLock +) { + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(32.dp)) + + Text( + modifier = Modifier + .background(IvyTheme.colors.medium, Shapes.roundedFull) + .padding(vertical = 12.dp) + .padding(horizontal = 32.dp), + text = "APP LOCKED", + style = Typo.body2.style( + fontWeight = FontWeight.ExtraBold, + ) + ) + + Spacer(Modifier.weight(1f)) + + Image( + modifier = Modifier + .size(width = 96.dp, height = 138.dp), + painter = painterResource(id = R.drawable.ic_fingerprint), + colorFilter = ColorFilter.tint(IvyTheme.colors.medium), + contentScale = ContentScale.FillBounds, + contentDescription = "unlock icon" + ) + + Spacer(Modifier.weight(1f)) + + Text( + text = "Authenticate to enter the app", + style = Typo.body2.style( + fontWeight = FontWeight.SemiBold, + color = Gray + ) + ) + + Spacer(Modifier.height(24.dp)) + + val context = LocalContext.current + IvyButton( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = "Unlock", + textStyle = Typo.body2.style( + color = White, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center + ), + wrapContentMode = false + ) { + if (hasLockScreen(context)) { + screen.onShowOSBiometricsModal() + } else { + screen.onContinueWithoutAuthentication() + } + } + Spacer(Modifier.height(24.dp)) + + //To automatically launch the biometric screen on load of this composable + LaunchedEffect(true) { + if (hasLockScreen(context)) { + screen.onShowOSBiometricsModal() + } else { + screen.onContinueWithoutAuthentication() + } + } + } +} + +@Preview +@Composable +private fun Preview_Locked() { + IvyAppPreview { + AppLockedScreen( + Screen.AppLock( + onContinueWithoutAuthentication = {}, + onShowOSBiometricsModal = {} + ) + ) + } +} \ No newline at end of file From 6140b9577b3517aeeff1d74683ed842503215b06 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 12:39:46 +0200 Subject: [PATCH 03/14] Implement proper "App Lock" mechanism (re-work the existing one) --- app/src/main/java/com/ivy/wallet/Constants.kt | 2 +- .../main/java/com/ivy/wallet/base/MVVMExt.kt | 6 + .../com/ivy/wallet/persistence/SharedPrefs.kt | 2 +- .../java/com/ivy/wallet/ui/IvyActivity.kt | 124 +++++++----------- .../main/java/com/ivy/wallet/ui/IvyContext.kt | 39 ------ .../java/com/ivy/wallet/ui/IvyViewModel.kt | 108 +++++++++++---- .../main/java/com/ivy/wallet/ui/Screens.kt | 5 - .../wallet/ui/applocked/AppLockedScreen.kt | 45 ++++--- .../wallet/ui/settings/SettingsViewModel.kt | 4 +- 9 files changed, 169 insertions(+), 166 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/Constants.kt b/app/src/main/java/com/ivy/wallet/Constants.kt index 1411cffef3..306913c825 100644 --- a/app/src/main/java/com/ivy/wallet/Constants.kt +++ b/app/src/main/java/com/ivy/wallet/Constants.kt @@ -27,5 +27,5 @@ object Constants { const val URL_IVY_CONTRIBUTORS = "https://github.com/ILIYANGERMANOV/ivy-wallet#contributors-see-graph" - const val USER_INACTIVE_TIME_LIMIT = 300 //Time in seconds + const val USER_INACTIVE_TIME_LIMIT = 1 //Time in seconds } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/base/MVVMExt.kt b/app/src/main/java/com/ivy/wallet/base/MVVMExt.kt index b46b2d04d3..27ba77b66b 100644 --- a/app/src/main/java/com/ivy/wallet/base/MVVMExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/MVVMExt.kt @@ -5,12 +5,18 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext fun MutableLiveData.asLiveData(): LiveData { return this } +fun MutableStateFlow.asFlow(): StateFlow { + return this +} + fun Fragment.args(putArgs: Bundle.() -> Unit): Fragment { arguments = Bundle().apply { putArgs() } return this diff --git a/app/src/main/java/com/ivy/wallet/persistence/SharedPrefs.kt b/app/src/main/java/com/ivy/wallet/persistence/SharedPrefs.kt index fbee48c5cd..15b3c99d2f 100644 --- a/app/src/main/java/com/ivy/wallet/persistence/SharedPrefs.kt +++ b/app/src/main/java/com/ivy/wallet/persistence/SharedPrefs.kt @@ -43,7 +43,7 @@ class SharedPrefs(appContext: Context) { //-------------------------------- Bank Integrations temp ---------------------------------- //----------------------------- App Settings ----------------------------------------------- - const val LOCK_APP = "lock_app" + const val APP_LOCK_ENABLED = "lock_app" const val START_DATE_OF_MONTH = "start_date_of_month" //----------------------------- App Settings ----------------------------------------------- diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt index dd8fd82dd0..0cfb3a8049 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt @@ -16,6 +16,7 @@ import android.widget.Toast import androidx.activity.compose.setContent import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricPrompt import androidx.compose.animation.ExperimentalAnimationApi @@ -23,7 +24,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.runtime.* -import androidx.compose.runtime.livedata.observeAsState import androidx.core.content.ContextCompat import androidx.core.view.WindowCompat import androidx.lifecycle.viewmodel.compose.viewModel @@ -96,7 +96,9 @@ class IvyActivity : AppCompatActivity() { private lateinit var openFileContract: ActivityResultLauncher private lateinit var onFileOpened: (fileUri: Uri) -> Unit - private var appLockedEnabled: Boolean = false + + private val viewModel: IvyViewModel by viewModels() + @ExperimentalAnimationApi @ExperimentalFoundationApi @@ -190,9 +192,6 @@ class IvyActivity : AppCompatActivity() { val viewModel: IvyViewModel = viewModel() val isSystemInDarkTheme = isSystemInDarkTheme() - val appLockedEnabled by viewModel.appLockedEnabled.observeAsState(false) - val isUserInactive by ivyContext.isUserInactive - LaunchedEffect(isSystemInDarkTheme) { viewModel.start(isSystemInDarkTheme, intent) viewModel.initBilling(this@IvyActivity) @@ -201,77 +200,50 @@ class IvyActivity : AppCompatActivity() { IvyApp( ivyContext = ivyContext, ) { - if (appLockedEnabled) { - //update this.appLockedEnabled here - // because only dependant Compose code on appLockedEnabled state will be updated - //when appLockedEnabled state is changed - this@IvyActivity.appLockedEnabled = true - - ivyContext.navigateTo( - Screen.AppLock( - onShowOSBiometricsModal = { - authenticateWithOSBiometricsModal( - viewModel.handleBiometricAuthenticationResult( - onAuthSuccess = { - viewModel.unlockAuthenticated(intent) - } - ) - ) - }, - onContinueWithoutAuthentication = { - viewModel.unlockAuthenticated(intent) - } - ), - allowBackStackStore = false - ) - } + val appLocked by viewModel.appLocked.collectAsState() - if (appLockedEnabled && isUserInactive) { - ivyContext.resetUserInActiveTimer() - ivyContext.navigateTo( - Screen.AppLock( + when (appLocked) { + null -> { + //display nothing + } + true -> { + AppLockedScreen( onShowOSBiometricsModal = { authenticateWithOSBiometricsModal( - viewModel.handleBiometricAuthenticationResult( - onAuthSuccess = { - //go back to previous screen - ivyContext.back() - } - ) + biometricPromptCallback = viewModel.handleBiometricAuthResult() ) }, onContinueWithoutAuthentication = { - ivyContext.back() + viewModel.unlockApp() } ) - ) - } - - when (val screen = ivyContext.currentScreen) { - - is Screen.Main -> MainScreen(screen = screen) - is Screen.Onboarding -> OnboardingScreen(screen = screen) - is Screen.EditTransaction -> EditTransactionScreen(screen = screen) - is Screen.ItemStatistic -> ItemStatisticScreen(screen = screen) - is Screen.PieChartStatistic -> PieChartStatisticScreen(screen = screen) - is Screen.Categories -> CategoriesScreen(screen = screen) - is Screen.Settings -> SettingsScreen(screen = screen) - is Screen.PlannedPayments -> PlannedPaymentsScreen(screen = screen) - is Screen.EditPlanned -> EditPlannedScreen(screen = screen) - is Screen.BalanceScreen -> BalanceScreen(screen = screen) - is Screen.Paywall -> PaywallScreen( - screen = screen, - activity = this@IvyActivity - ) - is Screen.Test -> TestScreen(screen = screen) - is Screen.AnalyticsReport -> AnalyticsReport(screen = screen) - is Screen.Import -> ImportCSVScreen(screen = screen) - is Screen.ConnectBank -> ConnectBankScreen(screen = screen) - is Screen.Report -> ReportScreen(screen = screen) - is Screen.Budget -> BudgetScreen(screen = screen) - is Screen.WebView -> WebViewScreen(screen = screen) - is Screen.AppLock -> AppLockedScreen(screen = screen) - null -> { + } + false -> { + when (val screen = ivyContext.currentScreen) { + is Screen.Main -> MainScreen(screen = screen) + is Screen.Onboarding -> OnboardingScreen(screen = screen) + is Screen.EditTransaction -> EditTransactionScreen(screen = screen) + is Screen.ItemStatistic -> ItemStatisticScreen(screen = screen) + is Screen.PieChartStatistic -> PieChartStatisticScreen(screen = screen) + is Screen.Categories -> CategoriesScreen(screen = screen) + is Screen.Settings -> SettingsScreen(screen = screen) + is Screen.PlannedPayments -> PlannedPaymentsScreen(screen = screen) + is Screen.EditPlanned -> EditPlannedScreen(screen = screen) + is Screen.BalanceScreen -> BalanceScreen(screen = screen) + is Screen.Paywall -> PaywallScreen( + screen = screen, + activity = this@IvyActivity + ) + is Screen.Test -> TestScreen(screen = screen) + is Screen.AnalyticsReport -> AnalyticsReport(screen = screen) + is Screen.Import -> ImportCSVScreen(screen = screen) + is Screen.ConnectBank -> ConnectBankScreen(screen = screen) + is Screen.Report -> ReportScreen(screen = screen) + is Screen.Budget -> BudgetScreen(screen = screen) + is Screen.WebView -> WebViewScreen(screen = screen) + null -> { + } + } } } } @@ -280,7 +252,7 @@ class IvyActivity : AppCompatActivity() { override fun onWindowFocusChanged(hasFocus: Boolean) { super.onWindowFocusChanged(hasFocus) - if (appLockedEnabled && !hasFocus) { + if (viewModel.isAppLockEnabled() && !hasFocus) { window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE @@ -292,14 +264,14 @@ class IvyActivity : AppCompatActivity() { override fun onResume() { super.onResume() - if (appLockedEnabled) - ivyContext.checkUserInactiveTimeStatus() + if (viewModel.isAppLockEnabled()) + viewModel.checkUserInactiveTimeStatus() } override fun onPause() { super.onPause() - if (appLockedEnabled) - ivyContext.startUserInactiveTimeCounter() + if (viewModel.isAppLockEnabled()) + viewModel.startUserInactiveTimeCounter() } private fun authenticateWithOSBiometricsModal( @@ -326,8 +298,12 @@ class IvyActivity : AppCompatActivity() { } override fun onBackPressed() { - if (!ivyContext.onBackPressed()) { + if (viewModel.isAppLocked()) { super.onBackPressed() + } else { + if (!ivyContext.onBackPressed()) { + super.onBackPressed() + } } } diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt index fc6589bb40..1d8c6597df 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt @@ -2,23 +2,19 @@ package com.ivy.wallet.ui import android.net.Uri import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.ivy.wallet.BuildConfig import com.ivy.wallet.Constants -import com.ivy.wallet.Constants.USER_INACTIVE_TIME_LIMIT import com.ivy.wallet.persistence.SharedPrefs import com.ivy.wallet.ui.main.MainTab import com.ivy.wallet.ui.onboarding.model.TimePeriod import com.ivy.wallet.ui.paywall.PaywallReason import com.ivy.wallet.ui.theme.Theme -import kotlinx.coroutines.* import java.time.LocalDate import java.time.LocalTime import java.util.* -import java.util.concurrent.atomic.AtomicLong class IvyContext { var currentScreen: Screen? by mutableStateOf(null) @@ -220,39 +216,4 @@ class IvyContext { fun switchTheme(theme: Theme) { this.theme = theme } - - // UserInactivity ------------------------------------------------------------------------------ - private val _isUserInactive = mutableStateOf(false) - val isUserInactive: State = _isUserInactive - - private val userInactiveTime = AtomicLong(0) - private var userInactiveJob: Job? = null - - fun resetUserInActiveTimer() { - _isUserInactive.value = (false) - userInactiveTime.set(0) - } - - fun startUserInactiveTimeCounter() { - if (userInactiveJob != null && userInactiveJob!!.isActive) return - - userInactiveJob = GlobalScope.launch(Dispatchers.IO) { - while (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT && userInactiveJob != null && !userInactiveJob?.isCancelled!!) { - delay(1000) - userInactiveTime.incrementAndGet() - } - if (!isUserInactive.value) - _isUserInactive.value = (true) - cancel() - } - } - - fun checkUserInactiveTimeStatus() { - if (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT) { - if (userInactiveJob != null && !userInactiveJob?.isCancelled!!) { - userInactiveJob?.cancel() - resetUserInActiveTimer() - } - } - } } diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt index 40b13eaf57..2b0f8913b0 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt @@ -3,11 +3,11 @@ package com.ivy.wallet.ui import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricPrompt -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.ivy.wallet.Constants import com.ivy.wallet.analytics.IvyAnalytics -import com.ivy.wallet.base.asLiveData +import com.ivy.wallet.base.asFlow import com.ivy.wallet.base.ioThread import com.ivy.wallet.base.sendToCrashlytics import com.ivy.wallet.base.uiThread @@ -20,8 +20,10 @@ import com.ivy.wallet.persistence.dao.SettingsDao import com.ivy.wallet.session.IvySession import com.ivy.wallet.ui.theme.Theme import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow import timber.log.Timber +import java.util.concurrent.atomic.AtomicLong import javax.inject.Inject @HiltViewModel @@ -40,8 +42,11 @@ class IvyViewModel @Inject constructor( const val EXTRA_ADD_TRANSACTION_TYPE = "add_transaction_type_extra" } - private val _appLockedEnabled = MutableLiveData() - val appLockedEnabled = _appLockedEnabled.asLiveData() + private var appLockEnabled = false + + private val _appLocked = MutableStateFlow(null) + val appLocked = _appLocked.asFlow() + fun start(systemDarkMode: Boolean, intent: Intent) { viewModelScope.launch { @@ -59,14 +64,12 @@ class IvyViewModel @Inject constructor( ivySession.loadFromCache() ivyAnalytics.loadSession() - if (onboardingCompleted()) { - val appLocked = sharedPrefs.getBoolean(SharedPrefs.LOCK_APP, false) + if (isOnboardingCompleted()) { + appLockEnabled = sharedPrefs.getBoolean(SharedPrefs.APP_LOCK_ENABLED, false) uiThread { - _appLockedEnabled.value = appLocked - } - - if (!appLocked) { - continueNavigation(intent) + //initial app locked state + _appLocked.value = appLockEnabled + navigateOnboardedUser(intent) } } else { ivyContext.navigateTo(Screen.Onboarding) @@ -75,6 +78,12 @@ class IvyViewModel @Inject constructor( } } + private fun navigateOnboardedUser(intent: Intent) { + if (!handleSpecialStart(intent)) { + ivyContext.navigateTo(Screen.Main) + transactionReminderLogic.scheduleReminder() + } + } private fun handleSpecialStart(intent: Intent): Boolean { val addTrnType = intent.getSerializableExtra(EXTRA_ADD_TRANSACTION_TYPE) as? TransactionType @@ -92,10 +101,13 @@ class IvyViewModel @Inject constructor( return false } - fun handleBiometricAuthenticationResult(onAuthSuccess: () -> Unit): BiometricPrompt.AuthenticationCallback { + fun handleBiometricAuthResult( + onAuthSuccess: () -> Unit = {} + ): BiometricPrompt.AuthenticationCallback { return object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { Timber.d("Authentication succeeded!") + unlockApp() onAuthSuccess() } @@ -109,18 +121,6 @@ class IvyViewModel @Inject constructor( } } - fun unlockAuthenticated(intent: Intent) { - _appLockedEnabled.value = false - continueNavigation(intent) - } - - private fun continueNavigation(intent: Intent) { - if (!handleSpecialStart(intent)) { - ivyContext.navigateTo(Screen.Main) - transactionReminderLogic.scheduleReminder() - } - } - fun initBilling(activity: AppCompatActivity) { ivyBilling.init( activity = activity, @@ -143,6 +143,60 @@ class IvyViewModel @Inject constructor( ) } - private fun onboardingCompleted() = - sharedPrefs.getBoolean(SharedPrefs.ONBOARDING_COMPLETED, false) + private fun isOnboardingCompleted(): Boolean { + return sharedPrefs.getBoolean(SharedPrefs.ONBOARDING_COMPLETED, false) + } + + + //App Lock & UserInactivity -------------------------------------------------------------------- + fun isAppLockEnabled(): Boolean { + return appLockEnabled + } + + fun isAppLocked(): Boolean { + //by default we assume that the app is locked + return appLocked.value ?: true + } + + fun lockApp() { + _appLocked.value = true + } + + fun unlockApp() { + _appLocked.value = false + } + + private val userInactiveTime = AtomicLong(0) + private var userInactiveJob: Job? = null + + fun startUserInactiveTimeCounter() { + if (userInactiveJob != null && userInactiveJob!!.isActive) return + + userInactiveJob = viewModelScope.launch(Dispatchers.IO) { + while (userInactiveTime.get() < Constants.USER_INACTIVE_TIME_LIMIT && + userInactiveJob != null && !userInactiveJob?.isCancelled!! + ) { + delay(1000) + userInactiveTime.incrementAndGet() + } + + if (!isAppLocked()) { + lockApp() + } + cancel() + } + } + + fun checkUserInactiveTimeStatus() { + if (userInactiveTime.get() < Constants.USER_INACTIVE_TIME_LIMIT) { + if (userInactiveJob != null && !userInactiveJob?.isCancelled!!) { + userInactiveJob?.cancel() + resetUserInactiveTimer() + } + } + } + + fun resetUserInactiveTimer() { + userInactiveTime.set(0) + } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/Screens.kt b/app/src/main/java/com/ivy/wallet/ui/Screens.kt index 9abcac201b..2afff9cd71 100644 --- a/app/src/main/java/com/ivy/wallet/ui/Screens.kt +++ b/app/src/main/java/com/ivy/wallet/ui/Screens.kt @@ -71,9 +71,4 @@ sealed class Screen { object Test : Screen() data class WebView(val url: String) : Screen() - - data class AppLock( - val onShowOSBiometricsModal: () -> Unit, - val onContinueWithoutAuthentication: () -> Unit - ) : Screen() } diff --git a/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt index 4b48d22690..43cdcbf1a0 100644 --- a/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/applocked/AppLockedScreen.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.ui.applocked +import android.content.Context import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* @@ -20,13 +21,13 @@ import com.google.accompanist.insets.systemBarsPadding import com.ivy.wallet.R import com.ivy.wallet.base.hasLockScreen import com.ivy.wallet.ui.IvyAppPreview -import com.ivy.wallet.ui.Screen import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.IvyButton @Composable -fun AppLockedScreen( - screen: Screen.AppLock +fun BoxWithConstraintsScope.AppLockedScreen( + onShowOSBiometricsModal: () -> Unit, + onContinueWithoutAuthentication: () -> Unit ) { Column( modifier = Modifier @@ -83,34 +84,44 @@ fun AppLockedScreen( ), wrapContentMode = false ) { - if (hasLockScreen(context)) { - screen.onShowOSBiometricsModal() - } else { - screen.onContinueWithoutAuthentication() - } + osAuthentication( + context = context, + onShowOSBiometricsModal = onShowOSBiometricsModal, + onContinueWithoutAuthentication = onContinueWithoutAuthentication + ) } Spacer(Modifier.height(24.dp)) //To automatically launch the biometric screen on load of this composable LaunchedEffect(true) { - if (hasLockScreen(context)) { - screen.onShowOSBiometricsModal() - } else { - screen.onContinueWithoutAuthentication() - } + osAuthentication( + context = context, + onShowOSBiometricsModal = onShowOSBiometricsModal, + onContinueWithoutAuthentication = onContinueWithoutAuthentication + ) } } } +private fun osAuthentication( + context: Context, + onShowOSBiometricsModal: () -> Unit, + onContinueWithoutAuthentication: () -> Unit +) { + if (hasLockScreen(context)) { + onShowOSBiometricsModal() + } else { + onContinueWithoutAuthentication() + } +} + @Preview @Composable private fun Preview_Locked() { IvyAppPreview { AppLockedScreen( - Screen.AppLock( - onContinueWithoutAuthentication = {}, - onShowOSBiometricsModal = {} - ) + onContinueWithoutAuthentication = {}, + onShowOSBiometricsModal = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt index f336d30385..5f9934e8c1 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt @@ -76,7 +76,7 @@ class SettingsViewModel @Inject constructor( } _currencyCode.value = settings.currency - _lockApp.value = sharedPrefs.getBoolean(SharedPrefs.LOCK_APP, false) + _lockApp.value = sharedPrefs.getBoolean(SharedPrefs.APP_LOCK_ENABLED, false) _opSync.value = OpResult.success(ioThread { ivySync.isSynced() }) } @@ -202,7 +202,7 @@ class SettingsViewModel @Inject constructor( fun setLockApp(lockApp: Boolean) { viewModelScope.launch { - sharedPrefs.putBoolean(SharedPrefs.LOCK_APP, lockApp) + sharedPrefs.putBoolean(SharedPrefs.APP_LOCK_ENABLED, lockApp) _lockApp.value = lockApp } } From 3b484f8d309a3fa4774a8eb0d703556ade80a6dd Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 12:46:10 +0200 Subject: [PATCH 04/14] Configure USER_INACTIVE_TIME_LIMIT to 60s for better security --- app/src/main/java/com/ivy/wallet/Constants.kt | 2 +- app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/Constants.kt b/app/src/main/java/com/ivy/wallet/Constants.kt index 306913c825..080ac1ca31 100644 --- a/app/src/main/java/com/ivy/wallet/Constants.kt +++ b/app/src/main/java/com/ivy/wallet/Constants.kt @@ -27,5 +27,5 @@ object Constants { const val URL_IVY_CONTRIBUTORS = "https://github.com/ILIYANGERMANOV/ivy-wallet#contributors-see-graph" - const val USER_INACTIVE_TIME_LIMIT = 1 //Time in seconds + const val USER_INACTIVITY_TIME_LIMIT = 60 //Time in seconds } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt index 2b0f8913b0..b65fc5716d 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyViewModel.kt @@ -173,7 +173,7 @@ class IvyViewModel @Inject constructor( if (userInactiveJob != null && userInactiveJob!!.isActive) return userInactiveJob = viewModelScope.launch(Dispatchers.IO) { - while (userInactiveTime.get() < Constants.USER_INACTIVE_TIME_LIMIT && + while (userInactiveTime.get() < Constants.USER_INACTIVITY_TIME_LIMIT && userInactiveJob != null && !userInactiveJob?.isCancelled!! ) { delay(1000) @@ -183,13 +183,14 @@ class IvyViewModel @Inject constructor( if (!isAppLocked()) { lockApp() } + cancel() } } fun checkUserInactiveTimeStatus() { - if (userInactiveTime.get() < Constants.USER_INACTIVE_TIME_LIMIT) { - if (userInactiveJob != null && !userInactiveJob?.isCancelled!!) { + if (userInactiveTime.get() < Constants.USER_INACTIVITY_TIME_LIMIT) { + if (userInactiveJob != null && userInactiveJob?.isCancelled == false) { userInactiveJob?.cancel() resetUserInactiveTimer() } From 70bffc3cc22f1e67f2ad1f5802770bc1e3e10b38 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 17:27:07 +0200 Subject: [PATCH 05/14] WIP: Localize amount formatting --- .../main/java/com/ivy/wallet/base/UtilExt.kt | 9 +- .../steps/archived/OnboardingBuffer.kt | 104 ------------------ .../wallet/ui/theme/modal/edit/AmountModal.kt | 62 ++++++++--- .../ui/theme/modal/edit/CalculatorModal.kt | 13 ++- 4 files changed, 55 insertions(+), 133 deletions(-) delete mode 100644 app/src/main/java/com/ivy/wallet/ui/onboarding/steps/archived/OnboardingBuffer.kt diff --git a/app/src/main/java/com/ivy/wallet/base/UtilExt.kt b/app/src/main/java/com/ivy/wallet/base/UtilExt.kt index c474d099ff..40e1c019a4 100644 --- a/app/src/main/java/com/ivy/wallet/base/UtilExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/UtilExt.kt @@ -5,7 +5,6 @@ import android.content.Context import android.icu.util.Currency import com.ivy.wallet.model.IvyCurrency import java.text.DecimalFormat -import java.text.DecimalFormatSymbols import java.util.* import kotlin.math.abs import kotlin.math.log10 @@ -183,13 +182,7 @@ fun shouldShortAmount(amount: Double): Boolean { } fun formatInt(number: Int): String { - return DecimalFormat( - "#,###,###,###", - DecimalFormatSymbols().apply { - this.groupingSeparator = ',' - //TODO: Support groupingSeparator localization, example CZK doesn't like "," - } - ).format(number) + return DecimalFormat("#,###,###,###").format(number) } fun hasLockScreen(context: Context): Boolean { diff --git a/app/src/main/java/com/ivy/wallet/ui/onboarding/steps/archived/OnboardingBuffer.kt b/app/src/main/java/com/ivy/wallet/ui/onboarding/steps/archived/OnboardingBuffer.kt deleted file mode 100644 index 659f9864be..0000000000 --- a/app/src/main/java/com/ivy/wallet/ui/onboarding/steps/archived/OnboardingBuffer.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.ivy.wallet.ui.onboarding.steps.archived - -import androidx.compose.foundation.layout.* -import androidx.compose.material.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.google.accompanist.insets.navigationBarsPadding -import com.google.accompanist.insets.statusBarsPadding -import com.ivy.wallet.ui.IvyAppPreview -import com.ivy.wallet.ui.theme.* -import com.ivy.wallet.ui.theme.components.OnboardingButton -import com.ivy.wallet.ui.theme.modal.edit.AmountCurrency -import com.ivy.wallet.ui.theme.modal.edit.AmountInput - -@Composable -fun OnboardingBuffer( - currency: String, - onBufferSet: (Double) -> Unit -) { - Column( - modifier = Modifier - .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - var amount by remember { mutableStateOf("") } - - Spacer(Modifier.weight(1f)) - - Text( - text = "Savings Buffer", - style = Typo.body1.style( - fontWeight = FontWeight.ExtraBold, - color = IvyTheme.colors.pureInverse - ) - ) - - Spacer(Modifier.height(24.dp)) - - Text( - modifier = Modifier.padding(horizontal = 40.dp), - text = "What's your savings goal?", - style = Typo.body2.style( - color = IvyTheme.colors.pureInverse, - fontWeight = FontWeight.Medium - ) - ) - - Spacer(Modifier.weight(1f)) - - AmountCurrency( - amount = amount, - currency = currency - ) - - Spacer(Modifier.height(32.dp)) - - - AmountInput( - currency = currency, - amount = amount - ) { - amount = it - } - - Spacer(Modifier.weight(1f)) - - OnboardingButton( - Modifier - .padding(horizontal = 24.dp) - .fillMaxWidth(), - text = "Set", - textColor = White, - backgroundGradient = GradientIvy, - hasNext = true, - enabled = amount.toDoubleOrNull() != null - ) { - try { - onBufferSet(amount.toDouble()) - } catch (e: Exception) { - e.printStackTrace() - } - } - - Spacer(Modifier.height(24.dp)) - } -} - -@Preview -@Composable -private fun Preview() { - IvyAppPreview { - OnboardingBuffer( - currency = "BGN" - ) { - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt index 618d13f173..16f36bc37e 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt @@ -27,6 +27,7 @@ import com.ivy.wallet.ui.theme.components.IvyIcon import com.ivy.wallet.ui.theme.modal.IvyModal import com.ivy.wallet.ui.theme.modal.ModalPositiveButton import com.ivy.wallet.ui.theme.modal.modalPreviewActionRowHeight +import java.text.DecimalFormatSymbols import java.util.* import kotlin.math.truncate @@ -77,7 +78,7 @@ fun BoxWithConstraintsScope.AmountModal( if (amount.isEmpty()) { onAmountChanged(0.0) } else { - onAmountChanged(amount.replace(",", "").toDouble()) + onAmountChanged(amount.amountToDouble()) } dismiss() } catch (e: Exception) { @@ -169,21 +170,21 @@ fun AmountInput( setAmount(it) firstInput = false } else { - val newlyEnteredNumberString = amount.replace(",", "") + it + val newlyEnteredNumberString = amount + it val decimalPartString = newlyEnteredNumberString - .split(".") + .split(localDecimalSeparator()) .getOrNull(1) val decimalCount = decimalPartString?.length ?: 0 - val amountDouble = newlyEnteredNumberString.toDoubleOrNull() + val amountDouble = newlyEnteredNumberString.amountToDoubleOrNull() val decimalCountOkay = IvyCurrency.fromCode(currency)?.isCrypto == true - || decimalCount <= 2 + || decimalCount <= 2 if (amountDouble != null && decimalCountOkay) { val intPart = truncate(amountDouble).toInt() val decimalPartFormatted = if (decimalPartString != null) { - ".${decimalPartString}" + "${localDecimalSeparator()}${decimalPartString}" } else "" val finalAmount = formatInt(intPart) + decimalPartFormatted @@ -194,11 +195,12 @@ fun AmountInput( }, onDecimalPoint = { if (firstInput) { - setAmount("0.") + setAmount("0${localDecimalSeparator()}") firstInput = false } else { - val newlyEnteredString = if (amount.isEmpty()) "0." else "$amount." - if (newlyEnteredString.replace(",", "").toDoubleOrNull() != null) { + val newlyEnteredString = if (amount.isEmpty()) + "0${localDecimalSeparator()}" else "$amount${localDecimalSeparator()}" + if (newlyEnteredString.amountToDoubleOrNull() != null) { setAmount(newlyEnteredString) } } @@ -219,19 +221,17 @@ fun AmountInput( } private fun formatNumber(number: String): String? { - val newAmountString = number.replace(",", "") - - val decimalPartString = newAmountString - .split(".") + val decimalPartString = number + .split(localDecimalSeparator()) .getOrNull(1) val newDecimalCount = decimalPartString?.length ?: 0 - val amountDouble = newAmountString.toDoubleOrNull() + val amountDouble = number.amountToDoubleOrNull() if (newDecimalCount <= 2 && amountDouble != null) { val intPart = truncate(amountDouble).toInt() val decimalFormatted = if (decimalPartString != null) { - ".${decimalPartString}" + "${localDecimalSeparator()}${decimalPartString}" } else "" return formatInt(intPart) + decimalFormatted @@ -368,7 +368,7 @@ fun AmountKeyboard( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { - KeypadCircleButton(text = ".") { + KeypadCircleButton(text = localDecimalSeparator()) { onDecimalPoint() } @@ -449,6 +449,35 @@ private fun circleButtonModifier( .border(2.dp, IvyTheme.colors.medium, Shapes.roundedFull) } +fun String.amountToDoubleOrNull(): Double? { + return this.normalizeAmount().toDoubleOrNull() +} + +fun String.amountToDouble(): Double { + return this.normalizeAmount().toDouble() +} + +fun String.normalizeAmount(): String { + return this.removeGroupingSeparator() + .normalizeDecimalSeparator() +} + +fun String.removeGroupingSeparator(): String { + return replace(localGroupingSeparator(), "") +} + +fun String.normalizeDecimalSeparator(): String { + return replace(localDecimalSeparator(), ".") +} + +fun localDecimalSeparator(): String { + return DecimalFormatSymbols.getInstance().decimalSeparator.toString() +} + +fun localGroupingSeparator(): String { + return DecimalFormatSymbols.getInstance().groupingSeparator.toString() +} + @Preview @Composable private fun Preview() { @@ -465,6 +494,5 @@ private fun Preview() { } } - } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt index 4a81ba8c56..9e277c2745 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt @@ -55,7 +55,7 @@ fun BoxWithConstraintsScope.CalculatorModal( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp), - text = if (isEmpty) "Expression (+-/*=)" else expression, + text = if (isEmpty) "Calculation (+-/*=)" else expression, style = Typo.numberH2.style( fontWeight = FontWeight.Bold, textAlign = TextAlign.Center, @@ -117,7 +117,7 @@ fun BoxWithConstraintsScope.CalculatorModal( KeypadCircleButton(text = "=") { val result = calculate(expression) if (result != null) { - expression = result.format(currency).replace(",", "") + expression = result.format(currency).removeGroupingSeparator() } } }, @@ -126,7 +126,7 @@ fun BoxWithConstraintsScope.CalculatorModal( expression += it }, onDecimalPoint = { - expression += "." + expression += localDecimalSeparator() }, onBackspace = { if (expression.isNotEmpty()) { @@ -141,12 +141,17 @@ fun BoxWithConstraintsScope.CalculatorModal( private fun calculate(expression: String): Double? { return try { - Keval.eval(expression) + Keval.eval(expression.normalizeExpression()) } catch (e: Exception) { null } } +private fun String.normalizeExpression(): String { + return this.replace(localGroupingSeparator(), "") + .replace(localDecimalSeparator(), ".") +} + @Preview @Composable private fun Preview() { From 56f5af34a470ca19b6905917e8e4a6255b8459a3 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 17:43:00 +0200 Subject: [PATCH 06/14] Closes #119; Localize input formatting --- .../com/ivy/wallet/base/AmountFormatting.kt | 154 ++++++++++++++++++ .../main/java/com/ivy/wallet/base/UtilExt.kt | 108 ------------ .../wallet/ui/theme/modal/edit/AmountModal.kt | 30 ---- .../ui/theme/modal/edit/CalculatorModal.kt | 9 +- 4 files changed, 157 insertions(+), 144 deletions(-) create mode 100644 app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt diff --git a/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt b/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt new file mode 100644 index 0000000000..37e3b93e53 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt @@ -0,0 +1,154 @@ +package com.ivy.wallet.base + +import com.ivy.wallet.model.IvyCurrency +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import kotlin.math.abs +import kotlin.math.log10 + +const val MILLION = 1000000 +const val N_100K = 100000 +const val THOUSAND = 1000 + +fun String.amountToDoubleOrNull(): Double? { + return this.normalizeAmount().toDoubleOrNull() +} + +fun String.amountToDouble(): Double { + return this.normalizeAmount().toDouble() +} + +fun String.normalizeAmount(): String { + return this.removeGroupingSeparator() + .normalizeDecimalSeparator() +} + +fun String.normalizeExpression(): String { + return this.removeGroupingSeparator() + .normalizeDecimalSeparator() +} + +fun String.removeGroupingSeparator(): String { + return replace(localGroupingSeparator(), "") +} + +fun String.normalizeDecimalSeparator(): String { + return replace(localDecimalSeparator(), ".") +} + +fun localDecimalSeparator(): String { + return DecimalFormatSymbols.getInstance().decimalSeparator.toString() +} + +fun localGroupingSeparator(): String { + return DecimalFormatSymbols.getInstance().groupingSeparator.toString() +} + +//Display Formatting +fun Double.format(digits: Int) = "%.${digits}f".format(this) + +fun Double.format(currencyCode: String): String { + return this.format(IvyCurrency.fromCode(currencyCode)) +} + +fun Double.format(currency: IvyCurrency?): String { + return if (currency?.isCrypto == true) { + val result = this.formatCrypto() + return when { + result.lastOrNull() == localDecimalSeparator().firstOrNull() -> { + val newResult = result.dropLast(1) + if (newResult.isEmpty()) "0" else newResult + } + result.isEmpty() -> { + "0" + } + else -> result + } + } else { + formatFIAT() + } +} + +fun Double.formatCrypto(): String { + val pattern = "###,###,##0.${"0".repeat(9)}" + val format = DecimalFormat(pattern) + val numberStringWithZeros = format.format(this) + + var lastTrailingZeroIndex: Int? = null + for (i in numberStringWithZeros.lastIndex.downTo(0)) { + if (numberStringWithZeros[i] == '0') { + lastTrailingZeroIndex = i + } else { + break + } + } + + return if (lastTrailingZeroIndex != null) + numberStringWithZeros.substring(0, lastTrailingZeroIndex) else numberStringWithZeros +} + +private fun Double.formatFIAT(): String = DecimalFormat("#,##0.00").format(this) + +fun shortenAmount(amount: Double): String { + return when { + abs(amount) >= MILLION -> { + formatShortenedNumber(amount / MILLION, "m") + } + abs(amount) >= THOUSAND -> { + formatShortenedNumber(amount / THOUSAND, "k") + } + else -> amount.toString() + } +} + +private fun formatShortenedNumber( + number: Double, + extension: String +): String { + return if (hasSignificantDecimalPart(number)) { + "${number.format(2)}$extension" + } else { + "${number.toInt()}$extension" + } +} + +fun hasSignificantDecimalPart(number: Double): Boolean { + //TODO: Review, might cause trouble when integrating crypto + val intPart = number.toInt() + return abs(number - intPart) >= 0.009 +} + +fun shouldShortAmount(amount: Double): Boolean { + return abs(amount) >= N_100K +} + +fun formatInt(number: Int): String { + return DecimalFormat("#,###,###,###").format(number) +} + +fun decimalPartFormatted(currency: String, value: Double): String { + return if (IvyCurrency.fromCode(currency)?.isCrypto == true) { + val decimalPartFormatted = value.formatCrypto() + .split(localDecimalSeparator()) + .getOrNull(1) ?: "null" + if (decimalPartFormatted.isNotBlank()) + "${localDecimalSeparator()}$decimalPartFormatted" else "" + } else { + "${localDecimalSeparator()}${decimalPartFormattedFIAT(value)}" + } +} + +private fun decimalPartFormattedFIAT(value: Double): String { + return DecimalFormat(".00").format(value) + .split(localDecimalSeparator()) + .getOrNull(1) + ?: value.toString() + .split(localDecimalSeparator()) + .getOrNull(1) + ?: "null" +} + +fun Long.length() = when (this) { + 0L -> 1 + else -> log10(abs(toDouble())).toInt() + 1 +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/base/UtilExt.kt b/app/src/main/java/com/ivy/wallet/base/UtilExt.kt index 40e1c019a4..ea0c2b5d4c 100644 --- a/app/src/main/java/com/ivy/wallet/base/UtilExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/UtilExt.kt @@ -3,16 +3,10 @@ package com.ivy.wallet.base import android.app.KeyguardManager import android.content.Context import android.icu.util.Currency -import com.ivy.wallet.model.IvyCurrency -import java.text.DecimalFormat import java.util.* import kotlin.math.abs import kotlin.math.log10 -const val MILLION = 1000000 -const val N_100K = 100000 -const val THOUSAND = 1000 - fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this) fun String.toBase64(): String = Base64.getEncoder().encodeToString(this.toByteArray()) @@ -70,76 +64,11 @@ fun MutableList?.orEmpty(): MutableList { fun String.nullifyEmpty() = if (this.isBlank()) null else this -fun Double.format(digits: Int) = "%.${digits}f".format(this) - -fun Double.format(currencyCode: String): String { - return this.format(IvyCurrency.fromCode(currencyCode)) -} - -fun Double.format(currency: IvyCurrency?): String { - return if (currency?.isCrypto == true) { - val result = this.formatCrypto() - return when { - result.lastOrNull() == '.' -> { - val newResult = result.dropLast(1) - if (newResult.isEmpty()) "0" else newResult - } - result.isEmpty() -> { - "0" - } - else -> result - } - } else { - formatFIAT() - } -} - -fun Double.formatCrypto(): String { - val pattern = "###,###,##0.${"0".repeat(9)}" - val format = DecimalFormat(pattern) - val numberStringWithZeros = format.format(this) - - var lastTrailingZeroIndex: Int? = null - for (i in numberStringWithZeros.lastIndex.downTo(0)) { - if (numberStringWithZeros[i] == '0') { - lastTrailingZeroIndex = i - } else { - break - } - } - - return if (lastTrailingZeroIndex != null) - numberStringWithZeros.substring(0, lastTrailingZeroIndex) else numberStringWithZeros -} - -fun Long.length() = when (this) { - 0L -> 1 - else -> log10(abs(toDouble())).toInt() + 1 -} - -private fun Double.formatFIAT(): String = DecimalFormat("#,##0.00").format(this) - fun getDefaultFIATCurrency(): Currency = Currency.getInstance(Locale.getDefault()) ?: Currency.getInstance("USD") ?: Currency.getInstance("usd") ?: Currency.getAvailableCurrencies().firstOrNull() ?: Currency.getInstance("EUR") -fun decimalPartFormatted(currency: String, value: Double): String { - return if (IvyCurrency.fromCode(currency)?.isCrypto == true) { - val decimalPartFormatted = value.formatCrypto() - .split(".") - .getOrNull(1) ?: "null" - if (decimalPartFormatted.isNotBlank()) ".$decimalPartFormatted" else "" - } else { - ".${decimalPartFormattedFIAT(value)}" - } -} - -private fun decimalPartFormattedFIAT(value: Double): String { - return DecimalFormat(".00").format(value).split(".", ",").getOrNull(1) - ?: value.toString().split(".", ",").getOrNull(1) ?: "null" -} - fun String.toUpperCaseLocal() = this.toUpperCase(Locale.getDefault()) fun String.toLowerCaseLocal() = this.toLowerCase(Locale.getDefault()) @@ -148,43 +77,6 @@ fun String.uppercaseLocal(): String = this.toUpperCase(Locale.getDefault()) fun String.capitalizeLocal(): String = this.capitalize(Locale.getDefault()) -fun shortenAmount(amount: Double): String { - return when { - abs(amount) >= MILLION -> { - formatShortenedNumber(amount / MILLION, "m") - } - abs(amount) >= THOUSAND -> { - formatShortenedNumber(amount / THOUSAND, "k") - } - else -> amount.toString() - } -} - -private fun formatShortenedNumber( - number: Double, - extension: String -): String { - return if (hasSignificantDecimalPart(number)) { - "${number.format(2)}$extension" - } else { - "${number.toInt()}$extension" - } -} - -fun hasSignificantDecimalPart(number: Double): Boolean { - //TODO: Review, might cause trouble when integrating crypto - val intPart = number.toInt() - return abs(number - intPart) >= 0.009 -} - -fun shouldShortAmount(amount: Double): Boolean { - return abs(amount) >= N_100K -} - -fun formatInt(number: Int): String { - return DecimalFormat("#,###,###,###").format(number) -} - fun hasLockScreen(context: Context): Boolean { val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager return keyguardManager.isDeviceSecure diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt index 16f36bc37e..0c268f55d6 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt @@ -27,7 +27,6 @@ import com.ivy.wallet.ui.theme.components.IvyIcon import com.ivy.wallet.ui.theme.modal.IvyModal import com.ivy.wallet.ui.theme.modal.ModalPositiveButton import com.ivy.wallet.ui.theme.modal.modalPreviewActionRowHeight -import java.text.DecimalFormatSymbols import java.util.* import kotlin.math.truncate @@ -449,35 +448,6 @@ private fun circleButtonModifier( .border(2.dp, IvyTheme.colors.medium, Shapes.roundedFull) } -fun String.amountToDoubleOrNull(): Double? { - return this.normalizeAmount().toDoubleOrNull() -} - -fun String.amountToDouble(): Double { - return this.normalizeAmount().toDouble() -} - -fun String.normalizeAmount(): String { - return this.removeGroupingSeparator() - .normalizeDecimalSeparator() -} - -fun String.removeGroupingSeparator(): String { - return replace(localGroupingSeparator(), "") -} - -fun String.normalizeDecimalSeparator(): String { - return replace(localDecimalSeparator(), ".") -} - -fun localDecimalSeparator(): String { - return DecimalFormatSymbols.getInstance().decimalSeparator.toString() -} - -fun localGroupingSeparator(): String { - return DecimalFormatSymbols.getInstance().groupingSeparator.toString() -} - @Preview @Composable private fun Preview() { diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt index 9e277c2745..cb51551037 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt @@ -9,6 +9,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.ivy.wallet.base.format +import com.ivy.wallet.base.localDecimalSeparator +import com.ivy.wallet.base.normalizeExpression import com.ivy.wallet.ui.IvyAppPreview import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.modal.IvyModal @@ -117,7 +119,7 @@ fun BoxWithConstraintsScope.CalculatorModal( KeypadCircleButton(text = "=") { val result = calculate(expression) if (result != null) { - expression = result.format(currency).removeGroupingSeparator() + expression = result.format(currency) } } }, @@ -147,11 +149,6 @@ private fun calculate(expression: String): Double? { } } -private fun String.normalizeExpression(): String { - return this.replace(localGroupingSeparator(), "") - .replace(localDecimalSeparator(), ".") -} - @Preview @Composable private fun Preview() { From 206c4fe2db45a41a276e9b74bb513ca1a129e7e9 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 17:52:25 +0200 Subject: [PATCH 07/14] Set CalculatorModal.kt initial value based on AmountModal.kt --- .../java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt | 1 + .../com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt index 0c268f55d6..1857b5cfd5 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt @@ -114,6 +114,7 @@ fun BoxWithConstraintsScope.AmountModal( CalculatorModal( visible = calculatorModalVisible, + initialAmount = amount.amountToDoubleOrNull(), currency = currency, dismiss = { calculatorModalVisible = false diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt index cb51551037..f3dec90d15 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt @@ -22,14 +22,15 @@ import java.util.* @Composable fun BoxWithConstraintsScope.CalculatorModal( id: UUID = UUID.randomUUID(), + initialAmount: Double?, visible: Boolean, currency: String, dismiss: () -> Unit, onCalculation: (Double) -> Unit ) { - var expression by remember(id) { - mutableStateOf("") + var expression by remember(id, initialAmount) { + mutableStateOf(initialAmount?.format(currency) ?: "") } IvyModal( @@ -155,6 +156,7 @@ private fun Preview() { IvyAppPreview { CalculatorModal( visible = true, + initialAmount = 50.23, currency = "BGN", dismiss = { }, onCalculation = {} From 8a6cb6d169fd396196ce7098b27a41a3a7593f3e Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:16:26 +0200 Subject: [PATCH 08/14] Closes #129; Format calculator amounts properly --- .../com/ivy/wallet/base/AmountFormatting.kt | 29 +++++++++++++ .../wallet/ui/theme/modal/edit/AmountModal.kt | 28 ++++--------- .../ui/theme/modal/edit/CalculatorModal.kt | 41 ++++++++++++++++--- 3 files changed, 72 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt b/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt index 37e3b93e53..41065cf388 100644 --- a/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt +++ b/app/src/main/java/com/ivy/wallet/base/AmountFormatting.kt @@ -5,6 +5,7 @@ import java.text.DecimalFormat import java.text.DecimalFormatSymbols import kotlin.math.abs import kotlin.math.log10 +import kotlin.math.truncate const val MILLION = 1000000 const val N_100K = 100000 @@ -151,4 +152,32 @@ private fun decimalPartFormattedFIAT(value: Double): String { fun Long.length() = when (this) { 0L -> 1 else -> log10(abs(toDouble())).toInt() + 1 +} + +fun formatInputAmount( + currency: String, + amount: String, + newSymbol: String +): String? { + val newlyEnteredNumberString = amount + newSymbol + + val decimalPartString = newlyEnteredNumberString + .split(localDecimalSeparator()) + .getOrNull(1) + val decimalCount = decimalPartString?.length ?: 0 + + val amountDouble = newlyEnteredNumberString.amountToDoubleOrNull() + + val decimalCountOkay = IvyCurrency.fromCode(currency)?.isCrypto == true + || decimalCount <= 2 + if (amountDouble != null && decimalCountOkay) { + val intPart = truncate(amountDouble).toInt() + val decimalPartFormatted = if (decimalPartString != null) { + "${localDecimalSeparator()}${decimalPartString}" + } else "" + + return formatInt(intPart) + decimalPartFormatted + } + + return null } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt index 1857b5cfd5..e2769f67c9 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/AmountModal.kt @@ -20,7 +20,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.ivy.wallet.R import com.ivy.wallet.base.* -import com.ivy.wallet.model.IvyCurrency import com.ivy.wallet.ui.IvyAppPreview import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.IvyIcon @@ -170,26 +169,13 @@ fun AmountInput( setAmount(it) firstInput = false } else { - val newlyEnteredNumberString = amount + it - - val decimalPartString = newlyEnteredNumberString - .split(localDecimalSeparator()) - .getOrNull(1) - val decimalCount = decimalPartString?.length ?: 0 - - val amountDouble = newlyEnteredNumberString.amountToDoubleOrNull() - - val decimalCountOkay = IvyCurrency.fromCode(currency)?.isCrypto == true - || decimalCount <= 2 - if (amountDouble != null && decimalCountOkay) { - val intPart = truncate(amountDouble).toInt() - val decimalPartFormatted = if (decimalPartString != null) { - "${localDecimalSeparator()}${decimalPartString}" - } else "" - - val finalAmount = formatInt(intPart) + decimalPartFormatted - - setAmount(finalAmount) + val formattedAmount = formatInputAmount( + currency = currency, + amount = amount, + newSymbol = it + ) + if (formattedAmount != null) { + setAmount(formattedAmount) } } }, diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt index f3dec90d15..2518b180a6 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/edit/CalculatorModal.kt @@ -8,9 +8,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.ivy.wallet.base.format -import com.ivy.wallet.base.localDecimalSeparator -import com.ivy.wallet.base.normalizeExpression +import com.ivy.wallet.base.* import com.ivy.wallet.ui.IvyAppPreview import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.modal.IvyModal @@ -126,10 +124,16 @@ fun BoxWithConstraintsScope.CalculatorModal( }, onNumberPressed = { - expression += it + expression = formatExpression( + expression = expression + it, + currency = currency + ) }, onDecimalPoint = { - expression += localDecimalSeparator() + expression = formatExpression( + expression = expression + localDecimalSeparator(), + currency = currency + ) }, onBackspace = { if (expression.isNotEmpty()) { @@ -142,6 +146,33 @@ fun BoxWithConstraintsScope.CalculatorModal( } } +private fun formatExpression(expression: String, currency: String): String { + var formattedExpression = expression + + expression + .split("(", ")", "/", "*", "-", "+") + .ifEmpty { + //handle only number expression formatting + listOf(expression) + } + .forEach { part -> + val numberPart = part.amountToDoubleOrNull() + if (numberPart != null) { + val formattedPart = formatInputAmount( + currency = currency, + amount = part, + newSymbol = "" + ) + + if (formattedPart != null) { + formattedExpression = formattedExpression.replace(part, formattedPart) + } + } + } + + return formattedExpression +} + private fun calculate(expression: String): Double? { return try { Keval.eval(expression.normalizeExpression()) From 0446f59c87cd5aead9085baabcbd0696b2539cec Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:28:25 +0200 Subject: [PATCH 09/14] Refactor vertical swipe handling by introducing Modifier#verticalSwipeListener() --- .../java/com/ivy/wallet/base/GesturesExt.kt | 45 +++++++++++++++++++ .../com/ivy/wallet/ui/home/HomeMoreMenu.kt | 27 +++-------- .../java/com/ivy/wallet/ui/home/HomeTab.kt | 27 ++++------- 3 files changed, 60 insertions(+), 39 deletions(-) create mode 100644 app/src/main/java/com/ivy/wallet/base/GesturesExt.kt diff --git a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt new file mode 100644 index 0000000000..eca7afa415 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt @@ -0,0 +1,45 @@ +package com.ivy.wallet.base + +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.input.pointer.pointerInput + +fun Modifier.verticalSwipeListener( + sensitivity: Int, + onSwipeUp: () -> Unit = {}, + onSwipeDown: () -> Unit = {} +): Modifier = composed { + var swipeOffset by remember { + mutableStateOf(0f) + } + + this.pointerInput(Unit) { + detectVerticalDragGestures( + onDragEnd = { + swipeOffset = 0f + }, + onVerticalDrag = { _, dragAmount -> + //dragAmount: positive when scrolling down; negative when scrolling up + swipeOffset += dragAmount + + when { + swipeOffset > sensitivity -> { + //offset > 0 when swipe down + onSwipeDown() + } + + swipeOffset < -sensitivity -> { + //offset < 0 when swipe up + onSwipeUp() + } + } + + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeMoreMenu.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeMoreMenu.kt index 8da2eb5a1e..e55521eab3 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeMoreMenu.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeMoreMenu.kt @@ -5,7 +5,6 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.* import androidx.compose.material.Text import androidx.compose.runtime.* @@ -16,7 +15,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler @@ -45,7 +43,7 @@ import com.ivy.wallet.ui.theme.wallet.AmountCurrencyB1 import java.util.* import kotlin.math.roundToInt -private const val SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU = -300 +private const val SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU = 300 @Composable @@ -118,8 +116,6 @@ fun BoxWithConstraintsScope.MoreMenu( } if (percentExpanded > 0.01f) { - var swipeOffset by remember { mutableStateOf(0f) } - Column( modifier = Modifier .statusBarsPadding() @@ -127,21 +123,12 @@ fun BoxWithConstraintsScope.MoreMenu( .fillMaxSize() .alpha(percentExpanded) .zIndex(510f) - .pointerInput(Unit) { - detectVerticalDragGestures( - onDragEnd = { - swipeOffset = 0f - }, - onVerticalDrag = { _, dragAmount -> - //dragAmount: positive when scrolling down; negative when scrolling up - swipeOffset += dragAmount - - if (swipeOffset < SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU) { - setExpanded(false) - } - } - ) - } + .verticalSwipeListener( + sensitivity = SWIPE_UP_THRESHOLD_CLOSE_MORE_MENU, + onSwipeUp = { + setExpanded(false) + } + ) ) { val modalId = remember { UUID.randomUUID() diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt index 7cd8f2e2df..bd96606a58 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt @@ -1,8 +1,8 @@ + package com.ivy.wallet.ui.home import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -10,13 +10,13 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding import com.ivy.wallet.base.onScreenStart +import com.ivy.wallet.base.verticalSwipeListener import com.ivy.wallet.logic.model.CustomerJourneyCardData import com.ivy.wallet.model.IvyCurrency import com.ivy.wallet.model.TransactionHistoryItem @@ -165,28 +165,17 @@ private fun BoxWithConstraintsScope.UI( } var expanded by remember { mutableStateOf(false) } - var headerSwipeOffset by remember { mutableStateOf(0f) } - Column( modifier = Modifier .fillMaxSize() .statusBarsPadding() .navigationBarsPadding() - .pointerInput(Unit) { - detectVerticalDragGestures( - onDragEnd = { - headerSwipeOffset = 0f - }, - onVerticalDrag = { _, dragAmount -> - //dragAmount: positive when scrolling down; negative when scrolling up - headerSwipeOffset += dragAmount - - if (headerSwipeOffset > SWIPE_DOWN_THRESHOLD_OPEN_MORE_MENU) { - expanded = true - } - } - ) - } + .verticalSwipeListener( + sensitivity = SWIPE_DOWN_THRESHOLD_OPEN_MORE_MENU, + onSwipeDown = { + expanded = true + } + ) ) { val ivyContext = LocalIvyContext.current val listState = rememberLazyListState( From 3df71220b31e9e5b721d7d963959fb6e3ea12308 Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:37:37 +0200 Subject: [PATCH 10/14] Closes #88; Navigate with horizontal swipe between "Home" and "Accounts" --- .../java/com/ivy/wallet/base/GesturesExt.kt | 36 +++++++++++++++++++ .../ivy/wallet/logic/CustomerJourneyLogic.kt | 2 +- .../main/java/com/ivy/wallet/ui/IvyContext.kt | 5 +++ .../com/ivy/wallet/ui/accounts/AccountsTab.kt | 12 ++++++- .../java/com/ivy/wallet/ui/home/HomeTab.kt | 12 +++++-- .../com/ivy/wallet/ui/home/HomeViewModel.kt | 2 +- .../com/ivy/wallet/ui/main/MainViewModel.kt | 4 +-- 7 files changed, 66 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt index eca7afa415..f75966d75d 100644 --- a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt @@ -1,5 +1,6 @@ package com.ivy.wallet.base +import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectVerticalDragGestures import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,6 +40,41 @@ fun Modifier.verticalSwipeListener( } } + } + ) + } +} + +fun Modifier.horizontalSwipeListener( + sensitivity: Int, + onSwipeLeft: () -> Unit = {}, + onSwipeRight: () -> Unit = {} +): Modifier = composed { + var swipeOffset by remember { + mutableStateOf(0f) + } + + this.pointerInput(Unit) { + detectHorizontalDragGestures( + onDragEnd = { + swipeOffset = 0f + }, + onHorizontalDrag = { _, dragAmount -> + //dragAmount: positive when scrolling down; negative when scrolling up + swipeOffset += dragAmount + + when { + swipeOffset > sensitivity -> { + //offset > 0 when swipe right + onSwipeRight() + } + + swipeOffset < -sensitivity -> { + //offset < 0 when swipe left + onSwipeLeft() + } + } + } ) } diff --git a/app/src/main/java/com/ivy/wallet/logic/CustomerJourneyLogic.kt b/app/src/main/java/com/ivy/wallet/logic/CustomerJourneyLogic.kt index 2111b22c91..d6c4228812 100644 --- a/app/src/main/java/com/ivy/wallet/logic/CustomerJourneyLogic.kt +++ b/app/src/main/java/com/ivy/wallet/logic/CustomerJourneyLogic.kt @@ -72,7 +72,7 @@ class CustomerJourneyLogic( backgroundColor = Ivy, hasDismiss = false, onAction = { ivyContext, _ -> - ivyContext.mainTab = MainTab.ACCOUNTS + ivyContext.selectMainTab(MainTab.ACCOUNTS) } ) diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt index 1d8c6597df..f513b4f9cb 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyContext.kt @@ -79,6 +79,11 @@ class IvyContext { var transactionsListState: LazyListState? = null var mainTab by mutableStateOf(MainTab.HOME) + private set + + fun selectMainTab(tab: MainTab) { + mainTab = tab + } //------------------------------------------ State --------------------------------------------- //------------------------------------------- BackStack ---------------------------------------- diff --git a/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt b/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt index 31e0d4889e..9cd0f7ec9c 100644 --- a/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt @@ -20,11 +20,13 @@ import com.google.accompanist.insets.statusBarsPadding import com.ivy.wallet.R import com.ivy.wallet.base.clickableNoIndication import com.ivy.wallet.base.format +import com.ivy.wallet.base.horizontalSwipeListener import com.ivy.wallet.base.onScreenStart import com.ivy.wallet.model.entity.Account import com.ivy.wallet.ui.IvyAppPreview import com.ivy.wallet.ui.LocalIvyContext import com.ivy.wallet.ui.Screen +import com.ivy.wallet.ui.main.MainTab import com.ivy.wallet.ui.theme.* import com.ivy.wallet.ui.theme.components.* import com.ivy.wallet.ui.theme.modal.edit.AccountModal @@ -64,12 +66,20 @@ private fun BoxWithConstraintsScope.UI( var reorderVisible by remember { mutableStateOf(false) } var accountModalData: AccountModalData? by remember { mutableStateOf(null) } + val ivyContext = LocalIvyContext.current + Column( modifier = Modifier .fillMaxSize() .statusBarsPadding() .navigationBarsPadding() - .verticalScroll(rememberScrollState()), + .verticalScroll(rememberScrollState()) + .horizontalSwipeListener( + sensitivity = 250, + onSwipeLeft = { + ivyContext.selectMainTab(MainTab.HOME) + } + ), ) { Spacer(Modifier.height(32.dp)) diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt index bd96606a58..db250f50de 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt @@ -1,4 +1,3 @@ - package com.ivy.wallet.ui.home import androidx.compose.animation.ExperimentalAnimationApi @@ -15,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.accompanist.insets.navigationBarsPadding import com.google.accompanist.insets.statusBarsPadding +import com.ivy.wallet.base.horizontalSwipeListener import com.ivy.wallet.base.onScreenStart import com.ivy.wallet.base.verticalSwipeListener import com.ivy.wallet.logic.model.CustomerJourneyCardData @@ -26,6 +26,7 @@ import com.ivy.wallet.model.entity.Transaction import com.ivy.wallet.ui.IvyAppPreview import com.ivy.wallet.ui.LocalIvyContext import com.ivy.wallet.ui.Screen +import com.ivy.wallet.ui.main.MainTab import com.ivy.wallet.ui.onboarding.model.TimePeriod import com.ivy.wallet.ui.theme.Theme import com.ivy.wallet.ui.theme.modal.* @@ -165,6 +166,8 @@ private fun BoxWithConstraintsScope.UI( } var expanded by remember { mutableStateOf(false) } + val ivyContext = LocalIvyContext.current + Column( modifier = Modifier .fillMaxSize() @@ -176,8 +179,13 @@ private fun BoxWithConstraintsScope.UI( expanded = true } ) + .horizontalSwipeListener( + sensitivity = 250, + onSwipeRight = { + ivyContext.selectMainTab(MainTab.ACCOUNTS) + } + ) ) { - val ivyContext = LocalIvyContext.current val listState = rememberLazyListState( initialFirstVisibleItemIndex = ivyContext.transactionsListState ?.firstVisibleItemIndex ?: 0, diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt index 38cce2e511..af3678c06e 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt @@ -174,7 +174,7 @@ class HomeViewModel @Inject constructor( ivyContext.navigateTo(Screen.BalanceScreen) } else { //doesn't have transactions lead him to adjust balance - ivyContext.mainTab = MainTab.ACCOUNTS + ivyContext.selectMainTab(MainTab.ACCOUNTS) ivyContext.navigateTo(Screen.Main) } } diff --git a/app/src/main/java/com/ivy/wallet/ui/main/MainViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/main/MainViewModel.kt index 6fe57d47ac..0e0ef93ae9 100644 --- a/app/src/main/java/com/ivy/wallet/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/main/MainViewModel.kt @@ -35,7 +35,7 @@ class MainViewModel @Inject constructor( fun start(screen: Screen.Main) { ivyContext.onBackPressed[screen] = { if (ivyContext.mainTab == MainTab.ACCOUNTS) { - ivyContext.mainTab = MainTab.HOME + ivyContext.selectMainTab(MainTab.HOME) true } else { //Exiting (the backstack will close the app) @@ -64,7 +64,7 @@ class MainViewModel @Inject constructor( } fun selectTab(tab: MainTab) { - ivyContext.mainTab = tab + ivyContext.selectMainTab(tab) } fun createAccount(data: CreateAccountData) { From cb7ef2dbd883908cd42b1b60b284ab765a369a9d Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:49:13 +0200 Subject: [PATCH 11/14] Closes #128; Improve GesturesExt.kt and implement change months with swipe --- .../java/com/ivy/wallet/base/GesturesExt.kt | 28 ++++++++++++++++--- .../java/com/ivy/wallet/ui/home/HomeHeader.kt | 22 +++++++++++++-- .../java/com/ivy/wallet/ui/home/HomeTab.kt | 12 ++++++-- .../com/ivy/wallet/ui/home/HomeViewModel.kt | 18 ++++++++++++ 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt index f75966d75d..4460ce94c1 100644 --- a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt @@ -18,11 +18,15 @@ fun Modifier.verticalSwipeListener( var swipeOffset by remember { mutableStateOf(0f) } + var gestureConsumed by remember { + mutableStateOf(false) + } this.pointerInput(Unit) { detectVerticalDragGestures( onDragEnd = { swipeOffset = 0f + gestureConsumed = false }, onVerticalDrag = { _, dragAmount -> //dragAmount: positive when scrolling down; negative when scrolling up @@ -31,12 +35,18 @@ fun Modifier.verticalSwipeListener( when { swipeOffset > sensitivity -> { //offset > 0 when swipe down - onSwipeDown() + if (!gestureConsumed) { + onSwipeDown() + gestureConsumed = true + } } swipeOffset < -sensitivity -> { //offset < 0 when swipe up - onSwipeUp() + if (!gestureConsumed) { + onSwipeUp() + gestureConsumed = true + } } } @@ -53,11 +63,15 @@ fun Modifier.horizontalSwipeListener( var swipeOffset by remember { mutableStateOf(0f) } + var gestureConsumed by remember { + mutableStateOf(false) + } this.pointerInput(Unit) { detectHorizontalDragGestures( onDragEnd = { swipeOffset = 0f + gestureConsumed = false }, onHorizontalDrag = { _, dragAmount -> //dragAmount: positive when scrolling down; negative when scrolling up @@ -66,12 +80,18 @@ fun Modifier.horizontalSwipeListener( when { swipeOffset > sensitivity -> { //offset > 0 when swipe right - onSwipeRight() + if (!gestureConsumed) { + onSwipeRight() + gestureConsumed = true + } } swipeOffset < -sensitivity -> { //offset < 0 when swipe left - onSwipeLeft() + if (!gestureConsumed) { + onSwipeLeft() + gestureConsumed = true + } } } diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt index 40777f62b9..fe30b17473 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt @@ -50,7 +50,10 @@ internal fun HomeHeader( onShowMonthModal: () -> Unit, onOpenMoreMenu: () -> Unit, - onBalanceClick: () -> Unit + onBalanceClick: () -> Unit, + + onSelectNextMonth: () -> Unit, + onSelectPreviousMonth: () -> Unit, ) { val percentExpanded by animateFloatAsState( targetValue = if (expanded) 1f else 0f, @@ -75,7 +78,10 @@ internal fun HomeHeader( balance = balance, onShowMonthModal = onShowMonthModal, - onBalanceClick = onBalanceClick + onBalanceClick = onBalanceClick, + + onSelectNextMonth = onSelectNextMonth, + onSelectPreviousMonth = onSelectPreviousMonth ) Spacer(Modifier.height(16.dp)) @@ -115,6 +121,9 @@ private fun HeaderStickyRow( onShowMonthModal: () -> Unit, onBalanceClick: () -> Unit, + + onSelectNextMonth: () -> Unit, + onSelectPreviousMonth: () -> Unit, ) { Row( modifier = Modifier.fillMaxWidth(), @@ -151,6 +160,15 @@ private fun HeaderStickyRow( Spacer(Modifier.weight(1f)) IvyOutlinedButton( + modifier = Modifier.horizontalSwipeListener( + sensitivity = 100, + onSwipeLeft = { + onSelectPreviousMonth() + }, + onSwipeRight = { + onSelectNextMonth() + } + ), iconStart = R.drawable.ic_calendar, text = period.toDisplayShort(LocalIvyContext.current.startDayOfMonth), ) { diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt index db250f50de..b419f4cb5b 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt @@ -113,7 +113,9 @@ fun BoxWithConstraintsScope.HomeTab(screen: Screen.Main) { onSetCurrency = viewModel::setCurrency, onSetPeriod = viewModel::setPeriod, onPayOrGet = viewModel::payOrGet, - onDismissCustomerJourneyCard = viewModel::dismissCustomerJourneyCard + onDismissCustomerJourneyCard = viewModel::dismissCustomerJourneyCard, + onSelectNextMonth = viewModel::nextMonth, + onSelectPreviousMonth = viewModel::previousMonth ) } @@ -157,7 +159,9 @@ private fun BoxWithConstraintsScope.UI( onSetBuffer: (Double) -> Unit = {}, onSetPeriod: (TimePeriod) -> Unit = {}, onPayOrGet: (Transaction) -> Unit = {}, - onDismissCustomerJourneyCard: (CustomerJourneyCardData) -> Unit = {} + onDismissCustomerJourneyCard: (CustomerJourneyCardData) -> Unit = {}, + onSelectNextMonth: () -> Unit = {}, + onSelectPreviousMonth: () -> Unit = {}, ) { var bufferModalData: BufferModalData? by remember { mutableStateOf(null) } var currencyModalVisible by remember { mutableStateOf(false) } @@ -213,7 +217,9 @@ private fun BoxWithConstraintsScope.UI( }, onBalanceClick = { onBalanceClick() - } + }, + onSelectNextMonth = onSelectNextMonth, + onSelectPreviousMonth = onSelectPreviousMonth ) HomeTransactionsLazyColumn( diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt index af3678c06e..7444100071 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeViewModel.kt @@ -243,4 +243,22 @@ class HomeViewModel @Inject constructor( customerJourneyLogic.dismissCard(card) load() } + + fun nextMonth() { + val month = period.value?.month + if (month != null) { + load( + period = month.incrementMonthPeriod(ivyContext, 1L), + ) + } + } + + fun previousMonth() { + val month = period.value?.month + if (month != null) { + load( + period = month.incrementMonthPeriod(ivyContext, -1L), + ) + } + } } \ No newline at end of file From b004169768ce2588520db423b75fe19eb46a827d Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:57:00 +0200 Subject: [PATCH 12/14] Improve horizontal swipe between tabs UX --- app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt | 3 +++ app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt b/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt index 9cd0f7ec9c..dc440bf5dc 100644 --- a/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/accounts/AccountsTab.kt @@ -78,6 +78,9 @@ private fun BoxWithConstraintsScope.UI( sensitivity = 250, onSwipeLeft = { ivyContext.selectMainTab(MainTab.HOME) + }, + onSwipeRight = { + ivyContext.selectMainTab(MainTab.HOME) } ), ) { diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt index b419f4cb5b..710c0e12b2 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeTab.kt @@ -185,6 +185,9 @@ private fun BoxWithConstraintsScope.UI( ) .horizontalSwipeListener( sensitivity = 250, + onSwipeLeft = { + ivyContext.selectMainTab(MainTab.ACCOUNTS) + }, onSwipeRight = { ivyContext.selectMainTab(MainTab.ACCOUNTS) } From dd7dd49631e4f35dfe4442e29b979f656043fa3d Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 18:58:39 +0200 Subject: [PATCH 13/14] Improve change months with horizontal swipe UX --- app/src/main/java/com/ivy/wallet/base/GesturesExt.kt | 6 ++++++ app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt index 4460ce94c1..fb630f76c8 100644 --- a/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt +++ b/app/src/main/java/com/ivy/wallet/base/GesturesExt.kt @@ -10,6 +10,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.input.pointer.pointerInput +/** + * sensitivity - the lower the number, the higher the sensitivity + */ fun Modifier.verticalSwipeListener( sensitivity: Int, onSwipeUp: () -> Unit = {}, @@ -55,6 +58,9 @@ fun Modifier.verticalSwipeListener( } } +/** + * sensitivity - the lower the number, the higher the sensitivity + */ fun Modifier.horizontalSwipeListener( sensitivity: Int, onSwipeLeft: () -> Unit = {}, diff --git a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt index fe30b17473..7031ac8a54 100644 --- a/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt +++ b/app/src/main/java/com/ivy/wallet/ui/home/HomeHeader.kt @@ -161,12 +161,12 @@ private fun HeaderStickyRow( IvyOutlinedButton( modifier = Modifier.horizontalSwipeListener( - sensitivity = 100, + sensitivity = 75, onSwipeLeft = { - onSelectPreviousMonth() + onSelectNextMonth() }, onSwipeRight = { - onSelectNextMonth() + onSelectPreviousMonth() } ), iconStart = R.drawable.ic_calendar, From 328c864fd5e934ee2bafe51cf3504e7d4a3eb7aa Mon Sep 17 00:00:00 2001 From: Iliyan Germanov Date: Mon, 15 Nov 2021 19:01:07 +0200 Subject: [PATCH 14/14] Bump version --- .../src/main/java/com/ivy/wallet/buildsrc/dependencies.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt index 1194ec1e1f..2e16a2f81b 100644 --- a/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt +++ b/buildSrc/src/main/java/com/ivy/wallet/buildsrc/dependencies.kt @@ -21,8 +21,8 @@ object Libs { object Project { //Version - const val versionName = "2.2.1-comet" - const val versionCode = 79 + const val versionName = "2.2.2-comet" + const val versionCode = 80 //Compile SDK & Build Tools const val compileSdkVersion = 30