diff --git a/app/src/main/java/org/robojackets/apiary/MainActivity.kt b/app/src/main/java/org/robojackets/apiary/MainActivity.kt index 3d9061b..b66f02f 100644 --- a/app/src/main/java/org/robojackets/apiary/MainActivity.kt +++ b/app/src/main/java/org/robojackets/apiary/MainActivity.kt @@ -22,6 +22,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -59,6 +60,7 @@ import org.robojackets.apiary.auth.ui.AuthenticationScreen import org.robojackets.apiary.base.GlobalSettings import org.robojackets.apiary.base.model.AttendableType import org.robojackets.apiary.base.ui.nfc.NfcRequired +import org.robojackets.apiary.base.ui.snackbar.SnackbarControllerProvider import org.robojackets.apiary.base.ui.theme.Apiary_MobileTheme import org.robojackets.apiary.merchandise.ui.MerchandiseDistributionScreen import org.robojackets.apiary.merchandise.ui.MerchandiseIndexScreen @@ -182,64 +184,67 @@ class MainActivity : ComponentActivity() { navReady = true } - // A surface container using the 'background' color from the theme - Surface(color = MaterialTheme.colorScheme.background) { - ModalBottomSheetLayout(bottomSheetNavigator) { - UpdateGate( - navReady = navReady, - onShowRequiredUpdatePrompt = { - navigationManager.navigate( - NavigationActions.UpdatePrompts.anyScreenToRequiredUpdatePrompt() - ) - }, - onShowOptionalUpdatePrompt = { - navigationManager.navigate( - NavigationActions.UpdatePrompts.anyScreenToOptionalUpdatePrompt() - ) - }, - onShowUpdateInProgressScreen = { - navigationManager.navigate( - NavigationActions.UpdatePrompts.anyScreenToUpdateInProgress() - ) - } - ) { - Scaffold( - topBar = { AppTopBar(settings.appEnv.production) }, - bottomBar = { - val current = currentRoute(navController) - if (shouldShowBottomNav(nfcEnabled, current)) { - NavigationBar { - navItems.forEach { screen -> - NavigationBarItem( - icon = { - Icon( - screen.icon, - contentDescription = screen.imgContentDescriptor - ) - }, - label = { Text(stringResource(screen.resourceId)) }, - selected = currentDestination - ?.hierarchy - ?.any { - it.route == screen.navigationDestination - } == true, - onClick = { - navigationManager.navigate( - NavigationActions.BottomNavTabs.withinBottomNavTabs( - screen.navigationDestination, - navController.graph.findStartDestination().id + SnackbarControllerProvider { snackbarHost -> + // A surface container using the 'background' color from the theme + Surface(color = MaterialTheme.colorScheme.background) { + ModalBottomSheetLayout(bottomSheetNavigator) { + UpdateGate( + navReady = navReady, + onShowRequiredUpdatePrompt = { + navigationManager.navigate( + NavigationActions.UpdatePrompts.anyScreenToRequiredUpdatePrompt() + ) + }, + onShowOptionalUpdatePrompt = { + navigationManager.navigate( + NavigationActions.UpdatePrompts.anyScreenToOptionalUpdatePrompt() + ) + }, + onShowUpdateInProgressScreen = { + navigationManager.navigate( + NavigationActions.UpdatePrompts.anyScreenToUpdateInProgress() + ) + } + ) { + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHost) }, + topBar = { AppTopBar(settings.appEnv.production) }, + bottomBar = { + val current = currentRoute(navController) + if (shouldShowBottomNav(nfcEnabled, current)) { + NavigationBar { + navItems.forEach { screen -> + NavigationBarItem( + icon = { + Icon( + screen.icon, + contentDescription = screen.imgContentDescriptor + ) + }, + label = { Text(stringResource(screen.resourceId)) }, + selected = currentDestination + ?.hierarchy + ?.any { + it.route == screen.navigationDestination + } == true, + onClick = { + navigationManager.navigate( + NavigationActions.BottomNavTabs.withinBottomNavTabs( + screen.navigationDestination, + navController.graph.findStartDestination().id + ) ) - ) - } - ) + } + ) + } } } } - } - ) { innerPadding -> - Box(modifier = Modifier.padding(innerPadding)) { - NfcRequired(nfcEnabled = nfcEnabled) { - AppNavigation(navController) + ) { innerPadding -> + Box(modifier = Modifier.padding(innerPadding)) { + NfcRequired(nfcEnabled = nfcEnabled) { + AppNavigation(navController) + } } } } diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt index de42a4c..4ef9397 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/model/AttendableTypeSelectionViewModel.kt @@ -3,12 +3,9 @@ package org.robojackets.apiary.attendance.model import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.skydoves.sandwich.StatusCode import com.skydoves.sandwich.message -import com.skydoves.sandwich.onError -import com.skydoves.sandwich.onException +import com.skydoves.sandwich.onFailure import com.skydoves.sandwich.onSuccess -import com.skydoves.sandwich.retrofit.statusCode import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -77,34 +74,16 @@ class AttendableTypeSelectionViewModel @Inject constructor( viewModelScope.launch { loadingUserPermissions.value = true - userRepository.getLoggedInUserInfo().onSuccess { - val missingPermissions = getMissingPermissions(this.data.user.allPermissions, requiredPermissions) - userMissingPermissions.value = missingPermissions - user.value = this.data.user - } - .onError { - when { - statusCode.code >= StatusCode.InternalServerError.code -> - Timber.e(this.message()) - else -> Timber.w(this.message()) - } - - permissionsCheckError.value = when { - this.statusCode.code >= StatusCode.InternalServerError.code -> - "A server error occurred while checking if you have permission to " + - "use this feature. Check your internet connection and try " + - "again, or ask in #it-helpdesk for assistance." - else -> - "An error occurred while checking if you have permission to use " + - "this feature. Check your internet connection and try again, or " + - "ask in #it-helpdesk for assistance." - } + userRepository.getLoggedInUserInfo() + .onSuccess { + val missingPermissions = + getMissingPermissions(this.data.user.allPermissions, requiredPermissions) + userMissingPermissions.value = missingPermissions + user.value = this.data.user } - .onException { - Timber.e(this.throwable) - permissionsCheckError.value = "An error occurred while checking if you have " + - "permission to use this feature. Check your internet connection and " + - "try again, or ask in #it-helpdesk for assistance." + .onFailure { + Timber.e(this.message()) + permissionsCheckError.value = "Error while checking permissions" } .also { loadingUserPermissions.value = false diff --git a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt index 0a644e6..4fbb6db 100644 --- a/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt +++ b/attendance/src/main/java/org/robojackets/apiary/attendance/ui/AttendableTypeSelectionScreen.kt @@ -46,7 +46,8 @@ fun AttendableTypeSelectionScreen( if (state.permissionsCheckError?.isNotEmpty() == true) { ErrorMessageWithRetry( message = state.permissionsCheckError ?: "An unknown error occurred", - onRetry = { viewModel.checkUserAttendanceAccess(forceRefresh = true) } + onRetry = { viewModel.checkUserAttendanceAccess(forceRefresh = true) }, + prioritizeRetryButton = true, ) return@ContentPadding } diff --git a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt index 54faec2..2c588ad 100644 --- a/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt +++ b/auth/src/main/java/org/robojackets/apiary/auth/ui/permissions/InsufficientPermissions.kt @@ -30,9 +30,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.robojackets.apiary.auth.model.Permission -import org.robojackets.apiary.auth.model.Permission.* +import org.robojackets.apiary.auth.model.Permission.CREATE_ATTENDANCE +import org.robojackets.apiary.auth.model.Permission.READ_TEAMS_HIDDEN +import org.robojackets.apiary.auth.model.Permission.READ_USERS import org.robojackets.apiary.base.ui.error.GoToItHelpdesk import org.robojackets.apiary.base.ui.icons.ErrorIcon +import org.robojackets.apiary.base.ui.theme.Apiary_MobileTheme import org.robojackets.apiary.base.ui.theme.danger import org.robojackets.apiary.base.ui.theme.success import org.robojackets.apiary.base.ui.util.ContentPadding @@ -55,18 +58,20 @@ fun InsufficientPermissions( ) { ErrorIcon(Modifier.size(90.dp), tint = danger) Text( - text = "$featureName unavailable", + text = "$featureName permissions required", + textAlign = TextAlign.Center, style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(top = 12.dp), ) Text( - text = "You don't have permission to use this feature. Please ask in #it-helpdesk for assistance.", + text = "You don't have permission to use this feature", modifier = Modifier.padding(top = 12.dp), textAlign = TextAlign.Center, ) Row(Modifier.padding(top = 18.dp)) { OutlinedButton(onClick = { onRefreshRequest() }) { - Text("Try again") + Text("Retry") } GoToItHelpdesk(modifier = Modifier.padding(start = 8.dp)) } @@ -74,7 +79,7 @@ fun InsufficientPermissions( TextButton(onClick = { showPermissionDetailsDialog = true }, Modifier.padding(top = 0.dp)) { - Text("More info") + Text("View details") } if (showPermissionDetailsDialog) { @@ -157,12 +162,14 @@ fun PermissionsListItem(hasPermission: Boolean, permissionName: String) { @Composable @Preview fun InsufficientPermissionsPreview() { - ContentPadding { - InsufficientPermissions( - featureName = "Attendance", - onRefreshRequest = {}, - missingPermissions = listOf(READ_TEAMS_HIDDEN), - requiredPermissions = listOf(CREATE_ATTENDANCE, READ_USERS, READ_TEAMS_HIDDEN), - ) + Apiary_MobileTheme { + ContentPadding { + InsufficientPermissions( + featureName = "Attendance", + onRefreshRequest = {}, + missingPermissions = listOf(READ_TEAMS_HIDDEN), + requiredPermissions = listOf(CREATE_ATTENDANCE, READ_USERS, READ_TEAMS_HIDDEN), + ) + } } } diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt b/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt index c8e9f2a..f57d794 100644 --- a/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt +++ b/base/src/main/java/org/robojackets/apiary/base/ui/error/ErrorMessage.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -17,20 +18,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext 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 org.robojackets.apiary.base.ui.icons.ErrorIcon +import org.robojackets.apiary.base.ui.theme.Apiary_MobileTheme import org.robojackets.apiary.base.ui.theme.danger @Composable fun ErrorMessageWithRetry( - message: String, + message: String? = null, onRetry: () -> Unit, - showHelpButton: Boolean = true, - retryButton: @Composable () -> Unit = { - OutlinedButton(onClick = onRetry) { - Text("Retry") - } + title: String? = null, + prioritizeRetryButton: Boolean = true, + icon: @Composable () -> Unit = { + ErrorIcon(Modifier.size(90.dp), tint = danger) } ) { Column( @@ -40,32 +42,91 @@ fun ErrorMessageWithRetry( .fillMaxWidth() .fillMaxHeight() ) { - ErrorIcon(Modifier.size(90.dp), tint = danger) - - Text( - text = message, - modifier = Modifier.padding(top = 12.dp), - textAlign = TextAlign.Center, - ) + icon() + if (title?.isNotEmpty() == true) { + Text( + text = title, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp), + style = MaterialTheme.typography.headlineMedium, + ) + } + if (message?.isNotEmpty() == true) { + Text( + text = message, + modifier = Modifier.padding(top = 12.dp), + textAlign = TextAlign.Center, + ) + } Row(Modifier.padding(top = 12.dp)) { - retryButton() - if (showHelpButton) { - GoToItHelpdesk(modifier = Modifier.padding(start = 8.dp)) + if (prioritizeRetryButton) { + GoToItHelpdesk( + modifier = Modifier.padding(end = 8.dp), + useOutlinedButton = true + ) + Button(onClick = onRetry) { + Text("Retry") + } + } else { + OutlinedButton(onClick = onRetry) { + Text("Retry") + } + GoToItHelpdesk( + modifier = Modifier.padding(start = 8.dp) + ) } } } } @Composable -fun GoToItHelpdesk(modifier: Modifier) { +fun GoToItHelpdesk(modifier: Modifier, useOutlinedButton: Boolean = false) { val context = LocalContext.current - Button(onClick = { + fun onClick() { val slackDeepLink = "slack://channel?team=T033JPZLT&id=C29Q3D8K0" val intent = Intent(Intent.ACTION_VIEW, Uri.parse(slackDeepLink)) ContextCompat.startActivity(context, intent, null) - }, modifier) { - Text("Go to #it-helpdesk") + } + + val text = @Composable { Text("Go to #it-helpdesk") } + + when { + useOutlinedButton -> { + OutlinedButton(onClick = { onClick() }, modifier) { + text() + } + } + else -> { + Button(onClick = { onClick() }, modifier) { + text() + } + } + } +} + +@Preview +@Composable +fun ErrorMessageWithRetryPreview() { + Apiary_MobileTheme { + ErrorMessageWithRetry( + title = "Error while checking permissions", + onRetry = {}, + prioritizeRetryButton = true, + ) + } +} + +@Preview +@Composable +fun ErrorMessageWithRetryPrioritizeHelpPreview() { + Apiary_MobileTheme { + ErrorMessageWithRetry( + title = "Attendance unavailable", + message = "An error occurred while checking your permissions", + onRetry = {}, + prioritizeRetryButton = false, + ) } } diff --git a/base/src/main/java/org/robojackets/apiary/base/ui/snackbar/SnackbarController.kt b/base/src/main/java/org/robojackets/apiary/base/ui/snackbar/SnackbarController.kt new file mode 100644 index 0000000..226975b --- /dev/null +++ b/base/src/main/java/org/robojackets/apiary/base/ui/snackbar/SnackbarController.kt @@ -0,0 +1,133 @@ +package org.robojackets.apiary.base.ui.snackbar + +/** + * This file is from https://gist.github.com/krizzu/60b7ea7e7865e6495cbd9359f20c4b91 + * See also https://www.kborowy.com/blog/easy-compose-snackbar/ + */ + +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.staticCompositionLocalOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import timber.log.Timber +import kotlin.coroutines.EmptyCoroutineContext + +private val LocalSnackbarController = staticCompositionLocalOf { + SnackbarController( + host = SnackbarHostState(), + scope = CoroutineScope(EmptyCoroutineContext) + ) +} +private val channel = Channel(capacity = Int.MAX_VALUE) + +@Composable +fun SnackbarControllerProvider(content: @Composable (snackbarHost: SnackbarHostState) -> Unit) { + val snackHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + val snackController = remember(scope) { SnackbarController(snackHostState, scope) } + + DisposableEffect(snackController, scope) { + val job = scope.launch { + for (payload in channel) { + Timber.d("Snackbar message: $payload") + snackController.showMessage( + message = payload.message, + duration = payload.duration, + action = payload.action + ) + } + } + + onDispose { + Timber.d("Snackbar job was disposed") + job.cancel() + } + } + + CompositionLocalProvider(LocalSnackbarController provides snackController) { + content( + snackHostState + ) + } +} + +@Immutable +class SnackbarController( + private val host: SnackbarHostState, + private val scope: CoroutineScope, +) { + companion object { + val current + @Composable + @ReadOnlyComposable + get() = LocalSnackbarController.current + + fun showMessage( + message: String, + action: SnackbarAction? = null, + duration: SnackbarDuration = SnackbarDuration.Short, + ) { + Timber.d("snackbar showMessage") + if (channel.isClosedForSend) { + throw IllegalStateException("snackbar channel is closed") + } + + val result = channel.trySend( + SnackbarChannelMessage( + message = message, + duration = duration, + action = action + ) + ) + Timber.d("Snackbar message sent: success? ${result.isSuccess}") + } + } + + + fun showMessage( + message: String, + action: SnackbarAction? = null, + duration: SnackbarDuration = SnackbarDuration.Short, + ) { + Timber.d("snackbar show message with scope") + scope.launch { + Timber.d("snackbar show message with scope: message is $message") + /** + * note: uncomment this line if you want snackbar to be displayed immediately, + * rather than being enqueued and waiting [duration] * current_queue_size + */ + Timber.d(host.currentSnackbarData.toString()) +// host.currentSnackbarData?.dismiss() + val result = + host.showSnackbar( + message = message, + actionLabel = action?.title, + duration = duration + ) + + Timber.d("snackbar with scope: result is $result") + if (result == SnackbarResult.ActionPerformed) { + action?.onActionPress?.invoke() + } + } + } +} + +data class SnackbarChannelMessage( + val message: String, + val action: SnackbarAction?, + val duration: SnackbarDuration = SnackbarDuration.Short, +) + + +data class SnackbarAction(val title: String, val onActionPress: () -> Unit) \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index d6ece75..00c8162 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0") - classpath("com.android.tools.build:gradle:8.5.1") + classpath("com.android.tools.build:gradle:8.5.2") classpath("com.google.dagger:hilt-android-gradle-plugin:2.51.1") // This version needs to // match the version for other Hilt dependencies defined in Dependencies.kt classpath("com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1") @@ -34,7 +34,7 @@ plugins { id("io.gitlab.arturbosch.detekt").version("1.23.0") id("com.autonomousapps.dependency-analysis").version("1.21.0") id("com.github.ben-manes.versions").version("0.46.0") - id("com.android.library") version "8.5.1" apply false + id("com.android.library") version "8.5.2" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false } diff --git a/merchandise/src/main/java/org/robojackets/apiary/merchandise/model/MerchandiseViewModel.kt b/merchandise/src/main/java/org/robojackets/apiary/merchandise/model/MerchandiseViewModel.kt index 6b613ac..0e0be05 100644 --- a/merchandise/src/main/java/org/robojackets/apiary/merchandise/model/MerchandiseViewModel.kt +++ b/merchandise/src/main/java/org/robojackets/apiary/merchandise/model/MerchandiseViewModel.kt @@ -14,6 +14,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.robojackets.apiary.base.model.ApiErrorMessage import org.robojackets.apiary.base.ui.nfc.BuzzCardTap +import org.robojackets.apiary.base.ui.snackbar.SnackbarController import org.robojackets.apiary.merchandise.network.MerchandiseRepository import org.robojackets.apiary.navigation.NavigationActions import org.robojackets.apiary.navigation.NavigationManager @@ -91,11 +92,11 @@ class MerchandiseViewModel @Inject constructor( loadingMerchandiseItems.value = false }.onError { Timber.e(this.toString(), "Could not fetch merchandise items due to an error") - merchandiseItemsListError.value = "Could not load available merchandise items" + merchandiseItemsListError.value = "Failed to load merchandise items" loadingMerchandiseItems.value = false }.onException { Timber.e(this.throwable, "Could not fetch merchandise items due to an exception") - merchandiseItemsListError.value = "Could not load available merchandise items" + merchandiseItemsListError.value = "Failed to load merchandise items" loadingMerchandiseItems.value = false } } @@ -207,6 +208,7 @@ class MerchandiseViewModel @Inject constructor( error = null, user = this.data.user ) + SnackbarController.showMessage("Saved pickup for ${this.data.user.name}") }.onError { // `this.errorBody` can only be consumed once. If you add a log statement // including it, then the deserializeErrorBody call will fail @@ -217,11 +219,11 @@ class MerchandiseViewModel @Inject constructor( Timber.e(e, "Could not deserialize error body") } Timber.d("status: ${errorModel?.status}, message: ${errorModel?.message}") - error.value = errorModel?.message ?: "Unable to record merchandise distribution" + error.value = errorModel?.message ?: "Error recording merchandise distribution" screenState.value = MerchandiseDistributionScreenState.ShowDistributionErrorDialog }.onException { Timber.e(this.throwable, "Unable to record merchandise distribution due to an exception") - error.value = "Unable to record merchandise distribution" + error.value = "Error recording merchandise distribution" screenState.value = MerchandiseDistributionScreenState.ShowDistributionErrorDialog } } diff --git a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistribution.kt b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistribution.kt index 8f0d6e3..4e945ad 100644 --- a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistribution.kt +++ b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistribution.kt @@ -86,9 +86,6 @@ fun MerchandiseDistribution( ) } HorizontalDivider() - if (state.lastStorePickupStatus != null) { - // TODO: show toast - } Column( verticalArrangement = Arrangement.SpaceAround, diff --git a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistributionScreen.kt b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistributionScreen.kt index 7366eb2..ca48165 100644 --- a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistributionScreen.kt +++ b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseDistributionScreen.kt @@ -9,6 +9,7 @@ import org.robojackets.apiary.base.ui.error.ErrorMessageWithRetry import org.robojackets.apiary.base.ui.util.ContentPadding import org.robojackets.apiary.base.ui.util.LoadingSpinner import org.robojackets.apiary.merchandise.model.MerchandiseViewModel +import timber.log.Timber @Composable fun MerchandiseDistributionScreen( @@ -17,6 +18,7 @@ fun MerchandiseDistributionScreen( merchandiseItemId: Int, ) { LaunchedEffect(merchandiseItemId) { + Timber.d("Launched effect: $merchandiseItemId") viewModel.loadMerchandiseItems(selectedItemId = merchandiseItemId) } @@ -26,13 +28,14 @@ fun MerchandiseDistributionScreen( when { state.merchandiseItemsListError != null -> { ErrorMessageWithRetry( - message = "Unable to load details about the selected merchandise item", + title = "Failed to load merchandise item", onRetry = { viewModel.loadMerchandiseItems( - forceRefresh = true, - selectedItemId = merchandiseItemId - ) - }, + forceRefresh = true, + selectedItemId = merchandiseItemId + ) + }, + prioritizeRetryButton = true, ) } state.loadingMerchandiseItems || state.selectedItem == null -> LoadingSpinner() @@ -42,9 +45,11 @@ fun MerchandiseDistributionScreen( onBuzzcardTap = { viewModel.onBuzzCardTap(it) }, - onNavigateToMerchandiseIndex = { viewModel.navigateToMerchandiseIndex() }, onConfirmPickup = { viewModel.confirmPickup() }, onDismissPickupDialog = { viewModel.dismissPickupDialog() }, + onNavigateToMerchandiseIndex = { + viewModel.navigateToMerchandiseIndex() + }, ) } } diff --git a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseIndexScreen.kt b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseIndexScreen.kt index 05f539b..e6f53e7 100644 --- a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseIndexScreen.kt +++ b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseIndexScreen.kt @@ -28,11 +28,14 @@ fun MerchandiseIndexScreen( ContentPadding { Column { when { - state.merchandiseItemsListError != null -> ErrorMessageWithRetry( - message = state.merchandiseItemsListError - ?: "Unable to load merchandise items available for distribution", - onRetry = { viewModel.loadMerchandiseItems(forceRefresh = true) } - ) + state.merchandiseItemsListError != null -> + ErrorMessageWithRetry( + title = state.merchandiseItemsListError + ?: "Unable to load merchandise items available for distribution", + onRetry = { viewModel.loadMerchandiseItems(forceRefresh = true) }, + prioritizeRetryButton = true, + ) + state.merchandiseItems == null || state.loadingMerchandiseItems -> LoadingSpinner() else -> MerchandiseItemSelection( title = { diff --git a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseItemSelection.kt b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseItemSelection.kt index 2942044..c63c2e1 100644 --- a/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseItemSelection.kt +++ b/merchandise/src/main/java/org/robojackets/apiary/merchandise/ui/MerchandiseItemSelection.kt @@ -16,8 +16,9 @@ fun MerchandiseItemSelection( ) { when { items.isNullOrEmpty() -> ErrorMessageWithRetry( - message = "No distributable merchandise items found", + title = "No merchandise to distribute", onRetry = { onRefreshList() }, + prioritizeRetryButton = true, ) else -> ItemList( items = items,