diff --git a/app/build.gradle b/app/build.gradle index 9332463b..5e8e4124 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -121,8 +121,8 @@ dependencies { implementation 'com.maxkeppeler.sheets-compose-dialogs:core:1.3.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:calendar:1.3.0' implementation 'com.maxkeppeler.sheets-compose-dialogs:date-time:1.3.0' - // Taptarget compose. - implementation "com.pierfrancescosoffritti.taptargetcompose:core:1.1.0" + // Tap-target compose. + implementation "com.pierfrancescosoffritti.taptargetcompose:core:1.1.1" // Lottie animations. implementation "com.airbnb.android:lottie-compose:4.1.0" // Bio-metric authentication. diff --git a/app/src/main/java/com/starry/greenstash/MainActivity.kt b/app/src/main/java/com/starry/greenstash/MainActivity.kt index e0f05025..a3586b81 100644 --- a/app/src/main/java/com/starry/greenstash/MainActivity.kt +++ b/app/src/main/java/com/starry/greenstash/MainActivity.kt @@ -30,10 +30,14 @@ import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface +import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Modifier import androidx.core.content.ContextCompat import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -41,10 +45,12 @@ import androidx.lifecycle.ViewModelProvider import androidx.navigation.compose.rememberNavController import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.starry.greenstash.ui.navigation.NavGraph +import com.starry.greenstash.ui.screens.other.AppLockedScreen import com.starry.greenstash.ui.screens.settings.SettingsViewModel import com.starry.greenstash.ui.screens.settings.ThemeMode import com.starry.greenstash.ui.theme.GreenStashTheme import com.starry.greenstash.utils.Utils +import com.starry.greenstash.utils.toToast import dagger.hilt.android.AndroidEntryPoint import java.util.concurrent.Executor @@ -73,8 +79,10 @@ class MainActivity : AppCompatActivity() { mainViewModel.refreshReminders() val appLockStatus = settingsViewModel.getAppLockValue() + val showAppContents = mutableStateOf(false) - if (appLockStatus && !mainViewModel.appUnlocked) { + // check if app lock is enabled and user has not unlocked the app. + if (appLockStatus && !mainViewModel.isAppUnlocked()) { executor = ContextCompat.getMainExecutor(this) biometricPrompt = BiometricPrompt(this, executor, object : BiometricPrompt.AuthenticationCallback() { @@ -84,8 +92,8 @@ class MainActivity : AppCompatActivity() { ) { super.onAuthenticationSucceeded(result) // make app contents visible after successful authentication. - setAppContents() - mainViewModel.appUnlocked = true + showAppContents.value = true + mainViewModel.setAppUnlocked(true) } override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { @@ -98,11 +106,16 @@ class MainActivity : AppCompatActivity() { */ val biometricManager = BiometricManager.from(this@MainActivity) if (biometricManager.canAuthenticate(Utils.getAuthenticators()) != BiometricManager.BIOMETRIC_SUCCESS) { - setAppContents() - mainViewModel.appUnlocked = true + // make app contents visible. + showAppContents.value = true + // disable app lock. + mainViewModel.setAppUnlocked(true) + // disable app lock in settings. settingsViewModel.setAppLock(false) + // show error message. + getString(R.string.app_lock_unable_to_authenticate).toToast(this@MainActivity) } else { - finish() // close the app. + showAppContents.value = false } } }) @@ -113,14 +126,15 @@ class MainActivity : AppCompatActivity() { .setAllowedAuthenticators(Utils.getAuthenticators()) .build() - biometricPrompt.authenticate(promptInfo) - } else { - setAppContents() + showAppContents.value = true } + + // set app contents based on the value of showAppContents. + setAppContents(showAppContents) } - fun setAppContents() { + private fun setAppContents(showAppContents: State) { setContent { GreenStashTheme(settingsViewModel = settingsViewModel) { val systemUiController = rememberSystemUiController() @@ -138,9 +152,23 @@ class MainActivity : AppCompatActivity() { modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { - val navController = rememberNavController() - val screen by mainViewModel.startDestination - NavGraph(navController = navController, screen) + Crossfade( + targetState = showAppContents, + label = "AppLockCrossFade", + animationSpec = tween(500) + ) { showAppContents -> + // show app contents only if user has authenticated. + if (showAppContents.value) { + val navController = rememberNavController() + val screen by mainViewModel.startDestination + NavGraph(navController = navController, screen) + } else { + // show app locked screen if user has not authenticated. + AppLockedScreen(onAuthRequest = { + biometricPrompt.authenticate(promptInfo) + }) + } + } } } } diff --git a/app/src/main/java/com/starry/greenstash/MainViewModel.kt b/app/src/main/java/com/starry/greenstash/MainViewModel.kt index 648e6d18..b951fcb6 100644 --- a/app/src/main/java/com/starry/greenstash/MainViewModel.kt +++ b/app/src/main/java/com/starry/greenstash/MainViewModel.kt @@ -49,11 +49,11 @@ class MainViewModel @Inject constructor( private val reminderManager: ReminderManager ) : ViewModel() { /** - * Storing app lock status to avoid asking for authentication + * Store app lock status to avoid asking for authentication * when activity restarts like when changing app or device * theme or when changing device orientation. */ - var appUnlocked = false + private var _appUnlocked = false private val _isLoading: MutableState = mutableStateOf(true) val isLoading: State = _isLoading @@ -82,4 +82,10 @@ class MainViewModel @Inject constructor( reminderManager.checkAndScheduleReminders(goalDao.getAllGoals()) } } + + fun isAppUnlocked(): Boolean = _appUnlocked + + fun setAppUnlocked(value: Boolean) { + _appUnlocked = value + } } \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt index 7108ed5e..c408112c 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/backups/composables/BackupScreen.kt @@ -182,7 +182,7 @@ private fun BackupScreenContent( contentAlignment = Alignment.Center ) { AsyncImage( - model = R.drawable.backup_logo, + model = R.drawable.backup_icon, contentDescription = null, modifier = Modifier.size(200.dp) ) diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/other/AppLockedScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/other/AppLockedScreen.kt new file mode 100644 index 00000000..f29bb17c --- /dev/null +++ b/app/src/main/java/com/starry/greenstash/ui/screens/other/AppLockedScreen.kt @@ -0,0 +1,157 @@ +/** + * MIT License + * + * Copyright (c) [2022 - Present] Stɑrry Shivɑm + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +package com.starry.greenstash.ui.screens.other + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Fingerprint +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.starry.greenstash.R +import com.starry.greenstash.ui.theme.greenstashFont +import kotlinx.coroutines.delay + + +@Composable +fun AppLockedScreen(onAuthRequest: () -> Unit) { + Column( + Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + LockIconCard() // Animated lock icon + + Text( + text = stringResource(id = R.string.app_lock_screen_title), + style = MaterialTheme.typography.headlineSmall, + fontFamily = greenstashFont, + modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) + ) + + Text( + text = stringResource(id = R.string.app_lock_screen_subtitle), + style = MaterialTheme.typography.bodySmall, + fontFamily = greenstashFont, + modifier = Modifier.padding(horizontal = 42.dp) + ) + + FilledTonalButton( + onClick = onAuthRequest, + modifier = Modifier.padding(top = 18.dp) + ) { + Icon( + imageVector = Icons.Filled.Fingerprint, + contentDescription = stringResource(id = R.string.app_lock_button_icon_desc), + modifier = Modifier.size(ButtonDefaults.IconSize) + ) + Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing)) + Text( + text = stringResource(id = R.string.app_lock_button_text), + fontFamily = greenstashFont, + ) + } + } +} + +@Composable +private fun LockIconCard() { + val isAnimated = remember { mutableStateOf(false) } + val animationSize by animateFloatAsState( + targetValue = if (isAnimated.value) 1f else 0.8f, + animationSpec = tween(durationMillis = 500), label = "animationSize" + ) + + LaunchedEffect(key1 = true) { + delay(300) + isAnimated.value = true + } + + Card( + modifier = Modifier + .size(350.dp * animationSize) + .clip(CircleShape) + .animateContentSize(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + AsyncImage( + model = R.drawable.app_lock_icon, + contentDescription = null, + modifier = Modifier.size(200.dp) + ) + } + } + +} + + +@Preview(showBackground = true) +@Composable +private fun AppLockedScreenPV() { + AppLockedScreen(onAuthRequest = { + // do nothing + }) +} \ No newline at end of file diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt index c5ef9803..d6c71282 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/GoalCardStyle.kt @@ -43,7 +43,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -115,6 +117,7 @@ fun GoalCardStyle(navController: NavController) { modifier = Modifier .fillMaxSize() .padding(paddingValues) + .verticalScroll(rememberScrollState()), ) { OutlinedCard( modifier = Modifier diff --git a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt index 89fe69df..9bdf8803 100644 --- a/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt +++ b/app/src/main/java/com/starry/greenstash/ui/screens/settings/composables/SettingsScreen.kt @@ -472,7 +472,7 @@ private fun SecuritySettings(viewModel: SettingsViewModel) { super.onAuthenticationSucceeded(result) context.getString(R.string.auth_successful) .toToast(context) - mainActivity.mainViewModel.appUnlocked = true + mainActivity.mainViewModel.setAppUnlocked(true) viewModel.setAppLock(true) } diff --git a/app/src/main/res/drawable/app_lock_icon.png b/app/src/main/res/drawable/app_lock_icon.png new file mode 100644 index 00000000..64ca89a4 Binary files /dev/null and b/app/src/main/res/drawable/app_lock_icon.png differ diff --git a/app/src/main/res/drawable/backup_logo.png b/app/src/main/res/drawable/backup_icon.png similarity index 100% rename from app/src/main/res/drawable/backup_logo.png rename to app/src/main/res/drawable/backup_icon.png diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index b60fa6bf..ec64c6ea 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -24,6 +24,13 @@ ¡Hola! Por favor selecciona tú moneda preferida para iniciar. Comencemos + + GreenStash está bloqueado + Haz clic en el botón de abajo para autenticar. + Autorizar + Desbloquear Aplicación + El bloqueo de la aplicación está desactivado porque el dispositivo no puede autenticarse + Metas de Ahorro Open side menu diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0961b3fc..eafb0885 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -24,6 +24,13 @@ Merhaba! Lütfen başlamak için tercih edilen para birimini seçin. Başla + + GreenStash kilitli + Kimlik doğrulamak için aşağıdaki düğmeye tıklayın. + Yetkilendir + Uygulamayı Aç + Cihaz kimlik doğrulayamadığı için uygulama kilitli değil + Birikim Hedefleri Yan menüyü aç diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3f59c4ac..2da5b2dd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -24,6 +24,13 @@ 您好!请选择您的货币。 开始 + + GreenStash已锁定 + 点击下面的按钮进行身份验证。 + 授权 + 解锁应用 + 由于设备无法进行身份验证,应用程序锁定已禁用 + 省钱目标 打开侧边菜单 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a429d93a..15f81e95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,6 +24,13 @@ Hello! Please select your preferred currency to get started. Let\'s Get Started! + + GreenStash is locked + Click the button below to authenticate. + Authorize + Unlock App + App-lock is disabled because the device is unable to authenticate + Saving Goals Open side menu