Skip to content

Commit

Permalink
Redesign Deposit & Withdraw UI (#73)
Browse files Browse the repository at this point in the history
 * Remove deposit withdraw dialog and add a dedicated screen for adding transactions.
 * Add ability to set custom date and time per transaction.
 * Add an optional text field to add notes per transactions (#72)
 * Update transactions list UI in goal info screen to reflect these changes.
 * Add ability to delete specific transactions from goal info screen. (#24)
 * Don't show "Authentication Succeeded" toast message every on launch, instead just open it normally. (#71)
---------
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam authored Feb 24, 2024
1 parent 1fa8b3b commit adf714b
Show file tree
Hide file tree
Showing 33 changed files with 64,814 additions and 149,940 deletions.
9 changes: 6 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ dependencies {
implementation 'androidx.activity:activity-compose:1.8.2'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0"
implementation "androidx.navigation:navigation-compose:2.7.6"
implementation "androidx.navigation:navigation-compose:2.7.7"
// Jetpack compose.
implementation "androidx.compose.ui:ui"
implementation "androidx.compose.ui:ui-tooling-preview"
Expand Down Expand Up @@ -116,8 +116,11 @@ dependencies {
// Coil Image loading library.
implementation "io.coil-kt:coil-compose:2.4.0"
// Material 3 calender / Date picker.
implementation 'com.maxkeppeler.sheets-compose-dialogs:core:1.1.0'
implementation 'com.maxkeppeler.sheets-compose-dialogs:calendar:1.1.0'
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'
// Swipe actions.
implementation "me.saket.swipe:swipe:1.2.0"
// Lottie animations.
implementation "com.airbnb.android:lottie-compose:4.1.0"
// Bio-metric authentication.
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/com/starry/greenstash/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import com.starry.greenstash.ui.screens.settings.viewmodels.SettingsViewModel
import com.starry.greenstash.ui.screens.settings.viewmodels.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 kotlinx.coroutines.ExperimentalCoroutinesApi
import java.util.concurrent.Executor
Expand Down Expand Up @@ -104,7 +103,6 @@ class MainActivity : AppCompatActivity() {
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
getString(R.string.auth_successful).toToast(this@MainActivity)
// make app contents visible after successful authentication.
setAppContents()
mainViewModel.appUnlocked = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ interface GoalDao {
@Query("SELECT * FROM saving_goal WHERE goalId = :goalId")
suspend fun getGoalWithTransactionById(goalId: Long): GoalWithTransactions?

@Transaction
@Query("SELECT * FROM saving_goal WHERE goalId = :goalId")
fun getGoalWithTransactionByIdAsFlow(goalId: Long): Flow<GoalWithTransactions?>

@Transaction
@Query(
"SELECT * FROM saving_goal ORDER BY " +
Expand Down
44 changes: 40 additions & 4 deletions app/src/main/java/com/starry/greenstash/ui/navigation/NavGraph.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import androidx.navigation.navArgument
import com.starry.greenstash.ui.screens.backups.BackupScreen
import com.starry.greenstash.ui.screens.home.composables.HomeScreen
import com.starry.greenstash.ui.screens.info.composables.GoalInfoScreen
import com.starry.greenstash.ui.screens.input.composables.CongratsScreen
import com.starry.greenstash.ui.screens.input.composables.DWScreen
import com.starry.greenstash.ui.screens.input.composables.InputScreen
import com.starry.greenstash.ui.screens.settings.composables.AboutScreen
import com.starry.greenstash.ui.screens.settings.composables.OSLScreen
Expand Down Expand Up @@ -117,6 +119,29 @@ fun NavGraph(
HomeScreen(navController)
}

/** Deposit Withdraw Screen */
composable(
route = Screens.DWScreen.route,
enterTransition = { enterTransition() },
exitTransition = { exitTransition() },
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() },
arguments = listOf(
navArgument(DW_GOAL_ID_ARG_KEY) {
type = NavType.StringType
},
),
) { backStackEntry ->
val goalId = backStackEntry.arguments!!.getString(DW_GOAL_ID_ARG_KEY)!!
val transactionType =
backStackEntry.arguments!!.getString(DW_TRANSACTION_TYPE_ARG_KEY)!!
DWScreen(
goalId = goalId,
transactionTypeName = transactionType,
navController = navController
)
}

/** Goal Info Screen */
composable(
route = Screens.GoalInfoScreen.route,
Expand All @@ -131,7 +156,7 @@ fun NavGraph(
),
) { backStackEntry ->
val goalId = backStackEntry.arguments!!.getString(GOAL_INFO_ARG_KEY)!!
GoalInfoScreen(goalId = goalId, navController)
GoalInfoScreen(goalId = goalId, navController = navController)
}

/** Input Screen */
Expand All @@ -148,7 +173,18 @@ fun NavGraph(
})
) { backStackEntry ->
val editGoalId = backStackEntry.arguments!!.getString(EDIT_GOAL_ARG_KEY)
InputScreen(editGoalId, navController)
InputScreen(editGoalId = editGoalId, navController = navController)
}

/** Goal Achieved Screen */
composable(
route = Screens.CongratsScreen.route,
enterTransition = { enterTransition() },
exitTransition = { exitTransition() },
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() },
) {
CongratsScreen(navController = navController)
}

/** Backup Screen */
Expand All @@ -159,7 +195,7 @@ fun NavGraph(
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() },
) {
BackupScreen(navController)
BackupScreen(navController = navController)
}

/** Settings Screen */
Expand All @@ -170,7 +206,7 @@ fun NavGraph(
popEnterTransition = { popEnterTransition() },
popExitTransition = { popExitTransition() },
) {
SettingsScreen(navController)
SettingsScreen(navController = navController)
}

/** Open Source Licenses Screen */
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/java/com/starry/greenstash/ui/navigation/Screens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,21 @@

package com.starry.greenstash.ui.navigation

const val DW_GOAL_ID_ARG_KEY = "dwGoal"
const val DW_TRANSACTION_TYPE_ARG_KEY = "dwTransactionType"
const val EDIT_GOAL_ARG_KEY = "editGoal"
const val GOAL_INFO_ARG_KEY = "goalId"

sealed class Screens(val route: String) {

data object DWScreen :
Screens("deposit_withdraw_screen/{$DW_GOAL_ID_ARG_KEY}/{$DW_TRANSACTION_TYPE_ARG_KEY}") {
fun withGoalId(goalId: String, trasactionType: String): String {
return route.replace("{$DW_GOAL_ID_ARG_KEY}", goalId)
.replace("{$DW_TRANSACTION_TYPE_ARG_KEY}", trasactionType)
}
}

data object InputScreen : Screens("input_screen?$EDIT_GOAL_ARG_KEY={$EDIT_GOAL_ARG_KEY}") {
fun withGoalToEdit(goalId: String): String {
return route.replace("{$EDIT_GOAL_ARG_KEY}", goalId)
Expand All @@ -41,6 +52,7 @@ sealed class Screens(val route: String) {
}
}

data object CongratsScreen : Screens("goal_achieved_screen")
data object AboutScreen : Screens("about_screen")
data object OSLScreen : Screens("osl_screen")
data object WelcomeScreen : Screens("welcome_screen")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fun GoalItem(
onEditClicked: () -> Unit,
onDeleteClicked: () -> Unit,
) {
val progress by animateFloatAsState(targetValue = goalProgress)
val progress by animateFloatAsState(targetValue = goalProgress, label = "goal progress")

Card(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,19 @@ import android.content.Context
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.navigation.NavController
import com.starry.greenstash.R
import com.starry.greenstash.database.core.GoalWithTransactions
import com.starry.greenstash.database.transaction.TransactionType
import com.starry.greenstash.ui.navigation.Screens
import com.starry.greenstash.ui.screens.home.viewmodels.BottomSheetType
import com.starry.greenstash.ui.screens.home.viewmodels.HomeViewModel
import com.starry.greenstash.utils.Utils
import com.starry.greenstash.utils.validateAmount
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch

Expand All @@ -63,19 +57,13 @@ fun GoalLazyColumnItem(
viewModel: HomeViewModel,
item: GoalWithTransactions,
snackBarHostState: SnackbarHostState,
bottomSheetState: ModalBottomSheetState,
bottomSheetType: MutableState<BottomSheetType>,
navController: NavController
) {
val coroutineScope = rememberCoroutineScope()
val progressPercent =
((item.getCurrentlySavedAmount() / item.goal.targetAmount) * 100).toInt()

val openDeleteDialog = remember { mutableStateOf(false) }
val openDepositDialog = remember { mutableStateOf(false) }
val openWithdrawDialog = remember { mutableStateOf(false) }

val hapticFeedback = LocalHapticFeedback.current

GoalItem(title = item.goal.title,
primaryText = viewModel.goalTextUtil.buildPrimaryText(context, progressPercent, item),
Expand All @@ -88,7 +76,12 @@ fun GoalLazyColumnItem(
snackBarHostState.showSnackbar(context.getString(R.string.goal_already_achieved))
}
} else {
openDepositDialog.value = true
navController.navigate(
Screens.DWScreen.withGoalId(
goalId = item.goal.goalId.toString(),
trasactionType = TransactionType.Deposit.name
)
)
}
},
onWithdrawClicked = {
Expand All @@ -97,7 +90,12 @@ fun GoalLazyColumnItem(
snackBarHostState.showSnackbar(context.getString(R.string.withdraw_button_error))
}
} else {
openWithdrawDialog.value = true
navController.navigate(
Screens.DWScreen.withGoalId(
goalId = item.goal.goalId.toString(),
trasactionType = TransactionType.Withdraw.name
)
)
}
},
onInfoClicked = {
Expand All @@ -116,55 +114,13 @@ fun GoalLazyColumnItem(
},
onDeleteClicked = { openDeleteDialog.value = true })

ActionDialogs(
HomeDialogs(
openDeleteDialog = openDeleteDialog,
openDepositDialog = openDepositDialog,
openWithdrawDialog = openWithdrawDialog,
onDeleteConfirmed = {
viewModel.deleteGoal(item.goal)
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.goal_delete_success))
}
}, onDepositConfirmed = { amount, notes ->
if (!amount.validateAmount()) {
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.amount_empty_err))
}
} else if (item.getCurrentlySavedAmount() >= item.goal.targetAmount) {
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.goal_already_achieved))
}
} else {
val amountDouble = Utils.roundDecimal(amount.toDouble())
viewModel.deposit(item.goal, amountDouble, notes, onGoalAchieved = {
// Show a congratulations message when goal is achieved.
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
coroutineScope.launch {
bottomSheetType.value = BottomSheetType.GOAL_ACHIEVED
if (!bottomSheetState.isVisible) {
bottomSheetState.show()
}
}
})
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.deposit_successful))
}
}
}, onWithdrawConfirmed = { amount, notes ->
if (!amount.validateAmount()) {
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.amount_empty_err))
}
} else {
val amountDouble = Utils.roundDecimal(amount.toDouble())
if (amountDouble > item.getCurrentlySavedAmount()) {
coroutineScope.launch {
snackBarHostState.showSnackbar(context.getString(R.string.withdraw_overflow_error))
}
} else {
viewModel.withdraw(item.goal, amountDouble, notes)
}
}
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fun MainAppBar(
) {
Crossfade(
targetState = searchWidgetState,
animationSpec = tween(durationMillis = 300)
animationSpec = tween(durationMillis = 300), label = "searchbar cross-fade"
) {
when (it) {
SearchWidgetState.CLOSED -> {
Expand Down
Loading

0 comments on commit adf714b

Please sign in to comment.