Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,42 @@ package com.woocommerce.android.ui.bookings.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.FileCopy
import androidx.compose.material.icons.outlined.MoreHoriz
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.woocommerce.android.R
import com.woocommerce.android.ui.compose.preview.LightDarkThemePreviews
import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground
import com.woocommerce.android.util.ActivityUtils

@Composable
fun BookingCustomerDetails(
model: BookingCustomerDetailsModel,
onEmailClick: () -> Unit,
onPhoneClick: () -> Unit,
modifier: Modifier = Modifier,
) {
var phoneMenuExpanded by remember { mutableStateOf(false) }
Comment on lines 29 to +38
Copy link
Preview

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Removing the onEmailClick and onPhoneClick callbacks hardcodes side effects (email intent, phone actions) inside the composable, reducing testability and flexibility (e.g., adding analytics, logging, or alternative behaviors). Consider reintroducing callbacks (with sensible defaults) so the UI remains a pure presenter while delegating side effects to higher layers.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flexibility (e.g., adding analytics,

I was about to mention that too. Adding analytics would indeed be easier if we handled actions in the view models. But it’s not a major concern, we can revisit it when we implement analytics.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I think there are pros and cons, like always. This way, the component is very easy to drop and use, without needing to implement the actions every time.

val context = LocalContext.current

Column(modifier = modifier) {
BookingSectionHeader(R.string.booking_customer_details_header)
Column(
Expand All @@ -44,23 +50,32 @@ fun BookingCustomerDetails(
value = model.email,
trailingIcon = {
Icon(
imageVector = Icons.Outlined.FileCopy,
painter = painterResource(R.drawable.ic_email),
contentDescription = stringResource(id = R.string.booking_customer_label_email),
tint = MaterialTheme.colorScheme.primary
)
},
modifier = Modifier.clickable { onEmailClick() }
modifier = Modifier.clickable {
ActivityUtils.sendEmail(context, model.email)
}
)
CustomerDetailsRow(
value = model.phone,
trailingIcon = {
Icon(
imageVector = Icons.Outlined.MoreHoriz,
contentDescription = stringResource(id = R.string.booking_customer_label_phone),
tint = MaterialTheme.colorScheme.primary
)
Box {
Icon(
painter = painterResource(R.drawable.ic_menu_more_vert),
contentDescription = stringResource(id = R.string.booking_customer_label_phone),
tint = MaterialTheme.colorScheme.primary
)
ContactDropdownMenu(
expanded = phoneMenuExpanded,
phone = model.phone,
onDismissRequest = { phoneMenuExpanded = false }
)
}
},
modifier = Modifier.clickable { onPhoneClick() }
modifier = Modifier.clickable { phoneMenuExpanded = true }
)
Column(
modifier = Modifier
Expand Down Expand Up @@ -145,8 +160,6 @@ private fun BookingCustomerDetailsPreview() {
"AL 36109"
)
),
onEmailClick = {},
onPhoneClick = {},
modifier = Modifier.fillMaxWidth()
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.woocommerce.android.ui.bookings.compose

import android.content.Context
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import com.woocommerce.android.R
import com.woocommerce.android.util.ActivityUtils
import com.woocommerce.android.util.PhoneContactOption
import com.woocommerce.android.util.getAvailablePhoneContactOptions
import com.woocommerce.android.util.stringRes
import org.wordpress.android.util.ToastUtils

@Composable
fun ContactDropdownMenu(
expanded: Boolean,
phone: String,
onDismissRequest: () -> Unit,
) {
val context = LocalContext.current
var contactOptions by remember { mutableStateOf(emptyList<PhoneContactOption>()) }

// Update the available contact options when the composable enters the RESUMED state
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
contactOptions = context.getAvailablePhoneContactOptions()
}

DropdownMenu(
expanded = expanded,
onDismissRequest = onDismissRequest
) {
contactOptions.forEach { contactOption ->
DropdownMenuItem(
text = { Text(stringResource(contactOption.stringRes)) },
onClick = {
contactOption.action(context, phone)
onDismissRequest()
}
)
}
}
}

private val PhoneContactOption.action: (Context, String) -> Unit
get() = when (this) {
PhoneContactOption.CALL -> {
// This is the lambda being returned. It must define its parameters.
{ context, phone ->
ActivityUtils.dialPhoneNumber(context, phone) {
ToastUtils.showToast(context, R.string.error_no_phone_app)
}
}
}

PhoneContactOption.SMS -> {
{ context, phone ->
ActivityUtils.sendSms(context, phone) {
ToastUtils.showToast(context, R.string.error_no_sms_app)
}
}
}

PhoneContactOption.WHATSAPP -> {
{ context, phone ->
ActivityUtils.openWhatsApp(context, phone)
}
}

PhoneContactOption.TELEGRAM -> {
{ context, phone ->
ActivityUtils.openTelegram(context, phone)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ fun BookingDetailsScreen(
)
BookingCustomerDetails(
model = viewState.bookingCustomerDetails,
onEmailClick = {},
onPhoneClick = {},
modifier = Modifier.fillMaxWidth()
)
BookingAttendanceSection(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package com.woocommerce.android.ui.orders

import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.woocommerce.android.R
import com.woocommerce.android.analytics.AnalyticsEvent
import com.woocommerce.android.analytics.AnalyticsTracker
Expand Down Expand Up @@ -83,15 +80,11 @@ object OrderCustomerHelper {
)
)

val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("smsto:$phone")
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
ActivityUtils.sendSms(context, phone) { error ->
AnalyticsTracker.track(
AnalyticsEvent.ORDER_CONTACT_ACTION_FAILED,
this.javaClass.simpleName,
e.javaClass.simpleName,
error.javaClass.simpleName,
"No SMS app was found"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import com.woocommerce.android.model.Order
import com.woocommerce.android.ui.orders.OrderCustomerHelper
import com.woocommerce.android.ui.orders.details.OrderDetailFragmentDirections
import com.woocommerce.android.util.ActivityUtils
import com.woocommerce.android.util.PhoneContactOption
import com.woocommerce.android.util.PhoneUtils
import com.woocommerce.android.util.getAvailablePhoneContactOptions
import com.woocommerce.android.widgets.AppRatingDialog

class OrderDetailCustomerInfoView @JvmOverloads constructor(
Expand All @@ -36,8 +38,6 @@ class OrderDetailCustomerInfoView @JvmOverloads constructor(
private companion object {
const val KEY_SUPER_STATE = "ORDER-DETAIL-CUSTOMER-INFO-VIEW-SUPER-STATE"
const val KEY_IS_CUSTOMER_INFO_VIEW_EXPANDED = "ORDER-DETAIL-CUSTOMER-INFO-VIEW-IS_CUSTOMER_INFO_VIEW_EXPANDED"
private const val WHATSAPP_PACKAGE_NAME = "com.whatsapp"
private const val TELEGRAM_PACKAGE_NAME = "org.telegram.messenger"
}

private val binding = OrderDetailCustomerInfoBinding.inflate(LayoutInflater.from(ctx), this)
Expand Down Expand Up @@ -281,6 +281,7 @@ class OrderDetailCustomerInfoView @JvmOverloads constructor(
binding.customerInfoMorePanel.expand()
binding.customerInfoViewMore.setOnClickListener(null)
}

else -> {
binding.customerInfoShippingAddr.setText(shippingAddress, R.string.order_detail_add_shipping_address)
binding.customerInfoShippingMethodSection.isVisible = order.shippingMethods.firstOrNull()?.let {
Expand Down Expand Up @@ -331,6 +332,8 @@ class OrderDetailCustomerInfoView @JvmOverloads constructor(
val popup = PopupMenu(context, binding.customerInfoCallOrMessageBtn)
popup.menuInflater.inflate(R.menu.menu_order_detail_phone_actions, popup.menu)

val contactOptions = context.getAvailablePhoneContactOptions()

popup.menu.findItem(R.id.menu_call)?.setOnMenuItemClickListener {
AnalyticsTracker.track(AnalyticsEvent.ORDER_DETAIL_CUSTOMER_INFO_PHONE_MENU_PHONE_TAPPED)
OrderCustomerHelper.dialPhone(context, order, order.billingAddress.phone)
Expand All @@ -345,7 +348,7 @@ class OrderDetailCustomerInfoView @JvmOverloads constructor(
true
}

if (ActivityUtils.isAppInstalled(context, WHATSAPP_PACKAGE_NAME)) {
if (contactOptions.contains(PhoneContactOption.WHATSAPP)) {
popup.menu.add(
0,
View.generateViewId(),
Expand All @@ -359,7 +362,7 @@ class OrderDetailCustomerInfoView @JvmOverloads constructor(
}
}

if (ActivityUtils.isAppInstalled(context, TELEGRAM_PACKAGE_NAME)) {
if (contactOptions.contains(PhoneContactOption.TELEGRAM)) {
popup.menu.add(
0,
View.generateViewId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Parcelable
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.woocommerce.android.R
import com.woocommerce.android.extensions.intentActivities
import com.woocommerce.android.model.UiString
Expand Down Expand Up @@ -53,6 +54,16 @@ object ActivityUtils {
}
}

fun sendSms(context: Context, phoneNumber: String, onError: (e: ActivityNotFoundException) -> Unit) {
val intent = Intent(Intent.ACTION_SENDTO)
intent.data = "smsto:$phoneNumber".toUri()
try {
context.startActivity(intent)
} catch (e: ActivityNotFoundException) {
onError(e)
}
}

/**
* Use this only when you want to open the external browser - otherwise use
* [ChromeCustomTabUtils.launchUrl] to provide a better in-app experience
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.woocommerce.android.util

import android.content.Context
import androidx.annotation.StringRes
import com.woocommerce.android.R

private const val WHATSAPP_PACKAGE_NAME = "com.whatsapp"
private const val TELEGRAM_PACKAGE_NAME = "org.telegram.messenger"

fun Context.getAvailablePhoneContactOptions(): List<PhoneContactOption> {
return buildList {
add(PhoneContactOption.CALL)
add(PhoneContactOption.SMS)
if (ActivityUtils.isAppInstalled(this@getAvailablePhoneContactOptions, WHATSAPP_PACKAGE_NAME)) {
add(PhoneContactOption.WHATSAPP)
}
if (ActivityUtils.isAppInstalled(this@getAvailablePhoneContactOptions, TELEGRAM_PACKAGE_NAME)) {
add(PhoneContactOption.TELEGRAM)
}
}
}

enum class PhoneContactOption {
CALL, SMS, WHATSAPP, TELEGRAM
}

val PhoneContactOption.stringRes: Int
@StringRes get() = when (this) {
PhoneContactOption.CALL -> R.string.orderdetail_call_customer
PhoneContactOption.SMS -> R.string.orderdetail_message_customer
PhoneContactOption.WHATSAPP -> R.string.orderdetail_message_customer_using_whatsapp
PhoneContactOption.TELEGRAM -> R.string.orderdetail_message_customer_using_telegram
}