M3 Changes#2
Conversation
Summary of ChangesHello @webianks, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the application by introducing robust user authentication via Firebase, allowing users to sign in and manage their carts and order history more personally. It also refactors core data structures related to orders and improves the overall UI consistency and code organization, laying the groundwork for a more feature-rich and user-centric experience. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a significant and well-structured user authentication feature using Firebase Auth. The integration into the existing app, including the separation of guest and user carts, is handled effectively. The refactoring of order-related data and UI components to reflect authentication state is also a welcome improvement. I've identified one high-severity issue regarding potential cart data loss during login, along with a few medium-severity suggestions to improve the design, reduce code duplication, and fix a minor bug in the timer logic. Overall, this is a strong implementation of a complex feature.
| suspend fun transferGuestCartToUser(uid: String) { | ||
| val guestCartJson = dataStore.data.first()[stringPreferencesKey(GUEST_CART_KEY)] | ||
| if (!guestCartJson.isNullOrEmpty()) { | ||
| val guestCart = json.decodeFromString<List<CartLine>>(guestCartJson) | ||
| if (guestCart.isNotEmpty()) { | ||
| dataStore.edit { | ||
| it[stringPreferencesKey(uid)] = json.encodeToString(guestCart) | ||
| it.remove(stringPreferencesKey(GUEST_CART_KEY)) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of transferGuestCartToUser overwrites the user's existing cart with the guest cart. If a user logs in and already has items in their cart from a previous session, those items will be lost. The guest cart should be merged with the user's existing cart instead of replacing it to prevent data loss.
suspend fun transferGuestCartToUser(uid: String) {
val guestCartJson = dataStore.data.first()[stringPreferencesKey(GUEST_CART_KEY)]
if (guestCartJson.isNullOrEmpty()) return
val guestCart = json.decodeFromString<List<CartLine>>(guestCartJson)
if (guestCart.isEmpty()) return
dataStore.edit { preferences ->
val userCartKey = stringPreferencesKey(uid)
val userCartJson = preferences[userCartKey]
val userCart = if (userCartJson.isNullOrEmpty()) {
emptyList()
} else {
json.decodeFromString<List<CartLine>>(userCartJson)
}
// Merge guest cart into user cart
val mergedCart = userCart.toMutableList()
guestCart.forEach { guestLine ->
val existingLineIndex = mergedCart.indexOfFirst { it.identityKey() == guestLine.identityKey() }
if (existingLineIndex != -1) {
val existingLine = mergedCart[existingLineIndex]
mergedCart[existingLineIndex] = existingLine.copy(quantity = existingLine.quantity + guestLine.quantity)
} else {
mergedCart.add(guestLine)
}
}
preferences[userCartKey] = json.encodeToString(mergedCart)
preferences.remove(stringPreferencesKey(GUEST_CART_KEY))
}
}| val cartRepository = cartRepo(context) as DataStoreCartRepository | ||
| return AuthViewModel(FirebaseAuth.getInstance(), cartRepository) as T |
There was a problem hiding this comment.
Casting cartRepo(context) to the concrete class DataStoreCartRepository breaks the Dependency Inversion Principle, as AuthViewModel now depends on a concrete implementation instead of an abstraction. This makes the code less flexible and harder to test.
To improve this, you could:
- Add the required methods (
transferGuestCartToUser,clearUserCart) to theCartRepositoryinterface, possibly with empty default implementations. - Update
AuthViewModelto depend on theCartRepositoryinterface. - Remove the cast in this factory.
This will lead to a more robust and maintainable architecture.
| val isPhoneNumberPotentiallyValid = { number: String -> | ||
| if (!number.startsWith("+")) { | ||
| false | ||
| } else { | ||
| val digits = number.substring(1) | ||
| val countryCode = (3 downTo 1).asSequence() | ||
| .mapNotNull { len -> | ||
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | ||
| } | ||
| .firstOrNull { it in countryCallingCodes } | ||
|
|
||
| if (countryCode == null) { | ||
| false | ||
| } else { | ||
| val nationalNumber = digits.substring(countryCode.toString().length) | ||
| nationalNumber.length in 7..12 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| val validatePhoneNumber = { number: String -> | ||
| if (isPhoneNumberPotentiallyValid(number)) { | ||
| isError = false | ||
| errorMessage = "" | ||
| } else { | ||
| isError = true | ||
| if (!number.startsWith("+")) { | ||
| errorMessage = "Phone number must start with '+' (country code)." | ||
| } else { | ||
| val digits = number.substring(1) | ||
| val countryCode = (3 downTo 1).asSequence() | ||
| .mapNotNull { len -> | ||
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | ||
| } | ||
| .firstOrNull { it in countryCallingCodes } | ||
| if (countryCode == null) { | ||
| errorMessage = "Invalid country code." | ||
| } else { | ||
| errorMessage = "Please enter a valid number of digits (7-12)." | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The logic for validating a phone number is duplicated within the isPhoneNumberPotentiallyValid and validatePhoneNumber lambdas. This makes the code harder to maintain, as any change to the validation rules needs to be applied in multiple places. This logic is also similar to what's in PhoneNumberVisualTransformation.
Consider extracting the core validation logic into a single, reusable top-level function. This function could be used by both lambdas to reduce duplication and improve clarity.
| timerJob?.cancel() | ||
| timerJob = viewModelScope.launch { | ||
| flow { | ||
| for (i in 60 downTo 0) { |
There was a problem hiding this comment.
The timer loop for (i in 60 downTo 0) runs for 61 seconds (from 60 to 0 inclusive). Standard countdown timers are usually exclusive of the top value (e.g., a 60-second timer counts from 59 down to 0). This should probably be for (i in 59 downTo 0) to implement a correct 60-second timer.
| for (i in 60 downTo 0) { | |
| for (i in 59 downTo 0) { |
There was a problem hiding this comment.
Pull Request Overview
This pull request introduces user authentication via Firebase Phone Auth and refactors cart management to support user-specific carts with seamless guest-to-user cart migration. The changes also include order history functionality, UI component improvements, and data model restructuring.
Key Changes:
- Implemented Firebase Phone Authentication with OTP verification, including phone number validation and visual formatting
- Refactored cart repository to store separate carts for authenticated users and guests, with cart transfer capability upon sign-in
- Introduced new Order data models and refactored OrdersRepository interface with updated implementations for order history display
Reviewed Changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
app/build.gradle.kts |
Added Firebase Auth dependency |
gradle/libs.versions.toml |
Added Firebase Auth library reference |
AppNavigation.kt |
Integrated AuthViewModel and added auth route to navigation |
GenericCoreModule.kt |
Added AuthViewModelFactory and updated cart repository with Firebase Auth instance |
AuthViewModel.kt |
New ViewModel managing phone authentication state, OTP verification, and cart transfer |
AuthScreen.kt |
New authentication UI with phone input, OTP verification, and custom phone number formatting |
HomeScreen.kt |
Added authentication state handling, logout dialog, and account click actions |
HistoryScreen.kt |
Refactored to show order history for authenticated users with responsive grid layout |
HistoryViewModel.kt |
Converted to proper ViewModel with StateFlow-based UI state management |
DataStoreCartRepository.kt |
Refactored to support user-specific carts with Firebase Auth integration and cart transfer logic |
Order.kt |
New data classes for Order and OrderItem |
OrdersRepository.kt |
Updated interface method name from history() to getOrderHistory() |
FakeOrdersRepository.kt |
New implementation providing sample order data for testing/preview |
PlaceholderOrdersRepository.kt |
Updated to match new repository interface |
OrderHistoryCard.kt |
New composable for displaying order information with status badges |
NavBars.kt |
Added account/logout icon button with authentication state awareness |
Dialogs.kt |
New logout confirmation dialog component |
Buttons.kt |
Refactored PrimaryGradientButton with improved sizing control and disabled state support |
BottomBar.kt |
Fixed parameter name to use buttonModifier instead of modifier |
Cards.kt |
Updated import paths for sample data |
Type.kt |
Added Label3Medium text style for smaller labels |
Color.kt |
Added new color values for warnings, success states, and transparency variants |
SampleData.kt |
Moved from ui/screens package to data package |
CountryCallingCodes.kt |
New utility file containing valid country calling codes for phone validation |
ViewModelFactory.kt |
New factory for creating HistoryViewModel with FakeOrdersRepository |
ic_user.xml |
New user icon drawable |
ic_logout.xml |
New logout icon drawable |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| val isPhoneNumberPotentiallyValid = { number: String -> | ||
| if (!number.startsWith("+")) { | ||
| false | ||
| } else { | ||
| val digits = number.substring(1) | ||
| val countryCode = (3 downTo 1).asSequence() | ||
| .mapNotNull { len -> | ||
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | ||
| } | ||
| .firstOrNull { it in countryCallingCodes } | ||
|
|
||
| if (countryCode == null) { | ||
| false | ||
| } else { | ||
| val nationalNumber = digits.substring(countryCode.toString().length) | ||
| nationalNumber.length in 7..12 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| val validatePhoneNumber = { number: String -> | ||
| if (isPhoneNumberPotentiallyValid(number)) { | ||
| isError = false | ||
| errorMessage = "" | ||
| } else { | ||
| isError = true | ||
| if (!number.startsWith("+")) { | ||
| errorMessage = "Phone number must start with '+' (country code)." | ||
| } else { | ||
| val digits = number.substring(1) | ||
| val countryCode = (3 downTo 1).asSequence() | ||
| .mapNotNull { len -> | ||
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | ||
| } | ||
| .firstOrNull { it in countryCallingCodes } | ||
| if (countryCode == null) { | ||
| errorMessage = "Invalid country code." | ||
| } else { | ||
| errorMessage = "Please enter a valid number of digits (7-12)." | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The phone number validation logic is duplicated between the validation lambda at line 249-267 and the validatePhoneNumber lambda at line 269-291. This code duplication makes the implementation harder to maintain.
Consider extracting this into a single reusable function to avoid inconsistencies if the validation logic needs to be updated.
| val isPhoneNumberPotentiallyValid = { number: String -> | |
| if (!number.startsWith("+")) { | |
| false | |
| } else { | |
| val digits = number.substring(1) | |
| val countryCode = (3 downTo 1).asSequence() | |
| .mapNotNull { len -> | |
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | |
| } | |
| .firstOrNull { it in countryCallingCodes } | |
| if (countryCode == null) { | |
| false | |
| } else { | |
| val nationalNumber = digits.substring(countryCode.toString().length) | |
| nationalNumber.length in 7..12 | |
| } | |
| } | |
| } | |
| val validatePhoneNumber = { number: String -> | |
| if (isPhoneNumberPotentiallyValid(number)) { | |
| isError = false | |
| errorMessage = "" | |
| } else { | |
| isError = true | |
| if (!number.startsWith("+")) { | |
| errorMessage = "Phone number must start with '+' (country code)." | |
| } else { | |
| val digits = number.substring(1) | |
| val countryCode = (3 downTo 1).asSequence() | |
| .mapNotNull { len -> | |
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | |
| } | |
| .firstOrNull { it in countryCallingCodes } | |
| if (countryCode == null) { | |
| errorMessage = "Invalid country code." | |
| } else { | |
| errorMessage = "Please enter a valid number of digits (7-12)." | |
| } | |
| } | |
| } | |
| data class PhoneNumberValidationResult(val isValid: Boolean, val errorMessage: String = "") | |
| fun getPhoneNumberValidationResult(number: String): PhoneNumberValidationResult { | |
| if (!number.startsWith("+")) { | |
| return PhoneNumberValidationResult( | |
| isValid = false, | |
| errorMessage = "Phone number must start with '+' (country code)." | |
| ) | |
| } | |
| val digits = number.substring(1) | |
| val countryCode = (3 downTo 1).asSequence() | |
| .mapNotNull { len -> | |
| digits.takeIf { it.length >= len }?.substring(0, len)?.toIntOrNull() | |
| } | |
| .firstOrNull { it in countryCallingCodes } | |
| if (countryCode == null) { | |
| return PhoneNumberValidationResult( | |
| isValid = false, | |
| errorMessage = "Invalid country code." | |
| ) | |
| } | |
| val nationalNumber = digits.substring(countryCode.toString().length) | |
| if (nationalNumber.length in 7..12) { | |
| return PhoneNumberValidationResult(isValid = true) | |
| } else { | |
| return PhoneNumberValidationResult( | |
| isValid = false, | |
| errorMessage = "Please enter a valid number of digits (7-12)." | |
| ) | |
| } | |
| } | |
| val isPhoneNumberPotentiallyValid = { number: String -> | |
| getPhoneNumberValidationResult(number).isValid | |
| } | |
| val validatePhoneNumber = { number: String -> | |
| val result = getPhoneNumberValidationResult(number) | |
| isError = !result.isValid | |
| errorMessage = result.errorMessage |
|
|
||
| private fun FirebaseAuth.authStateFlow(): Flow<com.google.firebase.auth.FirebaseUser?> = kotlinx.coroutines.flow.callbackFlow { | ||
| val listener = FirebaseAuth.AuthStateListener { auth -> | ||
| trySend(auth.currentUser).isSuccess |
There was a problem hiding this comment.
The authStateFlow() extension function uses trySend() but doesn't handle the case when the send fails. The result of trySend() returns a ChannelResult, and while you're checking .isSuccess, the boolean result is not used.
Consider logging failed sends or handling them appropriately:
val listener = FirebaseAuth.AuthStateListener { auth ->
val result = trySend(auth.currentUser)
if (!result.isSuccess) {
// Log or handle the failure
}
}| private fun startTimer() { | ||
| timerJob?.cancel() | ||
| timerJob = viewModelScope.launch { | ||
| flow { | ||
| for (i in 60 downTo 0) { | ||
| emit(i) | ||
| delay(1000) | ||
| } | ||
| }.collect { | ||
| _timerValue.value = it |
There was a problem hiding this comment.
The timer implementation uses a countdown from 60 to 0, but the timeout in PhoneAuthOptions is also set to 60 seconds. There's a potential race condition where the timer might reach 0 slightly before or after the actual Firebase timeout expires, leading to inconsistent user experience.
Consider synchronizing the timer with Firebase's actual timeout callback or adding a small buffer.
| private fun startTimer() { | |
| timerJob?.cancel() | |
| timerJob = viewModelScope.launch { | |
| flow { | |
| for (i in 60 downTo 0) { | |
| emit(i) | |
| delay(1000) | |
| } | |
| }.collect { | |
| _timerValue.value = it | |
| private fun startTimer(timeoutSeconds: Int = 60) { | |
| timerJob?.cancel() | |
| val startTime = System.currentTimeMillis() | |
| val endTime = startTime + timeoutSeconds * 1000L | |
| timerJob = viewModelScope.launch { | |
| while (true) { | |
| val currentTime = System.currentTimeMillis() | |
| val remaining = ((endTime - currentTime) / 1000).toInt() | |
| if (remaining >= 0) { | |
| _timerValue.value = remaining | |
| delay(1000) | |
| } else { | |
| _timerValue.value = 0 | |
| break | |
| } |
| /*Order( | ||
| orderId = "12344", | ||
| date = "September 24, 10:05", | ||
| items = listOf( | ||
| OrderItem(name = "Margherita", quantity = 1), | ||
| ), | ||
| status = "Canceled", | ||
| totalAmount = 8.99 | ||
| )*/ |
There was a problem hiding this comment.
The commented-out code for a canceled order should either be removed or properly documented with a TODO/FIXME comment explaining why it's commented out and when it should be uncommented.
Leaving commented code without explanation makes the codebase harder to maintain.
| data class Order( | ||
| val orderId: String, | ||
| val date: String, | ||
| val items: List<OrderItem>, | ||
| val status: String, | ||
| val totalAmount: Double | ||
| ) | ||
|
|
There was a problem hiding this comment.
The status field in the Order data class is a plain String, which allows any arbitrary value and makes the code prone to errors. The same issue exists with the date field being a formatted string rather than a structured type.
Consider using more appropriate types:
status: Use an enum or sealed classdate: Use a proper date/time type (e.g.,LocalDateTimeorInstant) and format it only for display
| data class Order( | |
| val orderId: String, | |
| val date: String, | |
| val items: List<OrderItem>, | |
| val status: String, | |
| val totalAmount: Double | |
| ) | |
| import java.time.LocalDateTime | |
| data class Order( | |
| val orderId: String, | |
| val date: LocalDateTime, | |
| val items: List<OrderItem>, | |
| val status: OrderStatus, | |
| val totalAmount: Double | |
| ) | |
| enum class OrderStatus { | |
| PENDING, | |
| CONFIRMED, | |
| DELIVERED, | |
| CANCELLED | |
| } |
| @Composable | ||
| fun OrderStatus(status: String) { | ||
| val backgroundColor = when (status) { | ||
| "In Progress" -> Warning | ||
| "Completed" -> Success | ||
| "Canceled" -> Color.Red | ||
| else -> Color.Gray | ||
| } | ||
| Text( | ||
| text = status, | ||
| color = MaterialTheme.colorScheme.onPrimary, | ||
| modifier = Modifier | ||
| .clip(RoundedCornerShape(100.dp)) | ||
| .background(backgroundColor) | ||
| .padding(horizontal = 8.dp, vertical = 4.dp), | ||
| style = AppTextStyles.Label3Medium | ||
| ) | ||
| } |
There was a problem hiding this comment.
The OrderStatus composable uses string matching to determine the background color, which is fragile and not type-safe. If the status string doesn't match exactly (e.g., case sensitivity, typos), it falls back to Color.Gray.
Consider using an enum or sealed class for order status:
sealed class OrderStatus(val displayName: String, val color: Color) {
object InProgress : OrderStatus("In Progress", Warning)
object Completed : OrderStatus("Completed", Success)
object Canceled : OrderStatus("Canceled", Color.Red)
}| verifyPhoneNumber(activity, phoneNumber, token) | ||
| } ?: run { | ||
| _authState.value = | ||
| AuthState.AuthError("Can't resend code, please try again from the beginning.") |
There was a problem hiding this comment.
The error message "Can't resend code, please try again from the beginning." could be clearer. Users might not understand what "from the beginning" means in this context.
Consider a more user-friendly message:
"Unable to resend code. Please go back and request a new verification code."| AuthState.AuthError("Can't resend code, please try again from the beginning.") | |
| AuthState.AuthError("Unable to resend code. Please go back and request a new verification code.") |
| /* CHANGED: Box no longer forces any size (no fillMaxWidth/height here). | ||
| It wraps its content so the Button decides the size. */ | ||
| Box( | ||
| modifier = modifier | ||
| .dropShadow( | ||
| shape = RoundedCornerShape(100.dp), | ||
| shadow = Shadow( | ||
| radius = 6.dp, | ||
| spread = 0.dp, | ||
| offset = DpOffset(0.dp, (4).dp), | ||
| alpha = 0.25f, | ||
| color = PrimaryGradientEnd | ||
| ) | ||
| ) | ||
| .clip(RoundedCornerShape(100.dp)) | ||
| .then(shadowModifier) // shadow outside the pill | ||
| .clip(CircleShape) | ||
| .background(if (enabled) gradient else SolidColor(TextPrimary8)) | ||
| .padding(outerPadding) | ||
| // NOTE: Do NOT set .fillMaxWidth() or .height(...) on the Box here. | ||
| // The Button inside will determine sizing. | ||
| ) { | ||
| Button( | ||
| modifier = modifier.background(gradient), | ||
| colors = ButtonDefaults.buttonColors(containerColor = Color.Transparent), | ||
| onClick = { onClick() }, | ||
| onClick = onClick, | ||
| enabled = enabled, | ||
|
|
||
| /* CHANGED: Button modifier uses buttonModifier provided by caller and ensures sensible defaults | ||
| with defaultMinSize(minWidth, minHeight). This makes the Button decide size; Box wraps to it. */ | ||
| modifier = buttonModifier.defaultMinSize(minWidth = minWidth, minHeight = minHeight), | ||
|
|
||
| shape = CircleShape, | ||
| colors = ButtonDefaults.buttonColors( | ||
| containerColor = Color.Transparent, | ||
| disabledContainerColor = Color.Transparent | ||
| ), | ||
|
|
||
| /* CHANGED: reasonable horizontal padding so the pill looks correct */ | ||
| contentPadding = PaddingValues(horizontal = 24.dp, vertical = 0.dp), |
There was a problem hiding this comment.
Multiple inline comments starting with "CHANGED:" are present throughout the file (lines 40, 61-62, 69, 76-77, 86-87). These appear to be code review or development notes that should be removed before merging.
Consider removing all these "CHANGED:" comments to maintain clean production code.
| onPhoneNumberEntered = { phoneNumber -> | ||
| activity?.let { | ||
| viewModel.sendOtp( | ||
| phoneNumber, | ||
| it | ||
| ) | ||
| } | ||
| }, | ||
| onOtpEntered = { otp -> viewModel.verifyOtp(otp) }, | ||
| onResendCode = { phoneNumber -> | ||
| activity?.let { | ||
| viewModel.resendCode( | ||
| phoneNumber, | ||
| it | ||
| ) | ||
| } | ||
| }, |
There was a problem hiding this comment.
Missing null safety check for activity. While the current code uses activity?.let, if the activity is null, the OTP send/resend operations will silently fail without providing any user feedback.
Consider showing an error message to the user when activity is null:
activity?.let {
viewModel.sendOtp(phoneNumber, it)
} ?: run {
// Show error: "Unable to send OTP, please try again"
}| false | ||
| } else { | ||
| val nationalNumber = digits.substring(countryCode.toString().length) | ||
| nationalNumber.length in 7..12 |
There was a problem hiding this comment.
The phone number length validation accepts 7-12 digits, but this range may not be accurate for all countries. Some countries have phone numbers outside this range (e.g., shorter or longer). This could prevent valid phone numbers from being entered.
Consider researching and using more accurate length ranges per country code, or widening the acceptable range to be more permissive.
This pull request introduces user authentication integration and improves cart management by associating carts with user accounts using Firebase Auth. It also refactors order-related data and repositories, and restructures sample data files. The most significant changes are grouped below.
Authentication and Cart Management Integration:
AuthViewModelto screens and navigation. (app/build.gradle.kts,AppNavigation.kt,GenericCoreModule.kt) [1] [2] [3] [4] [5] [6] [7] [8]DataStoreCartRepositoryto store cart data separately for authenticated users and guests, using Firebase Auth state. Added methods to transfer guest carts to user accounts and clear user carts. (DataStoreCartRepository.kt) [1] [2] [3] [4] [5]Order Data and Repository Refactor:
OrderandOrderItemdata classes to represent order details. (Order.kt)OrdersRepositoryinterface and its implementations to use the newOrderdata class and updated method names. (OrdersRepository.kt,PlaceholderOrdersRepository.kt,FakeOrdersRepository.kt) [1] [2] [3]ViewModelFactoryfor injecting a fake orders repository intoHistoryViewModelfor testing or preview purposes. (ViewModelFactory.kt)Sample Data and Utility Additions:
ui/screens/SampleData.kttodata/SampleData.ktfor better separation of concerns. (SampleData.kt) [1] [2]CountryCallingCodes.kt)UI Component Fixes:
AddToCartBottomBarto correctly pass the button modifier. (BottomBar.kt)Buttons.ktfor UI consistency. (Buttons.kt)