Skip to content

Commit

Permalink
Demo app: Order confirmation (#598)
Browse files Browse the repository at this point in the history
* created a confirmation of placed order screen

* moved checkout info to a ViewModel

* passed shipping info to checkout confirmation

* in progress

* changed layout a bit and cleared cart when leaving the confirmation screen using lifecycle owner

* fixed the inability to update payment and shipping info by removing an unnecessary remember

* added email address to the confirmation screen
  • Loading branch information
magda-woj authored Sep 18, 2024
1 parent eab13f0 commit 864e262
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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(),
Expand Down Expand Up @@ -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
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -53,6 +54,10 @@ class AstronomyShopNavController(
fun navigateToCheckoutInfo(){
navController.navigate(MainDestinations.CHECKOUT_INFO_ROUTE)
}

fun navigateToCheckoutConfirmation(){
navController.navigate(MainDestinations.CHECKOUT_CONFIRMATION_ROUTE)
}
}

class InstrumentedAstronomyShopNavController(
Expand Down Expand Up @@ -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<String, String>) {
val eventBuilder = OtelDemoApplication.eventBuilder("otel.demo.app.navigation", eventName)
payload.forEach { (key, value) ->
Expand Down
Original file line number Diff line number Diff line change
@@ -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}",
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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)) }
)
)

Expand All @@ -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")
}
}

Expand All @@ -164,4 +141,4 @@ fun InfoScreen(
.padding(8.dp)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -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 = "[email protected]",
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()
}
}

0 comments on commit 864e262

Please sign in to comment.