Message System

Message system was implemented to allow sending emergency messages to Trezor Suite app to a user with specific stack.

Example messages

Issue on Github

Types of in-app messages

There are four ways of displaying message to a user.

  • banner
    • looks like a cookie bar above the page
  • modal
    • TODO: missing implementation
  • context
    • messages on specific places in app - high level (e.g. settings page)
    • TODO: missing implementation
  • super-context
    • messages on specific places in the app - low level (e.g. category in settings page)
    • TODO: missing implementation

Implementation

Config

The system of messages is based on a configuration file in which messages with specific conditions ​are described. If specific conditions are satisfied, the message is shown to a user.

Current configuration file is located in packages/suite-data/src/message-system/config. Its name is config.vX.json. The X express current version messaging system.

The config is fetched at launch of the application and then every 6 hours. It remembers the previously fetched config to inform the user even if he is offline. For this reason, the latest available config during build time is bundled with the application.

If fetching of a new config fails, the fetching process is repeated every 1 hour.

Schema

The configuration structure is specified in JSON file using JSON schema. The file can be found in packages/suite-data/src/message-system/schema folder. Its name is config.schema.vX.json.

We use JSON schema for 2 reasons:

  • generating TypeScript types
  • validating configuration file

Types

Types are generated from JSON-schema during the build:libs process or can be generated manually by yarn workspace @trezor/suite-data msg-system-types. A messageSystem.ts file is created in packages/suite/src/types/suite.

  • This file should never be changed manually.
  • This file is committed into the repository.

Signing

To ensure the authenticity of a configuration file, JSON Web Signatures are used. The configuration file is signed by a private key using elliptic curves (ES256) and the data specified in the config are used. The authenticity is verified on client side using corresponding public key.

CI job

Validation

  • Validation of configuration file is performed in CI job in validation phase. It is used to detect possible structure and semantic errors.
  • It can be run locally by yarn workspace @trezor/suite-data msg-system-validate-config script in suite-data.

Signing

  • Signing of the configuration file is performed in CI job in prebuild phase.
  • The result is uploaded to https://data.trezor.io/config/$environment/config.vX.json and saved into suite-data/files/message-system to be bundled with application. For example, on localhost, the config is available at http://localhost:3000/static/message-system/config.vX.jws.
  • It can be run manually by yarn workspace @trezor/suite-data msg-system-sign-config script in suite-data. The resulting JWS is stored in packages/suite-data/files/message-system/ in config.vX.jws file.
  • Development public and private keys are baked into project structure. For production environment, these keys are replaced by CI job by production keys. This production CI job is activated on codesign branches.
  • Development private key can be found in suite-data/src/message-system/constants, the public key can be found in suite-web and suite-desktop in next.config.js file. Change when tschuss-next is merged

Versioning

If changes made to the message system are incompatible with the previous version, the version number should be bumped in messageSystemConstants.ts in suite package as well as in suite-data package in message-system/constants. Also in ci/packages/suite-data.yml file.

Config Structure

Structure of config, types and optionality of specific keys can be found in the schema or in generated types. Example config is commented below.

