diff --git a/.experimenter.json b/.experimenter.json deleted file mode 100644 index 9321f9c7a..000000000 --- a/.experimenter.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "default-browser-message": { - "description": "A small feature allowing experiments on the placement of a default browser message.", - "hasExposure": true, - "exposureDescription": "", - "variables": { - "message-location": { - "type": "string", - "description": "Where is the message to be put." - } - } - }, - "homescreen": { - "description": "The homescreen that the user goes to when they press home or new tab.", - "hasExposure": true, - "exposureDescription": "", - "variables": { - "sections-enabled": { - "type": "json", - "description": "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default." - } - } - }, - "messaging": { - "description": "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n", - "hasExposure": true, - "exposureDescription": "", - "variables": { - "actions": { - "type": "json", - "description": "A growable map of action URLs." - }, - "message-under-experiment": { - "type": "string", - "description": "Id or prefix of the message under experiment." - }, - "messages": { - "type": "json", - "description": "A growable collection of messages" - }, - "styles": { - "type": "json", - "description": "A map of styles to configure message appearance.\n" - }, - "triggers": { - "type": "json", - "description": "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n" - } - } - }, - "nimbus-validation": { - "description": "A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.", - "hasExposure": true, - "exposureDescription": "", - "variables": { - "settings-icon": { - "type": "string", - "description": "The drawable displayed in the app menu for Settings" - }, - "settings-punctuation": { - "type": "string", - "description": "The emoji displayed in the Settings screen title." - }, - "settings-title": { - "type": "string", - "description": "The title of displayed in the Settings screen and app menu." - } - } - }, - "search-term-groups": { - "description": "A feature allowing the grouping of URLs around the search term that it came from.", - "hasExposure": true, - "exposureDescription": "", - "variables": { - "enabled": { - "type": "boolean", - "description": "If true, the feature shows up on the homescreen and on the new tab screen." - } - } - } -} \ No newline at end of file diff --git a/.experimenter.yaml b/.experimenter.yaml new file mode 100644 index 000000000..794187f6f --- /dev/null +++ b/.experimenter.yaml @@ -0,0 +1,56 @@ +--- +default-browser-message: + description: A small feature allowing experiments on the placement of a default browser message. + hasExposure: true + exposureDescription: "" + variables: + message-location: + type: string + description: Where is the message to be put. +homescreen: + description: The homescreen that the user goes to when they press home or new tab. + hasExposure: true + exposureDescription: "" + variables: + sections-enabled: + type: json + description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default." +messaging: + description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n" + hasExposure: true + exposureDescription: "" + variables: + actions: + type: json + description: A growable map of action URLs. + messages: + type: json + description: A growable collection of messages + styles: + type: json + description: "A map of styles to configure message appearance.\n" + triggers: + type: json + description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n" +nimbus-validation: + description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production. + hasExposure: true + exposureDescription: "" + variables: + settings-icon: + type: string + description: The drawable displayed in the app menu for Settings + settings-punctuation: + type: string + description: The emoji displayed in the Settings screen title. + settings-title: + type: string + description: The title of displayed in the Settings screen and app menu. +search-term-groups: + description: A feature allowing the grouping of URLs around the search term that it came from. + hasExposure: true + exposureDescription: "" + variables: + enabled: + type: boolean + description: "If true, the feature shows up on the homescreen and on the new tab screen." diff --git a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt index 0c520506d..031b5c234 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -26,6 +26,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.gleanplumb.CustomAttributeProvider import org.mozilla.fenix.gleanplumb.OnDiskMessageMetadataStorage import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.nimbus.FxNimbus @@ -137,6 +138,7 @@ class Analytics( metrics.track(Event.Messaging.MessageMalformed(it)) }, messagingFeature = FxNimbus.features.messaging, + attributeProvider = CustomAttributeProvider, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt new file mode 100644 index 000000000..d18f2e855 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/CustomAttributeProvider.kt @@ -0,0 +1,34 @@ +/* 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.gleanplumb + +import android.content.Context +import org.json.JSONObject +import org.mozilla.fenix.utils.BrowsersCache +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.Calendar + +/** + * Custom attributes that the messaging framework will use to evaluate if message is eligible + * to be shown. + */ +object CustomAttributeProvider { + private val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) + + /** + * Returns a [JSONObject] that contains all the custom attributes, evaluated when the function + * was called. + */ + fun getCustomAttributes(context: Context): JSONObject { + val now = Calendar.getInstance() + return JSONObject( + mapOf( + "is_default_browser" to BrowsersCache.all(context).isDefaultBrowser, + "date_string" to formatter.format(now.time) + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt index 188911e55..12e35d168 100644 --- a/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/DefaultMessageController.kt @@ -42,7 +42,7 @@ class DefaultMessageController( } override fun onMessageDisplayed(message: Message) { - if (message.data.maxDisplayCount <= message.metadata.displayCount + 1) { + if (message.maxDisplayCount <= message.metadata.displayCount + 1) { metrics.track(Event.Messaging.MessageExpired(message.id)) } metrics.track(Event.Messaging.MessageShown(message.id)) diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt index 26d75afc9..b0e9df527 100644 --- a/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/Message.kt @@ -27,6 +27,12 @@ data class Message( val triggers: List, val metadata: Metadata ) { + val maxDisplayCount: Int + get() = style.maxDisplayCount + + val priority: Int + get() = style.priority + /** * A data class that holds metadata that help to identify if a message should shown. * diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt index ede523f6a..e83207a3a 100644 --- a/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorage.kt @@ -12,6 +12,7 @@ import org.mozilla.experiments.nimbus.GleanPlumbInterface import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.experiments.nimbus.internal.NimbusException +import org.mozilla.fenix.nimbus.ControlMessageBehavior import org.mozilla.fenix.nimbus.Messaging import org.mozilla.fenix.nimbus.StyleData @@ -23,12 +24,13 @@ class NimbusMessagingStorage( private val metadataStorage: MessageMetadataStorage, private val reportMalformedMessage: (String) -> Unit, private val gleanPlumb: GleanPlumbInterface, - private val messagingFeature: FeatureHolder + private val messagingFeature: FeatureHolder, + private val attributeProvider: CustomAttributeProvider? = null ) { private val logger = Logger("MessagingStorage") private val nimbusFeature = messagingFeature.value() private val customAttributes: JSONObject - get() = JSONObject() + get() = attributeProvider?.getCustomAttributes(context) ?: JSONObject() /** * Returns a list of available messages descending sorted by their priority. @@ -54,7 +56,7 @@ class NimbusMessagingStorage( ?: return@mapNotNull null ) }.filter { - it.data.maxDisplayCount >= it.metadata.displayCount && + it.maxDisplayCount >= it.metadata.displayCount && !it.metadata.dismissed && !it.metadata.pressed }.sortedByDescending { @@ -66,21 +68,33 @@ class NimbusMessagingStorage( * Returns the next higher priority message which all their triggers are true. */ fun getNextMessage(availableMessages: List): Message? { + val jexlCache = HashMap() val helper = gleanPlumb.createMessageHelper(customAttributes) - var message = availableMessages.firstOrNull { - isMessageEligible(it, helper) + val message = availableMessages.firstOrNull { + isMessageEligible(it, helper, jexlCache) } ?: return null - if (isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) { - messagingFeature.recordExposure() + // Check this isn't an experimental message. If not, we can go ahead and return it. + if (!isMessageUnderExperiment(message, nimbusFeature.messageUnderExperiment)) { + return message + } + // If the message is under experiment, then we need to record the exposure + messagingFeature.recordExposure() - if (message.data.isControl) { - message = availableMessages.firstOrNull { - !it.data.isControl && isMessageEligible(it, helper) - } ?: return null + // If this is an experimental message, but not a placebo, then just return the message. + return if (!message.data.isControl) { + message + } else { + // This is a control, so we need to either return the next message (there may not be one) + // or not display anything. + when (getOnControlBehavior()) { + ControlMessageBehavior.SHOW_NEXT_MESSAGE -> availableMessages.firstOrNull { + // There should only be one control message, and we've just detected it. + !it.data.isControl && isMessageEligible(it, helper, jexlCache) + } + ControlMessageBehavior.SHOW_NONE -> null } } - return message } /** @@ -136,7 +150,7 @@ class NimbusMessagingStorage( @VisibleForTesting internal fun isMessageUnderExperiment(message: Message, expression: String?): Boolean { - return when { + return message.data.isControl || when { expression.isNullOrBlank() -> { false } @@ -152,19 +166,26 @@ class NimbusMessagingStorage( @VisibleForTesting internal fun isMessageEligible( message: Message, - helper: GleanPlumbMessageHelper + helper: GleanPlumbMessageHelper, + jexlCache: MutableMap = mutableMapOf() ): Boolean { return message.triggers.all { condition -> - try { - helper.evalJexl(condition) - } catch (e: NimbusException.EvaluationException) { - reportMalformedMessage(message.id) - logger.info("Unable to evaluate $condition") - false - } + jexlCache[condition] + ?: try { + helper.evalJexl(condition).also { result -> + jexlCache[condition] = result + } + } catch (e: NimbusException.EvaluationException) { + reportMalformedMessage(message.id) + logger.info("Unable to evaluate $condition") + false + } } } + @VisibleForTesting + internal fun getOnControlBehavior(): ControlMessageBehavior = nimbusFeature.onControl + private suspend fun addMetadata(id: String): Message.Metadata { return metadataStorage.addMetadata( Message.Metadata( diff --git a/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt index 45e1a45fa..625e8001d 100644 --- a/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddleware.kt @@ -73,7 +73,7 @@ class MessagingMiddleware( val newMessage = oldMessage.copy( metadata = newMetadata ) - val newMessages = if (newMetadata.displayCount < oldMessage.data.maxDisplayCount) { + val newMessages = if (newMetadata.displayCount < oldMessage.maxDisplayCount) { updateMessage(context, oldMessage, newMessage) } else { consumeMessageToShowIfNeeded(context, oldMessage) diff --git a/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt b/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt index f8c26dac8..ae2f67836 100644 --- a/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/gleanplumb/DefaultMessageControllerTest.kt @@ -93,7 +93,7 @@ class DefaultMessageControllerTest { @Test fun `WHEN calling onMessageDisplayed THEN report to the messageManager`() { - val data = MessageData(_context = testContext, maxDisplayCount = 1) + val data = MessageData(_context = testContext) val message = mockMessage(data) controller.onMessageDisplayed(message) @@ -106,7 +106,7 @@ class DefaultMessageControllerTest { private fun mockMessage(data: MessageData = MessageData(_context = testContext)) = Message( id = "id", data = data, - style = mockk(), + style = mockk(relaxed = true), action = "action", triggers = emptyList(), metadata = Message.Metadata( diff --git a/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt b/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt index 316cf18ca..8bc94634b 100644 --- a/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt +++ b/app/src/test/java/org/mozilla/fenix/gleanplumb/NimbusMessagingStorageTest.kt @@ -9,9 +9,7 @@ import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify -import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest -import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -26,6 +24,7 @@ import org.mozilla.experiments.nimbus.internal.FeatureHolder import org.mozilla.experiments.nimbus.internal.NimbusException import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.nimbus.ControlMessageBehavior.SHOW_NEXT_MESSAGE import org.mozilla.fenix.nimbus.MessageData import org.mozilla.fenix.nimbus.Messaging import org.mozilla.fenix.nimbus.StyleData @@ -40,7 +39,6 @@ class NimbusMessagingStorageTest { private lateinit var gleanPlumb: GleanPlumbInterface private lateinit var messagingFeature: FeatureHolder private lateinit var messaging: Messaging - private val coroutineScope = TestCoroutineScope() private var malformedWasReported = false private val reportMalformedMessage: (String) -> Unit = { malformedWasReported = true @@ -195,13 +193,12 @@ class NimbusMessagingStorageTest { ) val messages = mapOf( "shown-many-times-message" to createMessageData( - style = "high-priority", - maxDisplayCount = 2 + style = "high-priority" ), "normal-message" to createMessageData(style = "high-priority"), ) val styles = mapOf( - "high-priority" to createStyle(priority = 100), + "high-priority" to createStyle(priority = 100, maxDisplayCount = 2), ) val metadataStorage: MessageMetadataStorage = mockk(relaxed = true) val messagingFeature = createMessagingFeature( @@ -253,7 +250,7 @@ class NimbusMessagingStorageTest { @Test fun `WHEN calling updateMetadata THEN delegate to metadataStorage`() = runBlockingTest { - storage.updateMetadata(mockk()) + storage.updateMetadata(mockk(relaxed = true)) coEvery { metadataStorage.updateMetadata(any()) } } @@ -294,9 +291,10 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN a null or black expression WHEN calling isMessageUnderExperiment THEN return false`() { val message = Message( - "id", mockk(), + "id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), emptyList(), Message.Metadata("id") ) @@ -309,9 +307,10 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN messages id that ends with - WHEN calling isMessageUnderExperiment THEN return true`() { val message = Message( - "end-", mockk(), + "end-", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), emptyList(), Message.Metadata("end-") ) @@ -324,9 +323,10 @@ class NimbusMessagingStorageTest { @Test fun `GIVEN message under experiment WHEN calling isMessageUnderExperiment THEN return true`() { val message = Message( - "same-id", mockk(), + "same-id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), emptyList(), Message.Metadata("same-id") ) @@ -340,9 +340,10 @@ class NimbusMessagingStorageTest { fun `GIVEN an eligible message WHEN calling isMessageEligible THEN return true`() { val helper: GleanPlumbMessageHelper = mockk(relaxed = true) val message = Message( - "same-id", mockk(), + "same-id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), listOf("trigger"), Message.Metadata("same-id") ) @@ -358,9 +359,10 @@ class NimbusMessagingStorageTest { fun `GIVEN a malformed trigger WHEN calling isMessageEligible THEN return false`() { val helper: GleanPlumbMessageHelper = mockk(relaxed = true) val message = Message( - "same-id", mockk(), + "same-id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), listOf("trigger"), Message.Metadata("same-id") ) @@ -376,9 +378,10 @@ class NimbusMessagingStorageTest { fun `GIVEN none available messages are eligible WHEN calling getNextMessage THEN return null`() { val spiedStorage = spyk(storage) val message = Message( - "same-id", mockk(), + "same-id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), listOf("trigger"), Message.Metadata("same-id") ) @@ -394,9 +397,10 @@ class NimbusMessagingStorageTest { fun `GIVEN an eligible message WHEN calling getNextMessage THEN return the message`() { val spiedStorage = spyk(storage) val message = Message( - "same-id", mockk(), + "same-id", + mockk(relaxed = true), action = "action", - mock(), + mockk(relaxed = true), listOf("trigger"), Message.Metadata("same-id") ) @@ -441,6 +445,7 @@ class NimbusMessagingStorageTest { val controlMessageData: MessageData = mockk(relaxed = true) every { messageData.isControl } returns false + every { spiedStorage.getOnControlBehavior() } returns SHOW_NEXT_MESSAGE every { controlMessageData.isControl } returns true val message = Message( @@ -473,14 +478,12 @@ class NimbusMessagingStorageTest { private fun createMessageData( action: String = "action-1", style: String = "style-1", - triggers: List = listOf("trigger-1"), - maxDisplayCount: Int = 5 + triggers: List = listOf("trigger-1") ): MessageData { val messageData1: MessageData = mockk(relaxed = true) every { messageData1.action } returns action every { messageData1.style } returns style every { messageData1.trigger } returns triggers - every { messageData1.maxDisplayCount } returns maxDisplayCount return messageData1 } @@ -506,9 +509,10 @@ class NimbusMessagingStorageTest { return messagingFeature } - private fun createStyle(priority: Int = 1): StyleData { + private fun createStyle(priority: Int = 1, maxDisplayCount: Int = 5): StyleData { val style1: StyleData = mockk(relaxed = true) every { style1.priority } returns priority + every { style1.maxDisplayCount } returns maxDisplayCount return style1 } } diff --git a/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt index 6c419f122..3408f6ccc 100644 --- a/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/gleanplumb/state/MessagingMiddlewareTest.kt @@ -37,6 +37,7 @@ import org.mozilla.fenix.gleanplumb.MessagingState import org.mozilla.fenix.gleanplumb.NimbusMessagingStorage import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.nimbus.MessageData +import org.mozilla.fenix.nimbus.StyleData @RunWith(FenixRobolectricTestRunner::class) class MessagingMiddlewareTest { @@ -276,12 +277,13 @@ class MessagingMiddlewareTest { @Test fun `GIVEN a message with that not surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN update the available messages and the updateMetadata`() { + val style: StyleData = mockk(relaxed = true) val oldMessageData: MessageData = mockk(relaxed = true) val oldMessage = Message( "oldMessage", oldMessageData, action = "action", - mockk(relaxed = true), + style, listOf("trigger"), Message.Metadata("same-id", displayCount = 0) ) @@ -289,7 +291,7 @@ class MessagingMiddlewareTest { val spiedMiddleware = spyk(middleware) every { spiedMiddleware.now() } returns 0 - every { oldMessageData.maxDisplayCount } returns 2 + every { style.maxDisplayCount } returns 2 every { spiedMiddleware.updateMessage( middlewareContext, @@ -307,12 +309,13 @@ class MessagingMiddlewareTest { @Test fun `GIVEN a message with that surpassed the maxDisplayCount WHEN onMessagedDisplayed THEN remove the message and consume it`() { + val style: StyleData = mockk(relaxed = true) val oldMessageData: MessageData = mockk(relaxed = true) val oldMessage = Message( "oldMessage", oldMessageData, action = "action", - mockk(relaxed = true), + style, listOf("trigger"), Message.Metadata("same-id", displayCount = 0) ) @@ -320,7 +323,7 @@ class MessagingMiddlewareTest { val spiedMiddleware = spyk(middleware) every { spiedMiddleware.now() } returns 0 - every { oldMessageData.maxDisplayCount } returns 1 + every { style.maxDisplayCount } returns 1 every { spiedMiddleware.consumeMessageToShowIfNeeded( middlewareContext, diff --git a/nimbus.fml.yaml b/nimbus.fml.yaml index 78983ec14..ac378e35f 100644 --- a/nimbus.fml.yaml +++ b/nimbus.fml.yaml @@ -94,51 +94,77 @@ features: expressions. Each entry maps to a valid JEXL expression. type: Map - default: - english-speaking: "'en' in locale" - ALWAYS: "true" - NEW_USER: "days_since_install < 7" - + default: {} styles: description: > A map of styles to configure message appearance. type: Map - default: - urgent: - background-color: red - text-color: white - button-background: bright-blue - button-text-color: white - priority: 70 - warning: - background-color: cyan, - text-color: black, - button-background: bright-blue - button-text-color: white - priority: 55 - default: - background-color: blue - text-color: white - button-background: bright-blue - button-text-color: white - priority: 50 - excited: - background-color: blue - text-color: white - button-background: bright-blue - button-text-color: white - priority: 60 + default: {} actions: type: Map description: A growable map of action URLs. - default: - OPEN_SYNC_SETTINGS: firefox://settings/sync - OPEN_POCKET_SETTINGS: https:///getpocket.com/settings?fxa={fxa-token} - OPEN_SETTINGS: ://settings - + default: {} + on-control: + type: ControlMessageBehavior + description: What should be displayed when a control message is selected. + default: show-next-message defaults: + - value: + triggers: + USER_RECENTLY_INSTALLED: days_since_install < 7 + USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update + USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale) + USER_EN_SPEAKER: "'en' in locale" + USER_DE_SPEAKER: "'de' in locale" + USER_FR_SPEAKER: "'fr' in locale" + DEVICE_ANDROID: os == 'Android' + DEVICE_IOS: os == 'iOS' + ALWAYS: "true" + NEVER: "false" + actions: + ENABLE_PRIVATE_BROWSING: ://enable_private_browsing + INSTALL_SEARCH_WIDGET: ://install_search_widget + MAKE_DEFAULT_BROWSER: ://make_default_browser + VIEW_BOOKMARKS: ://urls_bookmarks + VIEW_COLLECTIONS: ://home_collections + VIEW_HISTORY: ://urls_history + VIEW_HOMESCREEN: ://home + OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility + OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager + OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data + OPEN_SETTINGS_LOGINS: ://settings_logins + OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications + OPEN_SETTINGS_PRIVACY: ://settings_privacy + OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine + OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection + OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers + OPEN_SETTINGS: ://settings + TURN_ON_SYNC: ://turn_on_sync + styles: + DEFAULT: + priority: 50 + max-display-count: 5 + PERSISTENT: + priority: 50 + max-display-count: 20 + WARNING: + priority: 60 + max-display-count: 10 + URGENT: + priority: 100 + max-display-count: 10 + + - channel: developer + value: + styles: + DEFAULT: + priority: 50 + max-display-count: 100 + EXPIRES_QUICKLY: + priority: 100 + max-display-count: 1 - channel: developer value: { "messages": { @@ -147,13 +173,10 @@ features: "text": "Love Firefox? Fill in our survey!", "action": "https://surveyprovider.com/survey-id/{uuid}", "trigger": [ "ALWAYS" ], - "max-display-count": 5, - "style": "warning", + "style": "DEFAULT", "button-label": "Go to the survey" } - }, - - "message-under-experiment": "my-viewpoint-survey" + } } - channel: developer value: { @@ -161,12 +184,9 @@ features: "private-tabs-auto-close": { "action": "OPEN_SETTINGS", "text": "Sharing your phone? Autoclosing private tabs is for you!", - "style": "warning", "trigger": [ - "NEW_USER", - "first-private-tabs-opened" - ], - "max-display-count": 5 + "USER_RECENTLY_INSTALLED" + ] } }, @@ -175,23 +195,15 @@ features: - channel: developer value: { "triggers": { - "ireland": "'IE' in locale" + "USER_IE_COUNTRY": "'IE' in locale" }, "styles": { "irish-green": { - "background-color": "green", - "text-color": "dark-green", - "button-background": "foo", - "button-text-color": "very-green", "priority": 50 } }, - "actions": { - "OPEN_SETTINGS": "://settings" - }, - "messages": { "eu-tracking-protection-for-ireland": { "action": "OPEN_SETTINGS", @@ -199,9 +211,8 @@ features: "style": "irish-green", "trigger": [ "NEW_USER", - "ireland" - ], - "max-display-count": 5 + "USER_IE_COUNTRY" + ] } }, @@ -220,7 +231,7 @@ types: and call to action. fields: action: - type: String + type: Text description: > A URL of a page or a deeplink. This may have substitution variables in. @@ -239,7 +250,6 @@ types: type: Boolean description: "Indicates if this message is the control message, if true shouldn't be displayed" default: false - button-label: type: Option description: > @@ -251,20 +261,7 @@ types: description: > The style as described in a `StyleData` from the styles table. - default: "default" - max-display-count: - type: Int - description: > - The number of sessions the user is - shown the message before the message expires. - If the user is able to dismiss the message, - this is the number of times they dismiss - it before the message expires. - A count of -1 means that the message will - never expire. - default: 5 - # These triggers aren't part of the MVP, - # so may be excluded. + default: DEFAULT trigger: type: List description: > @@ -277,31 +274,27 @@ types: A group of properities (predominantly visual) to describe the style of the message. fields: - # How the string is transformed into a color is unspecified - background-color: - type: String - description: The color of the background. - default: "blue" - text-color: - type: String - description: The color of the background. - default: "white" - button-background: - type: String - description: The color of the button background. - default: "bright-blue" - button-text-color: - type: String - description: The color of the button text. - default: "white" priority: type: Int description: > The importance of this message. 0 is not very important, 100 is very important. default: 50 + max-display-count: + type: Int + description: > + How many sessions will this message be shown to the user + before it is expired. + default: 5 enums: + ControlMessageBehavior: + description: An enum to influence what should be displayed when a control message is selected. + variants: + show-next-message: + description: The next eligible message should be shown. + show-none: + description: The surface should show no message. HomeScreenSection: description: The identifiers for the sections of the homescreen. variants: