Skip to content

Subscriber detail stats #21982

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d77f39e
Updated wordpress-rs hash
nbradbury Jun 27, 2025
07b7169
Added country and email address to subscriber detail
nbradbury Jun 27, 2025
6ea36cb
Moved date formatting to the parent view model
nbradbury Jun 27, 2025
00667bc
Reverted date formatting change, format detail date
nbradbury Jun 27, 2025
d53d4b1
Added support for opening urls and emails
nbradbury Jun 27, 2025
810f331
Moved dummy data note
nbradbury Jun 27, 2025
86258d5
Added plan to detail screen
nbradbury Jun 27, 2025
30b80b9
First pass at fetching subscriber stats
nbradbury Jun 27, 2025
f85d765
Incomplete attempt at fetchSubscriber
nbradbury Jun 27, 2025
23b0664
Added logic to fetch both a subscriber and its stats
nbradbury Jun 27, 2025
a489f3c
Show actual stats when available
nbradbury Jun 27, 2025
09f7f61
Fetch subscriber stats when clicked
nbradbury Jun 27, 2025
f7a2ef0
Clear stats before fetching
nbradbury Jun 27, 2025
859fc07
Use a cancellable job for stats
nbradbury Jun 27, 2025
7d418fa
Hide country when not available
nbradbury Jun 27, 2025
e116fe3
Use withContext(ioDispatcher) when fetching data
nbradbury Jun 27, 2025
1216845
Merge branch 'trunk' into feature/subscriber-detail-stats
nbradbury Jun 27, 2025
0043633
Fixed empty line Detekt nag
nbradbury Jun 28, 2025
0a8daa0
Added open email vector drawable
nbradbury Jun 28, 2025
f954dfe
Removed unused detail view model
nbradbury Jun 28, 2025
ed95386
Updated wordpress-rs hash
nbradbury Jun 30, 2025
77f4c0c
Merge branch 'trunk' into feature/subscriber-detail-stats
adalpari Jul 1, 2025
3fbb0d7
Lazy init the WpComApiClient
nbradbury Jul 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ open class DataViewViewModel @Inject constructor(
private var page = 0
private var canLoadMore = true

lateinit var wpComApiClient: WpComApiClient
// TODO this is strictly for wp.com sites, we'll need different auth for self-hosted
val wpComApiClient: WpComApiClient by lazy {
WpComApiClient(
WpAuthenticationProvider.staticWithAuth(
WpAuthentication.Bearer(token = accountStore.accessToken!!)
)
)
}

init {
appLogWrapper.d(AppLog.T.MAIN, "$logTag init")
launch {
// TODO this is strictly for wp.com sites, we'll need different auth for self-hosted
wpComApiClient = WpComApiClient(
WpAuthenticationProvider.staticWithAuth(
WpAuthentication.Bearer(token = accountStore.accessToken!!)
)
)

_itemSortBy.value = getDefaultSort()

fetchData()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.wordpress.android.ui.subscribers

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
Expand All @@ -18,7 +19,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.MailOutline
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
Expand All @@ -27,28 +27,38 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.wordpress.android.R
import org.wordpress.android.models.wrappers.SimpleDateFormatWrapper
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.dataview.compose.RemoteImage
import org.wordpress.android.ui.subscribers.SubscribersViewModel.Companion.displayNameOrEmail
import uniffi.wp_api.IndividualSubscriberStats
import uniffi.wp_api.Subscriber
import uniffi.wp_api.SubscriberCountry
import java.util.Date

@Composable
fun SubscriberDetailScreen(
subscriber: Subscriber,
modifier: Modifier = Modifier
onUrlClick: (String) -> Unit,
onEmailClick: (String) -> Unit,
modifier: Modifier = Modifier,
subscriberStats: State<IndividualSubscriberStats?>? = null
) {
Column(
modifier = modifier
Expand All @@ -61,15 +71,21 @@ fun SubscriberDetailScreen(

Spacer(modifier = Modifier.height(24.dp))

EmailStatsCard()
subscriberStats?.value?.let { stats ->
EmailStatsCard(subscriberStats = stats)
}

Spacer(modifier = Modifier.height(16.dp))

NewsletterSubscriptionCard(subscriber)

Spacer(modifier = Modifier.height(16.dp))

SubscriberDetailsCard(subscriber)
SubscriberDetailsCard(
subscriber = subscriber,
onUrlClick = onUrlClick,
onEmailClick = onEmailClick
)

Spacer(modifier = Modifier.height(32.dp))

Expand Down Expand Up @@ -112,22 +128,13 @@ fun ProfileHeader(
textAlign = TextAlign.Center
)
}
// TODO remove this once we have actual data
Spacer(modifier = Modifier.height(12.dp))
Row {
Text(
text = "Note: Displaying dummy data",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
}
}

@Composable
fun EmailStatsCard() {
fun EmailStatsCard(
subscriberStats: IndividualSubscriberStats
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
Expand All @@ -142,19 +149,20 @@ fun EmailStatsCard() {
StatItem(
icon = Icons.Default.Email,
label = stringResource(R.string.subscribers_emails_sent_label),
value = "100"
value = subscriberStats.emailsSent.toString()
)
StatItem(
icon = Icons.Default.MailOutline,
icon = ImageVector.vectorResource(id = R.drawable.ic_email_open),
label = stringResource(R.string.subscribers_opened_label),
value = "10"
value = subscriberStats.uniqueOpens.toString()
)
StatItem(
icon = Icons.Default.Check,
label = stringResource(R.string.subscribers_clicked_label),
value = "10%"
value = subscriberStats.uniqueClicks.toString()
)
}
Spacer(modifier = Modifier.height(12.dp))
}
}

Expand Down Expand Up @@ -212,22 +220,29 @@ fun NewsletterSubscriptionCard(subscriber: Subscriber) {

DetailRow(
label = stringResource(R.string.subscribers_date_label),
value = subscriber.dateSubscribed.toString()
value = SimpleDateFormatWrapper().getDateInstance().format(subscriber.dateSubscribed)
)

Spacer(modifier = Modifier.height(12.dp))
if (subscriber.plans?.isNotEmpty() == true) {
val plan = subscriber.plans!!.first()
Spacer(modifier = Modifier.height(12.dp))

DetailRow(
label = stringResource(R.string.subscribers_plan_label),
value = "???",
valueColor = MaterialTheme.colorScheme.primary
)
DetailRow(
label = stringResource(R.string.subscribers_plan_label),
value = plan.title,
valueColor = MaterialTheme.colorScheme.primary
)
}
}
}
}

@Composable
fun SubscriberDetailsCard(subscriber: Subscriber) {
private fun SubscriberDetailsCard(
subscriber: Subscriber,
onUrlClick: (String) -> Unit,
onEmailClick: (String) -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
Expand All @@ -250,34 +265,46 @@ fun SubscriberDetailsCard(subscriber: Subscriber) {
DetailRow(
label = stringResource(R.string.subscribers_email_label),
value = subscriber.emailAddress,
valueColor = MaterialTheme.colorScheme.primary
valueColor = MaterialTheme.colorScheme.primary,
modifier = Modifier.clickable {
onEmailClick(subscriber.emailAddress)
}
)

Spacer(modifier = Modifier.height(12.dp))

DetailRow(
label = stringResource(R.string.subscribers_country_label),
value = "???"
)
subscriber.country?.name?.let { countryName ->
DetailRow(
label = stringResource(R.string.subscribers_country_label),
value = countryName
)
Spacer(modifier = Modifier.height(12.dp))
}

Spacer(modifier = Modifier.height(12.dp))

DetailRow(
label = stringResource(R.string.subscribers_site_label),
value = "???",
valueColor = MaterialTheme.colorScheme.primary
)
subscriber.url?.let { url ->
DetailRow(
label = stringResource(R.string.subscribers_site_label),
value = url,
valueColor = MaterialTheme.colorScheme.primary,
modifier = Modifier.clickable {
onUrlClick(url)
}
)
}
}
}
}

@Composable
fun DetailRow(
private fun DetailRow(
label: String,
value: String,
valueColor: Color = MaterialTheme.colorScheme.onSurface
modifier: Modifier = Modifier,
valueColor: Color = MaterialTheme.colorScheme.onSurface,
) {
Column {
Column(
modifier = modifier
) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
Expand All @@ -294,7 +321,7 @@ fun DetailRow(
}

@Composable
fun DeleteSubscriberButton() {
private fun DeleteSubscriberButton() {
Button(
onClick = { /* Handle delete action */ },
modifier = Modifier.fillMaxWidth(),
Expand Down Expand Up @@ -322,15 +349,31 @@ fun DeleteSubscriberButton() {
fun SubscriberDetailScreenPreview() {
val subscriber = Subscriber(
userId = 0L,
subscriptionId = 0u,
displayName = "User Name",
emailAddress = "[email protected]",
emailSubscriptionId = 0u,
isEmailSubscriber = true,
url = "https://example.com",
dateSubscribed = Date(),
subscriptionStatus = "Subscribed",
avatar = "",
country = SubscriberCountry("US", "United States"),
plans = emptyList(),
)

val subscriberStats = IndividualSubscriberStats(
emailsSent = 10u,
uniqueOpens = 5u,
uniqueClicks = 3u,
blogRegistrationDate = Date().toString(),
)

AppThemeM3 {
SubscriberDetailScreen(subscriber)
SubscriberDetailScreen(
subscriber = subscriber,
subscriberStats = remember { mutableStateOf(subscriberStats) },
onUrlClick = {},
onEmailClick = {}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.R
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.dataview.DataViewScreen
import org.wordpress.android.ui.main.BaseAppCompatActivity
Expand Down Expand Up @@ -131,7 +132,14 @@ class SubscribersActivity : BaseAppCompatActivity() {
titleState.value = subscriber.displayNameOrEmail()
SubscriberDetailScreen(
subscriber = subscriber,
modifier = Modifier.padding(contentPadding)
onEmailClick = { email ->
onEmailClick(email)
},
onUrlClick = { url ->
onUrlClick(url)
},
modifier = Modifier.padding(contentPadding),
subscriberStats = viewModel.subscriberStats.collectAsState()
)
}
}
Expand All @@ -141,6 +149,14 @@ class SubscribersActivity : BaseAppCompatActivity() {
}
}

private fun onEmailClick(email: String) {
ActivityLauncher.openUrlExternal(this, "mailto:$email")
}

private fun onUrlClick(url: String) {
ActivityLauncher.openUrlExternal(this, url)
}

companion object {
private const val KEY_ID = "id"
}
Expand Down
Loading