From 6169aaa7ebdb8f6492287408087b0ea500377a5d Mon Sep 17 00:00:00 2001 From: moheeeetgupta Date: Mon, 19 May 2025 15:03:07 +0530 Subject: [PATCH] Test Commit --- .../samples/apps/nowinandroid/MainActivity.kt | 705 ++++++++++++++++++ .../apps/nowinandroid/ReturnOrderScreen.kt | 475 ++++++++++++ .../apps/nowinandroid/ReturnOrderScreenV10.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV11.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV12.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV2.kt | 475 ++++++++++++ .../apps/nowinandroid/ReturnOrderScreenV3.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV4.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV5.kt | 447 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV6.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV7.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV8.kt | 446 +++++++++++ .../apps/nowinandroid/ReturnOrderScreenV9.kt | 446 +++++++++++ app/src/main/res/drawable/ic_card_icon.xml | 3 + 14 files changed, 6119 insertions(+) create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreen.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV10.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV11.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV12.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV2.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV3.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV4.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV5.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV6.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV7.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV8.kt create mode 100644 app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV9.kt create mode 100644 app/src/main/res/drawable/ic_card_icon.xml diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt index ecc23d80e..08ab272a0 100644 --- a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/MainActivity.kt @@ -16,16 +16,49 @@ package com.google.samples.apps.nowinandroid +import android.annotation.SuppressLint import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.paddingFromBaseline +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Spa +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.res.stringResource +import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -151,6 +184,7 @@ class MainActivity : ComponentActivity() { disableDynamicTheming = themeSettings.disableDynamicTheming, ) { NiaApp(appState) + MySootheAppPortrait() } } } @@ -188,3 +222,674 @@ data class ThemeSettings( val androidTheme: Boolean, val disableDynamicTheming: Boolean, ) +@Composable +fun HomeScreen(modifier: Modifier = Modifier) { + Column( + modifier + .verticalScroll(rememberScrollState()) + ) { + Spacer(Modifier.height(16.dp)) + SearchBar(Modifier.padding(horizontal = 16.dp)) + HomeSection(title ="Align Your Body") { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + // This is a simplified helper. For a production app, + // you'd use a robust number-to-words utility or a pre-generated list. + fun numberToWords(n: Int): String { + val units = listOf("", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", + "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen") + val tens = listOf("", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety") + + return when { + n < 0 -> "Minus " + numberToWords(-n) + n == 0 -> "Zero" // Though the loop is 1..100 + n < 20 -> units[n] + n < 100 -> { + val tenPart = tens[n / 10] + val unitPart = units[n % 10] + if (unitPart.isNotEmpty()) "$tenPart $unitPart" else tenPart + } + n == 100 -> "One Hundred" + else -> n.toString() // Fallback for numbers > 100, though not needed for this loop + } + } + + for (i in 1..100) { + Text( + text = numberToWords(i), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + } + } + HomeSection(title = "Favorite Collections") { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + // This is a simplified helper. For a production app, + // you'd use a robust number-to-words utility or a pre-generated list. + fun numberToWords(n: Int): String { + val units = listOf("", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", + "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen") + val tens = listOf("", "", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety") + + return when { + n < 0 -> "Minus " + numberToWords(-n) + n == 0 -> "Zero" // Though the loop is 1..100 + n < 20 -> units[n] + n < 100 -> { + val tenPart = tens[n / 10] + val unitPart = units[n % 10] + if (unitPart.isNotEmpty()) "$tenPart $unitPart" else tenPart + } + n == 100 -> "One Hundred" + else -> n.toString() // Fallback for numbers > 100, though not needed for this loop + } + } + + for (i in 1..100) { + Text( + text = numberToWords(i), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + } + } + Spacer(Modifier.height(16.dp)) + } +} + +@Composable +fun HomeSection( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Column(modifier) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .padding(horizontal = 16.dp) + .paddingFromBaseline(top = 40.dp, bottom = 16.dp) + ) + content() + } +} +@SuppressLint("DesignSystem") +@Composable +fun SearchBar( + modifier: Modifier = Modifier +) { + TextField( + value = "", + onValueChange = {}, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + }, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + focusedContainerColor = MaterialTheme.colorScheme.surface + ), + placeholder = { + Text("Search") + }, + modifier = modifier + .fillMaxWidth() + .heightIn(min = 56.dp) + ) +} +@SuppressLint("DesignSystem") +@Composable +private fun SootheBottomNavigation(modifier: Modifier = Modifier) { + NavigationBar( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = modifier + ) { + NavigationBarItem( + icon = { + Icon( + imageVector = Icons.Default.Spa, + contentDescription = null + ) + }, + label = { + Text("Home") + }, + selected = true, + onClick = {} + ) + NavigationBarItem( + icon = { + Icon( + imageVector = Icons.Default.AccountCircle, + contentDescription = null + ) + }, + label = { + Text("Profile") + }, + selected = false, + onClick = {} + ) + } +} + + +@SuppressLint("DesignSystem") +@Composable +fun MySootheAppPortrait() { + NiaTheme { + var tabIndex by remember { mutableStateOf(0) } + val tabs = listOf( + "HOME", "RETURN ORDER", "RETURN ORDER V2", + "RETURN ORDER V3", "RETURN ORDER V4", "RETURN ORDER V5", + "RETURN ORDER V6", "RETURN ORDER V7", "RETURN ORDER V8", + "RETURN ORDER V9", "RETURN ORDER V10", "RETURN ORDER V11", + "RETURN ORDER V12" + ) + + Scaffold( + bottomBar = { SootheBottomNavigation() } + ) { padding -> + Column( + modifier = Modifier.padding(padding) + ) { + TabRow(selectedTabIndex = tabIndex) { + tabs.forEachIndexed { index, title -> + Tab( + text = { Text(title) }, + selected = tabIndex == index, + onClick = { tabIndex = index } + ) + } + } + when (tabIndex) { + 0 -> HomeScreen() + 1 -> { + val orderItem = OrderItem( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptions = listOf( + RefundOption( + id = RefundMethodType.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOption( + id = RefundMethodType.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val state = rememberReturnOrderState( + initialOrderItem = orderItem, + initialRefundOptions = refundOptions + ) + + ReturnOrderScreen( + state = state, + onNavigateBack = {} + ) + } + 2 -> { + val orderItemV2 = OrderItemV2( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV2 = listOf( + RefundOptionV2( + id = RefundMethodTypeV2.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV2( + id = RefundMethodTypeV2.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV2 = rememberReturnOrderStateV2( + initialOrderItem = orderItemV2, + initialRefundOptions = refundOptionsV2 + ) + + ReturnOrderScreenV2( + state = stateV2, + onNavigateBack = {} + ) + } + 3 -> { + val orderItemV3 = OrderItemV3( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV3 = listOf( + RefundOptionV3( + id = RefundMethodTypeV3.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV3( + id = RefundMethodTypeV3.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV3 = rememberReturnOrderStateV3( + initialOrderItem = orderItemV3, + initialRefundOptions = refundOptionsV3 + ) + + ReturnOrderScreenV3( + state = stateV3, + onNavigateBack = {} + ) + } + 4 -> { + val orderItemV4 = OrderItemV4( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV4 = listOf( + RefundOptionV4( + id = RefundMethodTypeV4.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV4( + id = RefundMethodTypeV4.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV4 = rememberReturnOrderStateV4( + initialOrderItem = orderItemV4, + initialRefundOptions = refundOptionsV4 + ) + + ReturnOrderScreenV4( + state = stateV4, + onNavigateBack = {} + ) + } + 5 -> { + val orderItemV5 = OrderItemV5( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV5 = listOf( + RefundOptionV5( + id = RefundMethodTypeV5.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV5( + id = RefundMethodTypeV5.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV5 = rememberReturnOrderStateV5( + initialOrderItem = orderItemV5, + initialRefundOptions = refundOptionsV5 + ) + + ReturnOrderScreenV5( + state = stateV5, + onNavigateBack = {} + ) + } + 6 -> { + val orderItemV6 = OrderItemV6( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV6 = listOf( + RefundOptionV6( + id = RefundMethodTypeV6.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV6( + id = RefundMethodTypeV6.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV6 = rememberReturnOrderStateV6( + initialOrderItem = orderItemV6, + initialRefundOptions = refundOptionsV6 + ) + + ReturnOrderScreenV6( + state = stateV6, + onNavigateBack = {} + ) + } + 7 -> { + val orderItemV7 = OrderItemV7( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV7 = listOf( + RefundOptionV7( + id = RefundMethodTypeV7.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV7( + id = RefundMethodTypeV7.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV7 = rememberReturnOrderStateV7( + initialOrderItem = orderItemV7, + initialRefundOptions = refundOptionsV7 + ) + + ReturnOrderScreenV7( + state = stateV7, + onNavigateBack = {} + ) + } + 8 -> { + val orderItemV8 = OrderItemV8( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV8 = listOf( + RefundOptionV8( + id = RefundMethodTypeV8.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV8( + id = RefundMethodTypeV8.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV8 = rememberReturnOrderStateV8( + initialOrderItem = orderItemV8, + initialRefundOptions = refundOptionsV8 + ) + + ReturnOrderScreenV8( + state = stateV8, + onNavigateBack = {} + ) + } + 9 -> { + val orderItemV9 = OrderItemV9( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV9 = listOf( + RefundOptionV9( + id = RefundMethodTypeV9.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV9( + id = RefundMethodTypeV9.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV9 = rememberReturnOrderStateV9( + initialOrderItem = orderItemV9, + initialRefundOptions = refundOptionsV9 + ) + + ReturnOrderScreenV9( + state = stateV9, + onNavigateBack = {} + ) + } + 10 -> { + val orderItemV10 = OrderItemV10( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV10 = listOf( + RefundOptionV10( + id = RefundMethodTypeV10.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV10( + id = RefundMethodTypeV10.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV10 = rememberReturnOrderStateV10( + initialOrderItem = orderItemV10, + initialRefundOptions = refundOptionsV10 + ) + + ReturnOrderScreenV10( + state = stateV10, + onNavigateBack = {} + ) + } + 11 -> { + val orderItemV11 = OrderItemV11( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV11 = listOf( + RefundOptionV11( + id = RefundMethodTypeV11.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV11( + id = RefundMethodTypeV11.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV11 = rememberReturnOrderStateV11( + initialOrderItem = orderItemV11, + initialRefundOptions = refundOptionsV11 + ) + + ReturnOrderScreenV11( + state = stateV11, + onNavigateBack = {} + ) + } + 12 -> { + val orderItemV12 = OrderItemV12( + id = "123", + name = "Floral Print Dress", + imageUrl = R.drawable.ic_card_icon, + price = 599, + size = "M", + quantity = 1 + ) + + val refundOptionsV12 = listOf( + RefundOptionV12( + id = RefundMethodTypeV12.MEESHO_APP, + title = "Meesho App", + subtitle = "Instant refund", + icon = R.drawable.ic_card_icon, + getAmount = 599, + extraAmount = 50, + isSelected = true + ), + RefundOptionV12( + id = RefundMethodTypeV12.BANK_UPI, + title = "Bank/UPI", + subtitle = "3-5 business days", + icon = R.drawable.ic_card_icon, + getAmount = 599, + isSelected = false + ) + ) + + val stateV12 = rememberReturnOrderStateV12( + initialOrderItem = orderItemV12, + initialRefundOptions = refundOptionsV12 + ) + + ReturnOrderScreenV12( + state = stateV12, + onNavigateBack = {} + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreen.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreen.kt new file mode 100644 index 000000000..36a8a4048 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreen.kt @@ -0,0 +1,475 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodType.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionState.Error +import com.google.samples.apps.nowinandroid.SubmissionState.Idle +import com.google.samples.apps.nowinandroid.SubmissionState.Loading +import com.google.samples.apps.nowinandroid.SubmissionState.Success + +data class OrderItem( + val id: String, + val name: String, + val imageUrl: Int, // Using Drawable Res ID for simplicity + val price: Int, + val size: String, + val quantity: Int +) + +// Representing different refund methods +enum class RefundMethodType { + MEESHO_APP, BANK_UPI +} + +data class RefundOption( + val id: RefundMethodType, + val title: String, + val subtitle: String? = null, + val icon: Int, // Drawable Res ID + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +// --- State Holder (Can be hoisted to a ViewModel in a real app) --- +// This manages the state of the screen +class ReturnOrderState( + initialOrderItem: OrderItem, + initialRefundOptions: List, + initialSelectedOption: RefundMethodType? = null // Start with no selection or pre-select +) { + var orderItem by mutableStateOf(initialOrderItem) + private set // Can only be modified internally or via specific functions + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodType) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + // Reset submission state if user changes selection after an error + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + // --- Simulate Network Call --- + // In a real app, launch a coroutine and call repository/usecase + // viewModelScope.launch { ... } + // For now, just simulate success/failure after a delay + // Handler(Looper.getMainLooper()).postDelayed({ + // submissionState = if (Random.nextBoolean()) SubmissionState.Success + // else SubmissionState.Error("Refund submission failed. Please try again.") + // }, 2000) + println("Submitting refund for method: $selectedRefundMethod") + // For preview, let's just set to Success immediately for simplicity + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderState( + initialOrderItem: OrderItem, + initialRefundOptions: List, + initialSelectedOption: RefundMethodType? = MEESHO_APP // Pre-select Meesho +): ReturnOrderState { + return remember { + ReturnOrderState( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) // Set initial selection state + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +// --- Sealed class for Submission State (more robust error/loading handling) --- +sealed class SubmissionState { + object Idle : SubmissionState() + object Loading : SubmissionState() + object Success : SubmissionState() + data class Error(val message: String) : SubmissionState() +} + + +// --- Main Screen Composable --- + +@Composable +fun ReturnOrderScreen( + state: ReturnOrderState, + onNavigateBack: () -> Unit, + // In a real app, submission would likely trigger a ViewModel function + // onSubmit: (RefundMethodType) -> Unit +) { + val snackbarHostState = SnackbarHostState() + + // Handle submission state changes (e.g., show Snackbar for errors) + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + // Optionally reset state after showing error + // state.resetSubmissionState() + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + // Potentially navigate away or show success screen + // onNavigateBack() // Example: Navigate back on success + } + else -> {} // Idle or Loading + } + } + + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButton( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) // Apply scaffold padding + .verticalScroll(rememberScrollState()) // Make content scrollable + .padding(bottom = 80.dp) // Add padding to avoid overlap with bottom bar + ) { + OrderItemSummary(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelection( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + // Show info box only when Meesho App refund is selected + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBox(onLearnMoreClick = { /* TODO: Handle Learn More click */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + + +@Composable +fun OrderItemSummary(item: OrderItem) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), // Replace with actual image loading + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelection( + options: List, + selectedMethod: RefundMethodType?, + onOptionSelected: (RefundMethodType) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) // Spacing between cards + ) { + options.forEach { option -> + RefundOptionCard( + option = option.copy(isSelected = option.id == selectedMethod), // Ensure isSelected reflects state + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) // Make cards share width equally + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCard( + option: RefundOption, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray // Purple when selected + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + val backgroundColor = Color.White // Or MaterialTheme.colors.surface + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) // Clip needed for click effect ripple + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { // Use Box to overlay the checkmark + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), // Ensure column takes width for alignment + horizontalAlignment = Alignment.Start // Align content to the start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, // Decorative icon + modifier = Modifier.size(28.dp), // Adjust size as needed + tint = Color.Unspecified // Use original icon colors if applicable + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider(modifier = Modifier.padding(vertical = 8.dp), color = Color.LightGray.copy(alpha = 0.5f)) // Subtle divider + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background( + Color(0xFFE6F9E6), + RoundedCornerShape(4.dp) + ) // Light green background + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), // Dark green text + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + // Add Spacer at the bottom if extraAmount is null to maintain some height consistency + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) // Adjust height to match the 'Extra' box approx + } + } + + // Selection Indicator (Checkmark) + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), // Purple checkmark + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) // White background behind check + ) + } + } + } +} + +@Composable +fun MeeshoInfoBox(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle(style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButton( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) // Padding around the button + .height(48.dp), // Standard button height + enabled = isEnabled && !isLoading, // Disable if not selected OR loading + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV10.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV10.kt new file mode 100644 index 000000000..6f0dc624d --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV10.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV10.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV10.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV10.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV10.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV10.Success + +data class OrderItemV10( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV10 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV10( + val id: RefundMethodTypeV10, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV10( + initialOrderItem: OrderItemV10, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV10? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV10) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV10( + initialOrderItem: OrderItemV10, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV10? = MEESHO_APP +): ReturnOrderStateV10 { + return remember { + ReturnOrderStateV10( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV10 { + object Idle : SubmissionStateV10() + object Loading : SubmissionStateV10() + object Success : SubmissionStateV10() + data class Error(val message: String) : SubmissionStateV10() +} + +@Composable +fun ReturnOrderScreenV10( + state: ReturnOrderStateV10, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV10( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV10(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV10( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV10(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV10(item: OrderItemV10) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV10( + options: List, + selectedMethod: RefundMethodTypeV10?, + onOptionSelected: (RefundMethodTypeV10) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV10( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV10( + option: RefundOptionV10, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV10(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV10( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV11.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV11.kt new file mode 100644 index 000000000..2e4739055 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV11.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV11.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV11.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV11.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV11.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV11.Success + +data class OrderItemV11( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV11 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV11( + val id: RefundMethodTypeV11, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV11( + initialOrderItem: OrderItemV11, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV11? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV11) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV11( + initialOrderItem: OrderItemV11, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV11? = MEESHO_APP +): ReturnOrderStateV11 { + return remember { + ReturnOrderStateV11( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV11 { + object Idle : SubmissionStateV11() + object Loading : SubmissionStateV11() + object Success : SubmissionStateV11() + data class Error(val message: String) : SubmissionStateV11() +} + +@Composable +fun ReturnOrderScreenV11( + state: ReturnOrderStateV11, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV11( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV11(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV11( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV11(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV11(item: OrderItemV11) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV11( + options: List, + selectedMethod: RefundMethodTypeV11?, + onOptionSelected: (RefundMethodTypeV11) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV11( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV11( + option: RefundOptionV11, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV11(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV11( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV12.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV12.kt new file mode 100644 index 000000000..bb06e00f1 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV12.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV12.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV12.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV12.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV12.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV12.Success + +data class OrderItemV12( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV12 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV12( + val id: RefundMethodTypeV12, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV12( + initialOrderItem: OrderItemV12, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV12? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV12) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV12( + initialOrderItem: OrderItemV12, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV12? = MEESHO_APP +): ReturnOrderStateV12 { + return remember { + ReturnOrderStateV12( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV12 { + object Idle : SubmissionStateV12() + object Loading : SubmissionStateV12() + object Success : SubmissionStateV12() + data class Error(val message: String) : SubmissionStateV12() +} + +@Composable +fun ReturnOrderScreenV12( + state: ReturnOrderStateV12, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV12( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV12(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV12( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV12(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV12(item: OrderItemV12) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV12( + options: List, + selectedMethod: RefundMethodTypeV12?, + onOptionSelected: (RefundMethodTypeV12) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV12( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV12( + option: RefundOptionV12, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV12(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV12( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV2.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV2.kt new file mode 100644 index 000000000..3a86b5c0f --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV2.kt @@ -0,0 +1,475 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV2.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV2.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV2.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV2.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV2.Success + +data class OrderItemV2( + val id: String, + val name: String, + val imageUrl: Int, // Using Drawable Res ID for simplicity + val price: Int, + val size: String, + val quantity: Int +) + +// Representing different refund methods +enum class RefundMethodTypeV2 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV2( + val id: RefundMethodTypeV2, + val title: String, + val subtitle: String? = null, + val icon: Int, // Drawable Res ID + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +// --- State Holder (Can be hoisted to a ViewModel in a real app) --- +// This manages the state of the screen +class ReturnOrderStateV2( + initialOrderItem: OrderItemV2, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV2? = null // Start with no selection or pre-select +) { + var orderItem by mutableStateOf(initialOrderItem) + private set // Can only be modified internally or via specific functions + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV2) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + // Reset submission state if user changes selection after an error + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + // --- Simulate Network Call --- + // In a real app, launch a coroutine and call repository/usecase + // viewModelScope.launch { ... } + // For now, just simulate success/failure after a delay + // Handler(Looper.getMainLooper()).postDelayed({ + // submissionState = if (Random.nextBoolean()) SubmissionStateV2.Success + // else SubmissionStateV2.Error("Refund submission failed. Please try again.") + // }, 2000) + println("Submitting refund for method: $selectedRefundMethod") + // For preview, let's just set to Success immediately for simplicity + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV2( + initialOrderItem: OrderItemV2, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV2? = MEESHO_APP // Pre-select Meesho +): ReturnOrderStateV2 { + return remember { + ReturnOrderStateV2( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) // Set initial selection state + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +// --- Sealed class for Submission State (more robust error/loading handling) --- +sealed class SubmissionStateV2 { + object Idle : SubmissionStateV2() + object Loading : SubmissionStateV2() + object Success : SubmissionStateV2() + data class Error(val message: String) : SubmissionStateV2() +} + + +// --- Main Screen Composable --- + +@Composable +fun ReturnOrderScreenV2( + state: ReturnOrderStateV2, + onNavigateBack: () -> Unit, + // In a real app, submission would likely trigger a ViewModel function + // onSubmit: (RefundMethodTypeV2) -> Unit +) { + val snackbarHostState = SnackbarHostState() + + // Handle submission state changes (e.g., show Snackbar for errors) + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + // Optionally reset state after showing error + // state.resetSubmissionState() + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + // Potentially navigate away or show success screen + // onNavigateBack() // Example: Navigate back on success + } + else -> {} // Idle or Loading + } + } + + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV2( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) // Apply scaffold padding + .verticalScroll(rememberScrollState()) // Make content scrollable + .padding(bottom = 80.dp) // Add padding to avoid overlap with bottom bar + ) { + OrderItemSummaryV2(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV2( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + // Show info box only when Meesho App refund is selected + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV2(onLearnMoreClick = { /* TODO: Handle Learn More click */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + + +@Composable +fun OrderItemSummaryV2(item: OrderItemV2) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), // Replace with actual image loading + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV2( + options: List, + selectedMethod: RefundMethodTypeV2?, + onOptionSelected: (RefundMethodTypeV2) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) // Spacing between cards + ) { + options.forEach { option -> + RefundOptionCardV2( + option = option.copy(isSelected = option.id == selectedMethod), // Ensure isSelected reflects state + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) // Make cards share width equally + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV2( + option: RefundOptionV2, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray // Purple when selected + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + val backgroundColor = Color.White // Or MaterialTheme.colors.surface + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) // Clip needed for click effect ripple + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { // Use Box to overlay the checkmark + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), // Ensure column takes width for alignment + horizontalAlignment = Alignment.Start // Align content to the start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, // Decorative icon + modifier = Modifier.size(28.dp), // Adjust size as needed + tint = Color.Unspecified // Use original icon colors if applicable + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider(modifier = Modifier.padding(vertical = 8.dp), color = Color.LightGray.copy(alpha = 0.5f)) // Subtle divider + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background( + Color(0xFFE6F9E6), + RoundedCornerShape(4.dp) + ) // Light green background + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), // Dark green text + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + // Add Spacer at the bottom if extraAmount is null to maintain some height consistency + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) // Adjust height to match the 'Extra' box approx + } + } + + // Selection Indicator (Checkmark) + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), // Purple checkmark + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) // White background behind check + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV2(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle(style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV2( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) // Padding around the button + .height(48.dp), // Standard button height + enabled = isEnabled && !isLoading, // Disable if not selected OR loading + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV3.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV3.kt new file mode 100644 index 000000000..ce6f25e42 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV3.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV3.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV3.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV3.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV3.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV3.Success + +data class OrderItemV3( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV3 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV3( + val id: RefundMethodTypeV3, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV3( + initialOrderItem: OrderItemV3, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV3? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV3) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV3( + initialOrderItem: OrderItemV3, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV3? = MEESHO_APP +): ReturnOrderStateV3 { + return remember { + ReturnOrderStateV3( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV3 { + object Idle : SubmissionStateV3() + object Loading : SubmissionStateV3() + object Success : SubmissionStateV3() + data class Error(val message: String) : SubmissionStateV3() +} + +@Composable +fun ReturnOrderScreenV3( + state: ReturnOrderStateV3, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV3( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV3(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV3( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV3(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV3(item: OrderItemV3) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV3( + options: List, + selectedMethod: RefundMethodTypeV3?, + onOptionSelected: (RefundMethodTypeV3) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV3( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV3( + option: RefundOptionV3, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV3(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV3( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV4.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV4.kt new file mode 100644 index 000000000..c57be58bd --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV4.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV4.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV4.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV4.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV4.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV4.Success + +data class OrderItemV4( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV4 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV4( + val id: RefundMethodTypeV4, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV4( + initialOrderItem: OrderItemV4, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV4? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV4) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV4( + initialOrderItem: OrderItemV4, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV4? = MEESHO_APP +): ReturnOrderStateV4 { + return remember { + ReturnOrderStateV4( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV4 { + object Idle : SubmissionStateV4() + object Loading : SubmissionStateV4() + object Success : SubmissionStateV4() + data class Error(val message: String) : SubmissionStateV4() +} + +@Composable +fun ReturnOrderScreenV4( + state: ReturnOrderStateV4, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV4( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV4(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV4( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV4(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV4(item: OrderItemV4) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV4( + options: List, + selectedMethod: RefundMethodTypeV4?, + onOptionSelected: (RefundMethodTypeV4) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV4( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV4( + option: RefundOptionV4, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV4(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV4( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV5.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV5.kt new file mode 100644 index 000000000..41c5618a0 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV5.kt @@ -0,0 +1,447 @@ +package com.google.samples.apps.nowinandroid + +// Same imports as V4... +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV5.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV5.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV5.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV5.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV5.Success + +data class OrderItemV5( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV5 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV5( + val id: RefundMethodTypeV5, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV5( + initialOrderItem: OrderItemV5, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV5? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV5) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV5( + initialOrderItem: OrderItemV5, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV5? = MEESHO_APP +): ReturnOrderStateV5 { + return remember { + ReturnOrderStateV5( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV5 { + object Idle : SubmissionStateV5() + object Loading : SubmissionStateV5() + object Success : SubmissionStateV5() + data class Error(val message: String) : SubmissionStateV5() +} + +@Composable +fun ReturnOrderScreenV5( + state: ReturnOrderStateV5, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV5( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV5(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV5( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV5(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV5(item: OrderItemV5) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV5( + options: List, + selectedMethod: RefundMethodTypeV5?, + onOptionSelected: (RefundMethodTypeV5) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV5( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV5( + option: RefundOptionV5, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV5(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV5( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV6.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV6.kt new file mode 100644 index 000000000..1416cde1f --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV6.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV6.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV6.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV6.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV6.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV6.Success + +data class OrderItemV6( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV6 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV6( + val id: RefundMethodTypeV6, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV6( + initialOrderItem: OrderItemV6, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV6? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV6) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV6( + initialOrderItem: OrderItemV6, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV6? = MEESHO_APP +): ReturnOrderStateV6 { + return remember { + ReturnOrderStateV6( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV6 { + object Idle : SubmissionStateV6() + object Loading : SubmissionStateV6() + object Success : SubmissionStateV6() + data class Error(val message: String) : SubmissionStateV6() +} + +@Composable +fun ReturnOrderScreenV6( + state: ReturnOrderStateV6, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV6( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV6(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV6( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV6(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV6(item: OrderItemV6) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV6( + options: List, + selectedMethod: RefundMethodTypeV6?, + onOptionSelected: (RefundMethodTypeV6) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV6( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV6( + option: RefundOptionV6, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV6(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV6( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV7.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV7.kt new file mode 100644 index 000000000..aefc66aaa --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV7.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV7.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV7.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV7.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV7.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV7.Success + +data class OrderItemV7( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV7 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV7( + val id: RefundMethodTypeV7, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV7( + initialOrderItem: OrderItemV7, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV7? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV7) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV7( + initialOrderItem: OrderItemV7, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV7? = MEESHO_APP +): ReturnOrderStateV7 { + return remember { + ReturnOrderStateV7( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV7 { + object Idle : SubmissionStateV7() + object Loading : SubmissionStateV7() + object Success : SubmissionStateV7() + data class Error(val message: String) : SubmissionStateV7() +} + +@Composable +fun ReturnOrderScreenV7( + state: ReturnOrderStateV7, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV7( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV7(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV7( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV7(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV7(item: OrderItemV7) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV7( + options: List, + selectedMethod: RefundMethodTypeV7?, + onOptionSelected: (RefundMethodTypeV7) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV7( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV7( + option: RefundOptionV7, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV7(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV7( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV8.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV8.kt new file mode 100644 index 000000000..c7a3ec273 --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV8.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV8.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV8.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV8.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV8.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV8.Success + +data class OrderItemV8( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV8 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV8( + val id: RefundMethodTypeV8, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV8( + initialOrderItem: OrderItemV8, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV8? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV8) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV8( + initialOrderItem: OrderItemV8, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV8? = MEESHO_APP +): ReturnOrderStateV8 { + return remember { + ReturnOrderStateV8( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV8 { + object Idle : SubmissionStateV8() + object Loading : SubmissionStateV8() + object Success : SubmissionStateV8() + data class Error(val message: String) : SubmissionStateV8() +} + +@Composable +fun ReturnOrderScreenV8( + state: ReturnOrderStateV8, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV8( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV8(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV8( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV8(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV8(item: OrderItemV8) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV8( + options: List, + selectedMethod: RefundMethodTypeV8?, + onOptionSelected: (RefundMethodTypeV8) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV8( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV8( + option: RefundOptionV8, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV8(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV8( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV9.kt b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV9.kt new file mode 100644 index 000000000..10acd090a --- /dev/null +++ b/app/src/main/kotlin/com/google/samples/apps/nowinandroid/ReturnOrderScreenV9.kt @@ -0,0 +1,446 @@ +package com.google.samples.apps.nowinandroid + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +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.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.google.samples.apps.nowinandroid.RefundMethodTypeV9.MEESHO_APP +import com.google.samples.apps.nowinandroid.SubmissionStateV9.Error +import com.google.samples.apps.nowinandroid.SubmissionStateV9.Idle +import com.google.samples.apps.nowinandroid.SubmissionStateV9.Loading +import com.google.samples.apps.nowinandroid.SubmissionStateV9.Success + +data class OrderItemV9( + val id: String, + val name: String, + val imageUrl: Int, + val price: Int, + val size: String, + val quantity: Int +) + +enum class RefundMethodTypeV9 { + MEESHO_APP, BANK_UPI +} + +data class RefundOptionV9( + val id: RefundMethodTypeV9, + val title: String, + val subtitle: String? = null, + val icon: Int, + val getAmount: Int, + val extraAmount: Int? = null, + val isSelected: Boolean = false +) + +class ReturnOrderStateV9( + initialOrderItem: OrderItemV9, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV9? = null +) { + var orderItem by mutableStateOf(initialOrderItem) + private set + + var refundOptions by mutableStateOf(initialRefundOptions) + private set + + var selectedRefundMethod by mutableStateOf(initialSelectedOption) + private set + + var submissionState by mutableStateOf(Idle) + private set + + fun selectRefundMethod(type: RefundMethodTypeV9) { + selectedRefundMethod = type + refundOptions = refundOptions.map { + it.copy(isSelected = it.id == type) + } + if (submissionState is Error) { + submissionState = Idle + } + } + + fun submitRefund() { + if (selectedRefundMethod == null) { + submissionState = Error("Please select a refund method.") + return + } + submissionState = Loading + println("Submitting refund for method: $selectedRefundMethod") + submissionState = Success + } + + fun resetSubmissionState() { + submissionState = Idle + } +} + +@Composable +fun rememberReturnOrderStateV9( + initialOrderItem: OrderItemV9, + initialRefundOptions: List, + initialSelectedOption: RefundMethodTypeV9? = MEESHO_APP +): ReturnOrderStateV9 { + return remember { + ReturnOrderStateV9( + initialOrderItem = initialOrderItem, + initialRefundOptions = initialRefundOptions.map { + it.copy(isSelected = it.id == initialSelectedOption) + }, + initialSelectedOption = initialSelectedOption + ) + } +} + +sealed class SubmissionStateV9 { + object Idle : SubmissionStateV9() + object Loading : SubmissionStateV9() + object Success : SubmissionStateV9() + data class Error(val message: String) : SubmissionStateV9() +} + +@Composable +fun ReturnOrderScreenV9( + state: ReturnOrderStateV9, + onNavigateBack: () -> Unit, +) { + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(state.submissionState) { + when (val submission = state.submissionState) { + is Error -> { + snackbarHostState.showSnackbar( + message = submission.message, + duration = SnackbarDuration.Short + ) + } + is Success -> { + snackbarHostState.showSnackbar( + message = "Refund submitted successfully!", + duration = SnackbarDuration.Short + ) + } + else -> {} + } + } + + Scaffold( + snackbarHost = { SnackbarHost(snackbarHostState) }, + bottomBar = { + SubmitButtonV9( + isLoading = state.submissionState == Loading, + isEnabled = state.selectedRefundMethod != null && state.submissionState != Loading, + onClick = { state.submitRefund() } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + OrderItemSummaryV9(item = state.orderItem) + Spacer(modifier = Modifier.height(24.dp)) + RefundMethodSelectionV9( + options = state.refundOptions, + selectedMethod = state.selectedRefundMethod, + onOptionSelected = { state.selectRefundMethod(it) } + ) + Spacer(modifier = Modifier.height(16.dp)) + + if (state.selectedRefundMethod == MEESHO_APP) { + MeeshoInfoBoxV9(onLearnMoreClick = { /* TODO */ }) + Spacer(modifier = Modifier.height(16.dp)) + } + } + } +} + +@Composable +fun OrderItemSummaryV9(item: OrderItemV9) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = item.imageUrl), + contentDescription = item.name, + modifier = Modifier + .size(60.dp) + .clip(RoundedCornerShape(4.dp)) + .border(1.dp, Color.LightGray.copy(alpha = 0.5f), RoundedCornerShape(4.dp)), + contentScale = ContentScale.Crop + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + item.name, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium + ) + Spacer(modifier = Modifier.height(4.dp)) + Row { + Text( + "₹${item.price}", + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Size: ${item.size}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("•", style = MaterialTheme.typography.bodySmall, color = Color.Gray) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Qty: ${item.quantity}", + style = MaterialTheme.typography.bodySmall, + color = Color.Gray + ) + } + } + } +} + +@Composable +fun RefundMethodSelectionV9( + options: List, + selectedMethod: RefundMethodTypeV9?, + onOptionSelected: (RefundMethodTypeV9) -> Unit +) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + Text( + "Choose your refund method", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(bottom = 16.dp) + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + options.forEach { option -> + RefundOptionCardV9( + option = option.copy(isSelected = option.id == selectedMethod), + onClick = { onOptionSelected(option.id) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun RefundOptionCardV9( + option: RefundOptionV9, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = if (option.isSelected) Color(0xFF8A2BE2) else Color.LightGray + val borderWidth = if (option.isSelected) 1.5.dp else 1.dp + + Card( + modifier = modifier + .border(borderWidth, borderColor, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + .clickable(onClick = onClick), + shape = RoundedCornerShape(8.dp), + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + Icon( + painter = painterResource(id = option.icon), + contentDescription = null, + modifier = Modifier.size(28.dp), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + option.title, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.Bold, + color = Color.DarkGray + ) + option.subtitle?.let { + Text( + it, + style = MaterialTheme.typography.labelSmall, + color = Color.Gray + ) + } + Divider( + modifier = Modifier.padding(vertical = 8.dp), + color = Color.LightGray.copy(alpha = 0.5f) + ) + Text( + "Get ₹${option.getAmount}", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) + option.extraAmount?.let { extra -> + Box( + modifier = Modifier + .padding(top = 8.dp) + .background(Color(0xFFE6F9E6), RoundedCornerShape(4.dp)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + "Extra ₹$extra", + color = Color(0xFF006400), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium + ) + } + } + if (option.extraAmount == null) { + Spacer(modifier = Modifier.height(28.dp)) + } + } + + if (option.isSelected) { + Icon( + imageVector = Icons.Filled.CheckCircle, + contentDescription = "Selected", + tint = Color(0xFF8A2BE2), + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(20.dp) + .background(Color.White, CircleShape) + ) + } + } + } +} + +@Composable +fun MeeshoInfoBoxV9(onLearnMoreClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + "Use it on your next order", + color = Color.White, + style = MaterialTheme.typography.bodySmall + ) + Text( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = Color.White, + textDecoration = TextDecoration.Underline + ) + ) { + append("Learn more") + } + }, + modifier = Modifier.clickable(onClick = onLearnMoreClick), + style = MaterialTheme.typography.bodySmall + ) + } + } +} + +@SuppressLint("DesignSystem") +@Composable +fun SubmitButtonV9( + isLoading: Boolean, + isEnabled: Boolean, + onClick: () -> Unit +) { + Button( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + .height(48.dp), + enabled = isEnabled && !isLoading, + shape = RoundedCornerShape(8.dp), + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = Color.White, + strokeWidth = 2.dp + ) + } else { + Text( + "Submit", + fontWeight = FontWeight.Bold, + fontSize = 16.sp + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_card_icon.xml b/app/src/main/res/drawable/ic_card_icon.xml new file mode 100644 index 000000000..e1c73adee --- /dev/null +++ b/app/src/main/res/drawable/ic_card_icon.xml @@ -0,0 +1,3 @@ + + +