{
    // Version of message system implementation. Bump if new version is not backward compatible.
    "version": 1, 
    // Datetime in ISO8601 when was config created.
    "timestamp": "2021-03-03T03:48:16+00:00",
    // Version of config. New config is accepted only if sequence number is higher.
    "sequence": 1, 
    "actions": [
        {
            /*
            - User's stack has to match one of the condition objects to show this message.
            - The bitwise operation is OR.
            */
            "conditions": [ 
                /* 
                - All keys are optional (duration, os, environment, browser, settings, transport, 
                  devices, architecture (To be implemented))
                - If a value is specified then all its subkeys have to be specified 
                - The bitwise operation is AND.
                */
                {
                    /* 
                    - Datetime in ISO8601 from / to which date this message is valid.
                    - If duration category is used, then both times have to be set.
                    */
                    "duration": {
                        "from": "2021-03-01T12:10:00.000Z",
                        "to": "2022-01-31T12:10:00.000Z"
                    },
                    /* 
                    For os, environment, browser and transport.
                    - Values can be array, string or null.
                    - Semver npm library is used for working with versions https://www.npmjs.com/package/semver.
                    - "*" = all versions; "!" = not for this type of browser/os/...
                    - Options: gte, lt, ranges, tildes, carets,... are supported, see semver lib for more info.
                    */
                    "os": {
                        "macos": [
                            "10.14",
                            "10.18",
                            "11"
                        ],
                        "linux": "<20.04",
                        "windows": "!",
                        "android": "*",
                        "ios": "13"
                    },
                    "environment": {
                        "desktop": "<21.5",
                        "mobile": "!",
                        "web": "<22"
                    },
                    "browser": {
                        "firefox": [
                            "82",
                            "83"
                        ],
                        "chrome": "*",
                        "chromium": "!"
                    },
                    "transport": {
                        "bridge": [
                            "2.0.30",
                            "2.0.27"
                        ],
                        "webusbplugin": "*"
                    },
                    /*
                    - If key is not available (undefined), then it can be whatever.
                    - Currently supported keys are "tor" and coin symbols from "enabledNetworks".
                    - The bitwise operation is OR.
                    */
                    "settings": [
                        { 
                            "tor": true,
                            "btc": true
                        },
                        {
                            "tor": false,
                            "ltc": true
                        }
                    ],
                    // Empty device array is targeting users without a connected device.
                    "devices": [ 
                        {
                            // Possible values: "1" or "T"
                            "model": "1", 
                            "firmware": "1.9.4",
                            "vendor": "trezor.io"
                        }
                    ]
                }
            ],
            "message": {
                // Used for remembering dismissed messages.
                "id": "0f3ec64d-c3e4-4787-8106-162f3ac14c34",
                /*
                - Existing banners have defined priorities. 
                - The range is 0 to 100.
                */
                "priority": 100,
                // When user closes the message, it will never show again.
                "dismissible": true,
                /* 
                Variants:
                - info (blue)
                - warning (orange)
                - critical (red)
                */
                "variant": "warning",
                // Options: banner, modal, context, super-context
                "category": "banner",
                /*
                - Message shown to a user. 
                - Current implementation uses only "en-GB".
                */
                "content": {
                    "en-GB": "New Trezor firmware is available!",
                    "de-DE": "Neue Trezor Firmware ist verfügbar!"
                },
                // Call to action. Used only for banner and context.
                "cta": {
                    /*
                    Options: "internal-link" or "external-link"
                    - internal-link is route name, see routes.ts file
                    - external-link is url address
                    */
                    "action": "internal-link",
                    // Route name or url address according to action.
                    "link": "firmware-index",
                    /*
                    - Label of call to action button shown to a user. 
                    - Current implementation uses only "en-GB".
                    */
                    "label": {
                        "en-GB": "Update now",
                        "de-DE": "Jetzt aktualisieren"
                    }
                },
                // Used only for modals. (To be implemented)
                "modal": {
                    "title": {
                        "en-GB": "Update now",
                        "de-DE": "Jetzt aktualisieren"
                    },
                    "image": "https://example.com/example.png"
                },
                // Used only for context and super-context. (To be implemented)
                "context": {
                    "domain": [
                        "coins.*.receive",
                        "coins.btc"
                  ]
                }
            }
        }
    ]
}

Application steps

  1. Config is fetched on load of application and is stored in Redux state. To be persisted between sessions, is is mirrored into IndexDB.
  2. Conditions of config are evaluated on specific Redux actions. messageSystemMiddleware.ts
  3. If conditions of message satisfies user's stack, the message is accordingly propagated. If it is dismissible, its id is saved to Redux state (IndexDB) on close, to avoid displaying next time.