Bug 1895560 - Create a new message controller for Microsurveys r=android-reviewers,amejiamarmol

Differential Revision: https://phabricator.services.mozilla.com/D209740
This commit is contained in:
gela 2024-05-23 21:27:03 +00:00
parent 5bcf1b508a
commit c0221e385c
10 changed files with 256 additions and 24 deletions

View file

@ -108,3 +108,26 @@ messaging:
data_sensitivity:
- interaction
expires: never
micro_survey:
response:
type: event
description: User response data for a micro survey.
extra_keys:
survey_id:
description: The id of the survey.
type: string
user_selection:
description: |
The users selected option. For example, possible values are:
"Very satisfied", "satisfied", "neutral", "dissatisfied" and "Very dissatisfied".
type: string
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1891509
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1891509
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never

View file

@ -8,6 +8,7 @@ import android.content.Intent
import android.net.Uri
import androidx.annotation.VisibleForTesting
import androidx.core.net.toUri
import mozilla.components.service.nimbus.GleanMetrics.MicroSurvey
import mozilla.components.service.nimbus.GleanMetrics.Messaging as GleanMessaging
/**
@ -46,6 +47,19 @@ open class NimbusMessagingController(
messagingStorage.updateMetadata(updatedMetadata)
}
/**
* Called when a microsurvey attached to a message has been completed by the user.
*
* @param message The message containing the microsurvey that was completed.
* @param answer The user's response to the microsurvey question.
*/
override suspend fun onMicrosurveyCompleted(message: Message, answer: String) {
val messageMetadata = message.metadata
sendMicrosurveyCompletedTelemetry(messageMetadata.id, answer)
val updatedMetadata = messageMetadata.copy(pressed = true)
messagingStorage.updateMetadata(updatedMetadata)
}
/**
* Called once the user has clicked on a message.
*
@ -112,6 +126,10 @@ open class NimbusMessagingController(
)
}
private fun sendMicrosurveyCompletedTelemetry(messageId: String, answer: String?) {
MicroSurvey.response.record(MicroSurvey.ResponseExtra(surveyId = messageId, userSelection = answer))
}
private fun convertActionIntoDeepLinkSchemeUri(action: String): Uri =
if (action.startsWith("://")) {
"$deepLinkScheme$action".toUri()

View file

@ -15,6 +15,7 @@ import android.content.Intent
* - [onMessageClicked] and [getIntentForMessage] to be called when the user taps on
* the message, and to get the action for the message.
* - [onMessageDismissed] to be called when the user dismisses the message.
* - [onMicrosurveyCompleted] to be called when a microsurvey has been completed by the user.
*/
interface NimbusMessagingControllerInterface {
/**
@ -47,6 +48,18 @@ interface NimbusMessagingControllerInterface {
*/
suspend fun onMessageDismissed(message: Message)
/**
* Called when a microsurvey has been completed by the user.
*
* This function should be called when a microsurvey associated with a message
* has been completed by the user, providing the message object and the user's
* response in the form of LikertAnswers.
*
* @param message The message associated with the microsurvey.
* @param answer The user's response to the microsurvey, represented as LikertAnswers.
*/
suspend fun onMicrosurveyCompleted(message: Message, answer: String)
/**
* Called once the user has clicked on a message.
*

View file

@ -11571,29 +11571,6 @@ debug_drawer:
- android-probes@mozilla.com
expires: never
micro_survey:
response:
type: event
description: User response data for a micro survey.
extra_keys:
feature_name:
description: The name of the feature the survey is for e.g. "printing PDF".
type: string
user_selection:
description: |
The users selected option. For example, possible values are:
"Very satisfied", "satisfied", "neutral", "dissatisfied" and "Very dissatisfied".
type: string
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1891509
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1891509
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
navigation_bar:
home_search_tapped:
type: event

View file

@ -188,6 +188,20 @@ sealed class AppAction : Action {
* Indicates the given [message] was dismissed.
*/
data class MessageDismissed(val message: Message) : MessagingAction()
/**
* Sealed class representing actions related to microsurveys within messaging functionality.
* Extends [MessagingAction].
*/
sealed class MicrosurveyAction : MessagingAction() {
/**
* Indicates that the microsurvey associated with the [message] has been completed.
*
* @property message The message associated with the completed microsurvey.
* @property answer The answer provided for the microsurvey.
*/
data class Completed(val message: Message, val answer: String) : MicrosurveyAction()
}
}
/**

View file

@ -0,0 +1,55 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.messaging
import mozilla.components.service.nimbus.messaging.Message
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction.Completed
/**
* Handles interactions with the microsurveys.
*/
class MicrosurveyMessageController(
private val appStore: AppStore,
private val homeActivity: HomeActivity,
) : MessageController {
override fun onMessagePressed(message: Message) {
appStore.dispatch(MessageClicked(message))
}
override fun onMessageDismissed(message: Message) {
appStore.dispatch(MessageDismissed(message))
}
/**
* Handles the click event on the privacy link within a message.
* @param message The message containing the privacy link.
* @param privacyLink The URL of the privacy link.
*/
// Suppress unused parameter to work around the CI.
// message will be called by the UI when privacy noticed is clicked.
@Suppress("UNUSED_PARAMETER")
fun onPrivacyLinkClicked(message: Message, privacyLink: String) {
homeActivity.openToBrowserAndLoad(
searchTermOrURL = privacyLink,
newTab = true,
from = BrowserDirection.FromHome,
)
}
/**
* Dispatches an action when a survey is completed.
* @param message The message containing the completed survey.
* @param answer The answer provided in the survey.
*/
fun onSurveyCompleted(message: Message, answer: String) {
appStore.dispatch(Completed(message, answer))
}
}

