Add defaults to FML, add control behaviour

This commit is contained in:
jhugman 2022-03-27 10:13:49 -04:00 committed by mergify[bot]
parent 480ab3dca7
commit 2b36ca75bf
12 changed files with 263 additions and 225 deletions

View File

@ -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."
}
}
}
}

56
.experimenter.yaml Normal file
View File

@ -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."

View File

@ -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,
)
}
}

View File

@ -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)
)
)
}
}

View File

@ -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))

View File

@ -27,6 +27,12 @@ data class Message(
val triggers: List<String>,
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.
*

View File

@ -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<Messaging>
private val messagingFeature: FeatureHolder<Messaging>,
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>): Message? {
val jexlCache = HashMap<String, Boolean>()
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<String, Boolean> = 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(

View File

@ -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)

View File

@ -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(

View File

@ -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<Messaging>
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<String> = listOf("trigger-1"),
maxDisplayCount: Int = 5
triggers: List<String> = 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
}
}

View File

@ -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,

View File

@ -94,51 +94,77 @@ features:
expressions. Each entry maps to a
valid JEXL expression.
type: Map<String, String>
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<String, StyleData>
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<String, String>
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<Text>
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<String>
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: