diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/AstronomyShopActivity.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/AstronomyShopActivity.kt index a77a6ec88..66be7a043 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/AstronomyShopActivity.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/AstronomyShopActivity.kt @@ -31,6 +31,8 @@ import io.opentelemetry.android.demo.shop.ui.products.ProductDetails import io.opentelemetry.android.demo.shop.ui.products.ProductList import io.opentelemetry.android.demo.shop.ui.cart.CartViewModel import androidx.lifecycle.viewmodel.compose.viewModel +import io.opentelemetry.android.demo.shop.ui.cart.CheckoutConfirmationScreen +import io.opentelemetry.android.demo.shop.ui.cart.CheckoutInfoViewModel import io.opentelemetry.android.demo.shop.ui.cart.InfoScreen class AstronomyShopActivity : AppCompatActivity() { @@ -49,6 +51,8 @@ fun AstronomyShopScreen() { val context = LocalContext.current val astronomyShopNavController = rememberAstronomyShopNavController() val cartViewModel: CartViewModel = viewModel() + val checkoutInfoViewModel: CheckoutInfoViewModel = viewModel() + DemoAppTheme { Surface( modifier = Modifier.fillMaxSize(), @@ -102,7 +106,17 @@ fun AstronomyShopScreen() { } } composable(MainDestinations.CHECKOUT_INFO_ROUTE) { - InfoScreen(upPress = {astronomyShopNavController.upPress()}) + InfoScreen( + onPlaceOrderClick = {astronomyShopNavController.navigateToCheckoutConfirmation()}, + upPress = {astronomyShopNavController.upPress()}, + checkoutInfoViewModel = checkoutInfoViewModel + ) + } + composable(MainDestinations.CHECKOUT_CONFIRMATION_ROUTE){ + CheckoutConfirmationScreen( + cartViewModel = cartViewModel, + checkoutInfoViewModel = checkoutInfoViewModel + ) } } } diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/Navigation.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/Navigation.kt index 8cd4d6944..feac77803 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/Navigation.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/Navigation.kt @@ -26,6 +26,7 @@ object MainDestinations { const val PRODUCT_DETAIL_ROUTE = "product" const val PRODUCT_ID_KEY = "productId" const val CHECKOUT_INFO_ROUTE = "checkout-info" + const val CHECKOUT_CONFIRMATION_ROUTE = "checkout-confirmation" } @Composable @@ -53,6 +54,10 @@ class AstronomyShopNavController( fun navigateToCheckoutInfo(){ navController.navigate(MainDestinations.CHECKOUT_INFO_ROUTE) } + + fun navigateToCheckoutConfirmation(){ + navController.navigate(MainDestinations.CHECKOUT_CONFIRMATION_ROUTE) + } } class InstrumentedAstronomyShopNavController( @@ -84,6 +89,14 @@ class InstrumentedAstronomyShopNavController( ) } + fun navigateToCheckoutConfirmation() { + delegate.navigateToCheckoutConfirmation() + generateNavigationEvent( + eventName = "navigate.to.checkout.confirmation", + payload = emptyMap() + ) + } + private fun generateNavigationEvent(eventName: String, payload: Map) { val eventBuilder = OtelDemoApplication.eventBuilder("otel.demo.app.navigation", eventName) payload.forEach { (key, value) -> diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutConfirmation.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutConfirmation.kt new file mode 100644 index 000000000..809470aef --- /dev/null +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutConfirmation.kt @@ -0,0 +1,141 @@ +package io.opentelemetry.android.demo.shop.ui.cart + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import io.opentelemetry.android.demo.shop.ui.products.ProductCard +import java.util.Locale + +@Composable +fun CheckoutConfirmationScreen( + cartViewModel: CartViewModel, + checkoutInfoViewModel: CheckoutInfoViewModel +) { + val lifecycleOwner = LocalLifecycleOwner.current + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_PAUSE || event == Lifecycle.Event.ON_STOP) { + cartViewModel.clearCart() + } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + val shippingInfo = checkoutInfoViewModel.shippingInfo + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = "Your order is complete!", + fontSize = 24.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + Text( + text = "We've sent a confirmation email to ${shippingInfo.email}.", + fontSize = 18.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) + ) + + val cartItems = cartViewModel.cartItems.collectAsState().value + cartItems.forEach { cartItem -> + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + ProductCard( + product = cartItem.product, + onProductClick = {}, + modifier = Modifier + .width(300.dp) + .height(170.dp), + isNarrow = true + ) + Column( + modifier = Modifier + .fillMaxHeight(), + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Quantity: ${cartItem.quantity}", + fontSize = 12.sp, + modifier = Modifier + .padding(horizontal = 8.dp) + ) + + Text( + text = "Total: \$${String.format(Locale.US, "%.2f", cartItem.totalPrice())}", + fontSize = 14.sp, + modifier = Modifier + .padding(8.dp), + ) + } + } + } + + Text( + text = "Total Price: \$${String.format(Locale.US, "%.2f", cartViewModel.getTotalPrice())}", + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp), + textAlign = TextAlign.End + ) + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = "Shipping Data", + fontWeight = FontWeight.Bold + ) + Text( + text = "Street: ${shippingInfo.streetAddress}", + ) + Text( + text = "City: ${shippingInfo.city}", + ) + Text( + text = "State: ${shippingInfo.state}", + ) + Text( + text = "Zip Code: ${shippingInfo.zipCode}", + ) + Text( + text = "Country: ${shippingInfo.country}", + ) + } + } + } +} diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfo.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfo.kt index 2839abced..d8365bf39 100644 --- a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfo.kt +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfo.kt @@ -19,32 +19,6 @@ import io.opentelemetry.android.demo.shop.ui.components.UpPressButton import androidx.compose.ui.Alignment import androidx.compose.ui.text.style.TextAlign -data class ShippingInfo( - var email: String = "", - var streetAddress: String = "", - var zipCode: String = "", - var city: String = "", - var state: String = "", - var country: String = "" -) { - fun isComplete(): Boolean { - return arrayOf(email, streetAddress, zipCode, city, state, country) - .all { it.isNotBlank() } - } -} - -data class PaymentInfo( - var creditCardNumber: String = "", - var expiryMonth: String = "", - var expiryYear: String = "", - var cvv: String = "" -) { - fun isComplete(): Boolean { - return arrayOf(creditCardNumber, expiryMonth, expiryYear, cvv) - .all { it.isNotBlank() } - } -} - @Composable fun InfoField( value: String, @@ -99,20 +73,21 @@ fun InfoFieldsSection( @Composable fun InfoScreen( - upPress: () -> Unit + onPlaceOrderClick: () -> Unit, + upPress: () -> Unit, + checkoutInfoViewModel: CheckoutInfoViewModel ) { - var shippingInfo by remember { mutableStateOf(ShippingInfo()) } - var paymentInfo by remember { mutableStateOf(PaymentInfo()) } + val shippingInfo = checkoutInfoViewModel.shippingInfo + val paymentInfo = checkoutInfoViewModel.paymentInfo val focusManager = LocalFocusManager.current - val canProceed = shippingInfo.isComplete() && paymentInfo.isComplete() + val canProceed = checkoutInfoViewModel.canProceedToCheckout() Box( modifier = Modifier .fillMaxSize() .background(Color.White) ) { - // Content inside a Column Column( modifier = Modifier .fillMaxSize() @@ -124,12 +99,12 @@ fun InfoScreen( InfoFieldsSection( fields = listOf( - Triple("E-mail Address", shippingInfo.email) { shippingInfo = shippingInfo.copy(email = it) }, - Triple("Street Address", shippingInfo.streetAddress) { shippingInfo = shippingInfo.copy(streetAddress = it) }, - Triple("Zip Code", shippingInfo.zipCode) { shippingInfo = shippingInfo.copy(zipCode = it) }, - Triple("City", shippingInfo.city) { shippingInfo = shippingInfo.copy(city = it) }, - Triple("State", shippingInfo.state) { shippingInfo = shippingInfo.copy(state = it) }, - Triple("Country", shippingInfo.country) { shippingInfo = shippingInfo.copy(country = it) } + Triple("E-mail Address", shippingInfo.email) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(email = it)) }, + Triple("Street Address", shippingInfo.streetAddress) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(streetAddress = it)) }, + Triple("Zip Code", shippingInfo.zipCode) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(zipCode = it)) }, + Triple("City", shippingInfo.city) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(city = it)) }, + Triple("State", shippingInfo.state) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(state = it)) }, + Triple("Country", shippingInfo.country) { checkoutInfoViewModel.updateShippingInfo(shippingInfo.copy(country = it)) } ) ) @@ -139,21 +114,23 @@ fun InfoScreen( InfoFieldsSection( fields = listOf( - Triple("Credit Card Number", paymentInfo.creditCardNumber) { paymentInfo = paymentInfo.copy(creditCardNumber = it) }, - Triple("Month", paymentInfo.expiryMonth) { paymentInfo = paymentInfo.copy(expiryMonth = it) }, - Triple("Year", paymentInfo.expiryYear) { paymentInfo = paymentInfo.copy(expiryYear = it) }, - Triple("CVV", paymentInfo.cvv) { paymentInfo = paymentInfo.copy(cvv = it) } + Triple("Credit Card Number", paymentInfo.creditCardNumber) { checkoutInfoViewModel.updatePaymentInfo(paymentInfo.copy(creditCardNumber = it)) }, + Triple("Month", paymentInfo.expiryMonth) { checkoutInfoViewModel.updatePaymentInfo(paymentInfo.copy(expiryMonth = it)) }, + Triple("Year", paymentInfo.expiryYear) { checkoutInfoViewModel.updatePaymentInfo(paymentInfo.copy(expiryYear = it)) }, + Triple("CVV", paymentInfo.cvv) { checkoutInfoViewModel.updatePaymentInfo(paymentInfo.copy(cvv = it)) } ) ) Spacer(modifier = Modifier.height(16.dp)) Button( - onClick = { /*TODO Handle*/ }, + onClick = { + onPlaceOrderClick() + }, modifier = Modifier.fillMaxWidth(), enabled = canProceed ) { - Text("Proceed") + Text("Place Order") } } @@ -164,4 +141,4 @@ fun InfoScreen( .padding(8.dp) ) } -} +} \ No newline at end of file diff --git a/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfoViewModel.kt b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfoViewModel.kt new file mode 100644 index 000000000..4755b4397 --- /dev/null +++ b/demo-app/src/main/java/io/opentelemetry/android/demo/shop/ui/cart/CheckoutInfoViewModel.kt @@ -0,0 +1,53 @@ +package io.opentelemetry.android.demo.shop.ui.cart + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel + +data class ShippingInfo( + var email: String = "someone@example.com", + var streetAddress: String = "1600 Amphitheatre Parkway", + var zipCode: String = "94043", + var city: String = "Mountain View", + var state: String = "CA", + var country: String = "United States" +) { + fun isComplete(): Boolean { + return arrayOf(email, streetAddress, zipCode, city, state, country) + .all { it.isNotBlank() } + } +} + +data class PaymentInfo( + var creditCardNumber: String = "4432-8015-6152-0454", + var expiryMonth: String = "01", + var expiryYear: String = "2030", + var cvv: String = "137" +) { + fun isComplete(): Boolean { + return arrayOf(creditCardNumber, expiryMonth, expiryYear, cvv) + .all { it.isNotBlank() } + } +} + +class CheckoutInfoViewModel : ViewModel() { + + var shippingInfo by mutableStateOf(ShippingInfo()) + private set + + var paymentInfo by mutableStateOf(PaymentInfo()) + private set + + fun updateShippingInfo(newShippingInfo: ShippingInfo) { + shippingInfo = newShippingInfo + } + + fun updatePaymentInfo(newPaymentInfo: PaymentInfo) { + paymentInfo = newPaymentInfo + } + + fun canProceedToCheckout(): Boolean { + return shippingInfo.isComplete() && paymentInfo.isComplete() + } +} \ No newline at end of file