View file

@ -16,6 +16,7 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.ConsumeMe
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Evaluate
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageDismissed
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.Restore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessageToShow
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.UpdateMessages
@ -58,6 +59,10 @@ class MessagingMiddleware(
is MessageDismissed -> onMessageDismissed(context, action.message)
is MicrosurveyAction.Completed -> {
onMicrosurveyCompleted(context, action.message, action.answer)
}
else -> {
// no-op
}
@ -65,6 +70,19 @@ class MessagingMiddleware(
next(action)
}
private fun onMicrosurveyCompleted(
context: AppStoreMiddlewareContext,
message: Message,
answer: String,
) {
val newMessages = removeMessage(context, message = message)
context.store.dispatch(UpdateMessages(newMessages))
consumeMessageToShowIfNeeded(context, message)
coroutineScope.launch {
controller.onMicrosurveyCompleted(message, answer)
}
}
private fun onMessagedDisplayed(
oldMessage: Message,
context: AppStoreMiddlewareContext,

View file

@ -15,7 +15,7 @@ import org.mozilla.fenix.messaging.MessagingState
* Reducer for [MessagingState].
*/
internal object MessagingReducer {
fun reduce(state: AppState, action: AppAction.MessagingAction): AppState = when (action) {
fun reduce(state: AppState, action: AppAction): AppState = when (action) {
is UpdateMessageToShow -> {
val messageToShow = state.messaging.messageToShow.toMutableMap()
messageToShow[action.message.surface] = action.message

View file

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package mozilla.components.service.nimbus.messaging.microsurvey
import androidx.annotation.DrawableRes
/**
* Model containing the required data from a raw [MicrosurveyUiData] object in a UI state.
*/
data class MicrosurveyUiData(
val type: Type,
@DrawableRes val imageRes: Int,
val title: String,
val description: String,
val primaryButtonLabel: String,
val secondaryButtonLabel: String,
) {
/**
* Model for different types of Microsurvey ratings.
*
* @property fivePointScaleItems Identifier for the likert scale rating.
*/
enum class Type(
val fivePointScaleItems: String,
) {
VERY_DISSATISFIED(
fivePointScaleItems = "very_dissatisfied",
),
DISSATISFIED(
fivePointScaleItems = "dissatisfied",
),
NEUTRAL(
fivePointScaleItems = "neutral",
),
SATISFIED(
fivePointScaleItems = "satisfied",
),
VERY_SATISFIED(
fivePointScaleItems = "very_satisfied",
),
}
}

View file

@ -0,0 +1,70 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.messaging
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.support.test.robolectric.testContext
import mozilla.telemetry.glean.testing.GleanTestRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MessageClicked
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction.MicrosurveyAction.Completed
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@RunWith(FenixRobolectricTestRunner::class)
class MicrosurveyMessageControllerTest {
@get:Rule
val gleanTestRule = GleanTestRule(testContext)
private val homeActivity: HomeActivity = mockk(relaxed = true)
private val message: Message = mockk(relaxed = true)
private lateinit var microsurveyMessageController: MicrosurveyMessageController
private val appStore: AppStore = mockk(relaxed = true)
@Before
fun setup() {
microsurveyMessageController = MicrosurveyMessageController(
appStore = appStore,
homeActivity = homeActivity,
)
}
@Test
fun `WHEN calling onMessagePressed THEN update the app store with the MessageClicked action`() {
microsurveyMessageController.onMessagePressed(message)
verify { appStore.dispatch(MessageClicked(message)) }
}
@Test
fun `WHEN calling onMessageDismissed THEN update the app store with the MessageDismissed action`() {
microsurveyMessageController.onMessageDismissed(message)
verify { appStore.dispatch(AppAction.MessagingAction.MessageDismissed(message)) }
}
@Test
fun `WHEN calling onPrivacyLinkClicked THEN open the privacy URL in a new tab`() {
val privacyURL = "www.mozilla.com"
microsurveyMessageController.onPrivacyLinkClicked(message, privacyURL)
verify { homeActivity.openToBrowserAndLoad(any(), newTab = true, any(), any()) }
}
@Test
fun `WHEN calling onSurveyCompleted THEN update the app store with the SurveyCompleted action`() {
val answer = "satisfied"
microsurveyMessageController.onSurveyCompleted(message, answer)
verify { appStore.dispatch(Completed(message, answer)) }
}
}