# Documentation Home

The Mobile Growth SDK

<table data-column-title-hidden data-view="cards" data-full-width="false"><thead><tr><th></th><th></th><th data-hidden></th><th data-hidden data-type="content-ref"></th><th data-hidden data-type="files"></th><th data-hidden data-card-cover data-type="files"></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>Quick Start</strong></td><td>Get started in minutes</td><td></td><td></td><td></td><td><a href="/files/Z9PHZNyW6ZkVwsZ4v7Dk">/files/Z9PHZNyW6ZkVwsZ4v7Dk</a></td><td><a href="/pages/dOpnGUCxrkR91veweNAi">/pages/dOpnGUCxrkR91veweNAi</a></td></tr><tr><td><strong>What is Critical Moments?</strong></td><td>A primer on our SDK</td><td></td><td><a href="/pages/w3L2WDmLHLvM2EGunWJh">/pages/w3L2WDmLHLvM2EGunWJh</a></td><td><a href="/files/ViYF9cfu8DxsW5h5ZaDI">/files/ViYF9cfu8DxsW5h5ZaDI</a></td><td><a href="/files/x1nKXxBjp3Fgv6nU2fgb">/files/x1nKXxBjp3Fgv6nU2fgb</a></td><td><a href="/pages/w3L2WDmLHLvM2EGunWJh">/pages/w3L2WDmLHLvM2EGunWJh</a></td></tr><tr><td><strong>Concepts Overview</strong></td><td>All the tools at your disposal</td><td></td><td><a href="/pages/szsgFcnGfFB9Mxu1cCFf">/pages/szsgFcnGfFB9Mxu1cCFf</a></td><td><a href="/files/ViYF9cfu8DxsW5h5ZaDI">/files/ViYF9cfu8DxsW5h5ZaDI</a></td><td><a href="/files/7afOL0FaQhUhzSip368n">/files/7afOL0FaQhUhzSip368n</a></td><td><a href="/pages/szsgFcnGfFB9Mxu1cCFf">/pages/szsgFcnGfFB9Mxu1cCFf</a></td></tr></tbody></table>


# What is Critical Moments?

If "this" then "that" for mobile apps

Critical moments is an if "this" then "that" system for mobile apps. You can install our SDK for iOS to unlock a powerful set of tools to help grow your business, speed up development, and increase user satisfaction.

### "This" Triggers

A number of triggers can kick off an action:

* [Events](/events/event-overview): trigger an action when a user performs an action, or built-in actions occur (like `app_start`)
* [Conditions](/conditional-targeting/intro-to-conditions): build powerful conditional statements that can evaluate over 100 realtime device states, custom properties, functions and more. For example: `!dark_mode && (device_brightness < 0.1 || hour_of_day > 21)` could be a trigger for promoting your new "dark mode" feature
* Powerful combinations: combine events, realtime conditions, and even past device state history to build advanced targeting for when actions fire

### "That" Actions

When a condition is met, you can perform a wide array of actions:

* Ask users to subscribe or buy: firing your upsell prompts at the right moment can have a 72% increase on conversion rate, while reducing alert fatigue and user distraction
* Ask users to rate your app when they are most likely to give you a great review: after a positive interaction, and while they aren't distrated
* Launch actions built into our SDK like announcement banners,  modal popovers, alerts, web link, deep links, and more
* Enable or disable [feature flags](/feature-flags/conditional-feature-flags) to control any aspect of your app. Rollout features over time, or even fix bugs after they ship
* Custom actions: link into actions in your codebase

For details on all the actions available, see the [actions overview](/actions-in-app-messaging/actions-overview).

### Update anytime from the cloud

[From the cloud you can update your action, conditions, triggers, or how they connect](/remote-control-service) at anytime without needing app store updates. You can even add entirely new messages, announcements or features from the cloud.

Shipping updates any time unlocks powerful set of use cases:

* AB test important optimizations like when to ask users to make a purchase or rate your app
* Fix bugs after they ship with conditional feature flags
* Let users know they should update their app (for old builds, or major bugs)
* Targeted user messaging: sending a message to the right user, and the right moment, including in builds that have shipped to app store long ago
* Diffuse outages by letting users know you're on it, and publishing proactive updates

Critical Moments is built as what we call "[Trustless SaaS](/trustless-saas)": software designed to be remotely controlled, without tracking your users or needing to trust a 3rd party service provider for correctness, security, or uptime SLAs.

### Privacy Built In

While Critical Moments gives you an deep array of data to target users and optimize, that data never leaves the user's device. Conditional statements are evaluated locally. Our privacy-centric architecture makes it possible to optimize on much more data per user, including realtime signals.

With Critical Moments, powerful targeting and optimization doesn't have to mean compromising user privacy.

### Get Started

Ready to go? Jump to our [**quick start guide**](/quick-start) to get rolling.


# Quick Start

Get up and running! 👟👟

Let's get you started! This guide will walk you through the most important steps

### Install the SDK 🔧

Our iOS SDK is distributed as as a SPM package (Swift Package Manager).  Simply add a SPM dependency in Xcode on `https://github.com/criticalmoments/criticalmoments`.

<details>

<summary>Detailed steps for adding SPM package</summary>

* Open your app project in Xcode. Click "File" > "Add Package Dependencies..."&#x20;
* Paste the following URL in the search box in the top right: `https://github.com/criticalmoments/criticalmoments` and select the Critical Moments package. We suggest leaving the default settings for dependency rules: (Branch=main). We only merge stable releases to our main branch.&#x20;
* Select your iOS app project in the "Add to Project..." dropdown
* Click "Add Package"
* Select your iOS app target in the "Add to Target" dropdown
* Click "Add Package"

</details>

### Create your config file 📄

The CM config file defines how the SDK integrates into your app. Let's get started with a template integration:

* Download our template  `cmDevConfig.json` ([link](https://raw.githubusercontent.com/CriticalMoments/CriticalMoments/refs/heads/vnext/ios/sample_app/SampleApp/demoConfig.json)), which includes a demo of our features on launch. We'll remove the demo later, but it's helpful to test the integration.
* Set the appId on the 3rd line to your app's bundle ID.
* Add the file to your Xcode project.&#x20;

### Integrate the SDK into your app 🧩

Add the following code snippet into your app.

* Put it somewhere that will run right after app startup, such as AppDelegate.didFinishLaunchingWithOptions
* Replace `"cmDevConfig.json"` if you used a different name for the config file.
* The console will show a warning for now, but this will be resolved in a later step of this guide

{% tabs %}
{% tab title="Swift" %}
In your App Delegate imports add:

```swift
import CriticalMoments
```

Add the following code to your app launch. The location depends on the type of app you have:

* &#x20;App Delegate based apps, call from `application(_:didFinishLaunchingWithOptions:)`&#x20;
* SwiftUI lifecycle apps, call from `YourApp.init`

```swift
CriticalMoments.shared().setDevelopmentConfigName("cmDevConfig.json")
// We'll add this later. This must be set before releasing app.
// CriticalMoments.shared().setReleaseConfigUrl("YOUR_URL")
CriticalMoments.shared().start()
```

{% endtab %}

{% tab title="Objective-C" %}
In App Delegate imports section add:

```objectivec
@import CriticalMoments;
```

In App Delegate `application:didFinishLaunchingWithOptions:` add:

```objectivec
[CriticalMoments.shared setDevelopmentConfigName:@"cmDevConfig.json"];
// We will fill this in during a later step. This must be set before releasing to app store
// [CriticalMoments.shared setReleaseConfigUrl:@"YOUR_URL"];
[CriticalMoments.shared start];
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Startup work will be dispatched to background threads, and will not impact your app's startup time.
{% endhint %}

### Update Your Info.plist :notebook\_with\_decorative\_cover:

Critical Moments requires a few updates to your Info.plist file. We've made a script to make this easy. Run the following command in your project folder in terminal, and then follow the instructions. It will explain the changes it's making, and won't make any changes until you confim:

{% code overflow="wrap" %}

```sh
bash <(curl -fsSL https://raw.githubusercontent.com/CriticalMoments/CriticalMoments/refs/heads/vnext/ios/tools/setup_tool/setup_cm.sh)
```

{% endcode %}

<details>

<summary>Manual Instructions</summary>

Don't like magic scripts? We get that. You can also update the Info.plist manually.

#### Part 1: Enable Processing For Smart Notifications

Features like [smart notifications](/notifications/smart-delivery) require running your app to run in the background for brief periods, to check for ideal conditions. To setup background work:

1. In Xcode > project editor > Signing & Capabilities > Add (+) > Background Modes check the boxes for "Background fetch" and "Background processing". See [Apple guide](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app?language=objc).&#x20;
2. In your `Info.plist` add an array "Permitted background task scheduler identifiers" (`BGTaskSchedulerPermittedIdentifiers`)  with these two values: `io.criticalmoments.bg_fetch` and `io.criticalmoments.bg_process` . See [Apple guide](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app?language=objc).&#x20;

#### Part 2: Set Bluetooth Reason String

Some optional properties like `has_bt_headphones` and `bluetooth_permission` use the system's bluetooth APIs. These won't be called unless you include them in your config. However Apple will detect them, and want a description in the Info.plist file.

Add an entry for `NSBluetoothAlwaysUsageDescription` to your Info.plist describing your bluetooth usage. If you app uses bluetooth directly, keep the description you have. If you don't otherwise use bluetooth, add a general description, such as "Used to show messages when not using peripherals".&#x20;

</details>

### Test the Integration 🧪

Launch your app. If the integration is working, you should see a CM demo when the app starts; this demo was included in the template file you downloaded earlier.&#x20;

If there are any issues, check the logs. When running a debug build, Critical Moments will log any issues preventing the SDK from working.

{% hint style="info" %}
Note: while CM now works in debug mode, more steps below are needed before releasing to the App Store.&#x20;
{% endhint %}

### Explore the Demo :telescope:

Try out the demo to see each feature of the SDK integrated into your app. To re-lauch the demo, simply restart the app.&#x20;

Once your done with the demo, you can delete the demo content in `cmDevConfig.json`; just be sure leave the `configVersion` and `appId` json entries.

{% hint style="success" %}
**This demo is implemented entirely in config**

It shows how you can add many features to your app without writing code, including fully native UI, integrations into native APIs, powerful conditions, and notifications. If you're curious how any part of the demo works, simply read `cmDevConfig.json`.

In a later step, we'll setup your app to update this config over-the-air, without app updates.
{% endhint %}

### Log important events (optional, recommended) 🎪

The more Critical Moments knows about your user's behaviours, the better you can target and optimize with Critical Moments.&#x20;

We recommend you log important events, including the important actions your users performs in app. See the [events documentation](/events/event-overview) for the suggested events and details.

{% tabs %}
{% tab title="Swift Example" %}

```swift
CriticalMoments.shared().sendEvent("completed_game_level")
```

{% endtab %}

{% tab title="Objective-C Example" %}

<pre class="language-objectivec"><code class="lang-objectivec"><strong>[CriticalMoments.shared sendEvent:@"completed_game_level"];
</strong></code></pre>

{% endtab %}
{% endtabs %}

### Add custom properties (optional, recommended) 📅

Properties are datapoints used in targeting. The more useful data you add, the more powerful the targeting system can be.

We recommend you add the important properties like account\_creation\_date, has\_subscription, and others. See the [custom properties documentation](/conditional-targeting/custom-properties) for suggested custom properties, and API reference.

{% tabs %}
{% tab title="Swift Example" %}

```swift
try? CriticalMoments.shared().setIntegerProperty(42, forKey: "max_game_level")
```

{% endtab %}

{% tab title="Objective-C Example" %}

```objectivec
[CriticalMoments.shared registerIntegerProperty:42 
               forKey:@"max_game_level" 
               error:nil];
```

{% endtab %}
{% endtabs %}

### Setup a cloud config file ☁️

For local development you can use your local JSON config file built into the app binary. However, production builds must use a signed config file, hosted on the web.

Read the [Remote Control Docs](/remote-control-service) for the best practices of picking a hosting provider, signing, and deploying your config.

Once deployed, set the release config URL in your App Delegate, and uncomment the call to `setReleaseConfigUrl("YOUR_URL")`.

You can test your app using the production config by running a "release" build instead of a "debug" build in XCode.

{% hint style="success" %}
**Update Anytime**

Deploying the config file can be done outside of app releases, allowing you to update your apps behaviour without waiting for app reviews, or users to update their builds!
{% endhint %}

### Request Notification Permissions :mailbox\_with\_mail:

If you ever want to show the user notifications, you'll need to ask the user for permission first.

Requesting permission with the Critical Moments helper method instead of the system `requestAuthorization` method is beneficial as it will schedule any CM notifications after approval. An optional callback will be called after the user approves/denies notifications.

{% tabs %}
{% tab title="Swift Example" %}

```swift
CriticalMoments.shared().requestNotificationPermission()
```

{% endtab %}

{% tab title="Objective-C Example" %}

```objectivec
[CriticalMoments.shared
        requestNotificationPermissionWithCompletionHandler:nil];
```

{% endtab %}
{% endtabs %}

### Customize your Integration 🏗️

Remove the demo content from your copy of `cmDevConfig.json` and start building using the powerful combination of conditionals, notifications, events, properties, feature flags, and actions!

Some good places to start and get ideas:

* Download our [**starter template** with 13 features most apps need](https://criticalmoments.io/blog/mobile_app_growth_plan_in_json). Our [blog post](https://criticalmoments.io/blog/mobile_app_growth_plan_in_json) describes how each works, and how to enable them.
* Read our [Concepts Overview](/concepts-overview) to understand all the tools at your disposal.


# Concepts Overview

All the tools at your disposal

This is a quick summary of all of the concepts in the Critical Moments SDK. Each is documented in detail later in this developer guide.

* [**Notifications**](/notifications/intro-to-notifications): delivery mobile notifications to your users, without a push server
* [**Smart Notifications**](/notifications/smart-delivery): wait for the ideal moment to delivery a notification. For example, when the user is holding their device and at home.
* [**Conditionals**](/conditional-targeting/intro-to-conditions) are boolean string expressions that can be evaluated with the current state of the users device or event history. For example `device_orientations == 'face_up' || has_wifi`. They can be used for advanced targeting or optimizing.
* **Properties** can be [built in](/conditional-targeting/built-in-properties), or [custom](/conditional-targeting/custom-properties) properties. They are the data points used in conditions, like `has_wifi`. &#x20;
* [**Events**](/events/event-overview) are logged when something happens, like a user action, or the app launching. They can be used to trigger actions, or you can query the event history in conditionals.
* [**The events and properties database**](/conditional-targeting/conditional-guides/event-and-property-history) tracks all past events and past property states in a private database. These can be queried in conditionals.
* [**Actions**](/actions-in-app-messaging/actions-overview) can be fired when events occur or conditions are met. A wide array of options are built into the SDK including showing modal popups, banner announcements, alerts, requesting app reviews, launching webpages, app deeplinks, and more. You can add you own custom actions, tying into parts of your app.
* [**Triggers**](/actions-in-app-messaging/triggers) bind events to actions, firing an action when an event occurs.
* [**Feature Flags**](/feature-flags/conditional-feature-flags) enable you to disable a feature without modifying your source code or requiring a redeploy. Critical Moments feature flags use the conditional system for advanced targeting
* [**Themes**](/themes/theme-overview) update the color and fonts of our action UI to match the look and feel of your brand.
* [**The Config File**](/config-file-structure) binds all of the above concepts together. Even native UI actions can be defined in the config. By defining these in config instead of code we enable AB tests, iterative optimizations and powerful updates, launched with unprecended flexibility.
* [**Remote Update**](/remote-control-service) allows you to update your app's behaviour outside of the app store release cycle. This is useful for dealing with unexpected scenarios like showing an announcement in app, outages, disabling bugs, and more.


# Remote Control / Service

Remotely control your app's behaviour without App Store updates

The behaviour of the Critical Moments SDK is controlled by a powerful [config file](/config-file-structure), which can be remotely updated from a server without app updates. Changing the SDK config unlocks a powerful set of features:

* Update the conditional targeting to optimize your revenue engine (eg: Subscription prompts)
* Update your user journey conditional targeting and messaging to improve user activation
* Update your app review prompt conditional targeting
* Push new user messaging for unexpected issues and announcements. Examples: outages, announcements, or legal compliance (new privacy policy, or EU data regulations)
* Updating which feature flags are on/off, including rolling out AB test segments, rolling back bugs, and targeting feature flags based on live-device conditions.

### Who Hosts the Remote Config?

**You do!** You can host our config anywhere your company hosts files on the web.

Other feature flag and remote-messaging tools require learning new dashboards, adding new access controls, creating new review processes, and sometimes even hosting/monitoring new services. Critical Moments allows you to use your existing tools (Git, Github, your webserver, etc)!

We offer a guide for quickly setting up [free hosting using Github Pages](/remote-control-service/host-config-on-github-pages), which should work for companies at any scale.

{% hint style="success" %}
**Trustless: Security and SLA**

Since you host the config file yourself, Critical Moments can't push updates to your config without your permission/consent.&#x20;

Further: an outage of our service can't impact your users.
{% endhint %}

### How to Host Your Config / Remote Updates

<details>

<summary>Considerations if you want to use a host other than Github</summary>

If you want to use another host for your config, here are some best practices to consider:

* Choose a hosting provider that has robust access controls, to control who on your team can update this config. Since this config can push messaging to all your users, you want to keep this secure and limit updates to trusted team members.
* If using a custom domain name, use one you own, keep secure, and intend to keep owning for the lifetime of your application.
* Choose a hosting provider which supports the [ETag header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) is best, and will reduce the amount of network traffic from your app. Almost all major hosts support this, including Github Pages, Amazon S3, Google GCP Cloud Storage, and Microsoft Azure.
* Choose hosting provider you already use, and is familiar to your dev/ops team

</details>

Once you pick a URL, update your code to point to it (in your app delegate's call to `setReleaseConfigUrl`, see the [Quick Start](/quick-start) guide). This must be done before you release to the app store.

Be sure to test everything works as desired before deploying a new config to your production host. Remember: if deploying to a config to a URL used by older app releases, ensure to not break them with new configs.

{% hint style="success" %}
**Trustless: Access Control**

Each company has different ways of securing their services and granting employees access.&#x20;

By self-hosting the config file, you can use your existing access control systems to match the way you already work, and comply with your internal security requirements.
{% endhint %}

### Trustless SaaS

Critical Moments is built as what we call "Trustless SaaS": software designed to be remotely controlled from services, without needing to trust a 3rd party service provider for correctness, security, or uptime SLAs.

[Learn more about Trustless SaaS -> ](#trustless-saas)


# Host Config on Github Pages

Use the tools you already know, with version control and automatic signing

Our suggested host for your config file is Github Pages. It has many benefits:

* Familiar tools: Git and Github
* Version control built-in
* Access controls built-in
* Branch protections to authorize who can deploy

### Setup Repository

Create a repository to host your config file.&#x20;

We suggest new dedicated repository used only for this and this guide section assumes that approach. Add your JSON config file as a file named `config.json` in the root. See detailed instructions below if needed.

{% hint style="info" %}
Github Pages is only available on public repos, or private repos if you are a paid Github user.&#x20;

If you are a free Github user, you can still use this approach with a public repository. As the config file is going to be on the public internet for customers to retrieve, you aren't exposing any additional data.
{% endhint %}

<details>

<summary>Detailed Instructions</summary>

**Using the Github UI create a new repository:**

![](/files/KipfKPU7avKVOkOvtRjl)

**Then add your config. You can use the web UI or the CLI:**

````
```
git init
git add config.json
git commit -m "current config"
git branch -M main
git remote add origin git@github.com:CriticalMoments/sample_app_config.git
git push -u origin main
```
````

</details>

### Add Custom Domain (optional)

Adding a custom domain will allow you to change to a different host at a later time, if you no longer want to use GitHub Pages as your host.

<details>

<summary>Detailed Instructions</summary>

In **Github > Repo Settings > Github Pages > Custom Domain** set domain you want to use:

![](/files/ZFPXutxGUB5exfABvyAP)

**In your DNS Provicer, setup CNAME:**

This screenshot shows Cloudflare's DNS settings, your DNS provider may look different.

<img src="/files/2qrtQPq7yB3oOPuQMwcP" alt="" data-size="original">

See [Github's full instructions](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site) for details.

**Enable “Enforce HTTPs” in Github Settings**

You may need to wait a few minutes for Github to provision a certificate. As the config can control aspects of your app, you always want to serve it over HTTPS/TLS.

![](/files/By7O2o6o1oAaiPHaRU9u)

</details>

### Setup Automatic Deployment

Github Actions can automatically sign and deploy your config file for you, each time you merge a change. This has the added benefit of checking for errors, and aborting the deployment if any issues are found.

<details>

<summary>Detailed Instructions</summary>

Add the Github Action config below to the location `.github/workflows/cm_deploy_to_pages.yml` in your repository.

You may want to change these config options in the file below:

* branches: defaults to main. If you prefer a different production deployment branch, change this value.
* JSON\_CONFIG: defaults to `config.json`. If you put your json config somewhere else, specify it’s path from root and filename here
* DEPLOYED\_NAME: the name where the config file will be hosted.&#x20;

{% code title=".github/workflows/cm\_deploy\_to\_pages.yml" %}

```yaml
# Sample workflow for building and deploying a criticalmoments.io config file to GitHub Pages (including signing)
name: Deploy Critical Moments config to Github Pages

on:
  # Runs on pushes targeting the default branch
  push:
    branches: ["main"]

  # Allows you to run this workflow manually from the Actions tab
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
  group: "pages"
  cancel-in-progress: false

# Default to bash
defaults:
  run:
    shell: bash

jobs:
  # Build job
  build:
    runs-on: ubuntu-latest
    env:
      JSON_CONFIG: 'config.json'
      DEPLOYED_NAME: 'cmConfig.json'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
      - name: Setup Pages
        id: pages
        uses: actions/configure-pages@v5
      - name: Sign Config File
        run: |
          mkdir -p public && \
          cp "${JSON_CONFIG}" "${SIGNED_NAME}"
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

  # Deployment job
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
```

{% endcode %}

</details>

Once your follow the detailed instructions above, check that the action was successful in Github's Actions UI.

### Github Settings: Action Based Pages Build

In the github.com UI, open the repo's settings, then open the "Pages" tab. Set the "Source" to "Github Actions" as seen below

<figure><img src="/files/sEotopdIT58js3dxSOFL" alt="" width="248"><figcaption></figcaption></figure>

{% hint style="warning" %}
Don't skip this step, or builds may not work!
{% endhint %}

### Confirm the Config is Online

Double check everything worked and your signed config file is accessible from the public internet!

Your signed config should now be online at your URL such as:&#x20;

* If you setup custom DNS: `https://democonfig.criticalmoments.io/cmConfig.json`
* Or via direct Github URL:  `https://criticalmoments.github.io/sample_app_config/cmConfig.json`

Fetch it using the tool of your choice (`curl,` browser).

### Use your New Hosted Config

You can use this URL for your app release in your app delegate like so:

{% tabs %}
{% tab title="Swift" %}

```swift
// Add this before `CriticalMoments.sharedInstance().start()`
CriticalMoments.sharedInstance().setReleaseConfigUrl("YOUR_URL")
```

{% endtab %}

{% tab title="Objective C" %}

```objectivec
// Add this before `[CriticalMoments.sharedInstance start];`
[CriticalMoments.sharedInstance setReleaseConfigUrl:@"YOUR_URL"];
```

{% endtab %}
{% endtabs %}

After setting the URL, you can test everything works in-app by running your app with a "Release" build configuration in it's Xcode scheme:&#x20;

<figure><img src="/files/NRitBEe82hLEvTehCR4y" alt="" width="375"><figcaption><p>Xcode "Run" settings in the "Edit Scheme" UI</p></figcaption></figure>

### Setup Access Control and Branch Protections (optional, recommended)

Setup any access controls and branch protections you desire.

Since the Critical Moments config file can push messaging to your users in app, we suggest you require approvals before merging into the deployment branch (defaults to main branch), just as you would a any webpage.


# Config File Structure

The Critical Moments config file is a web-hosted configuration file, with a JSON structure defining how your events, actions, conditions, and feature flags connect together.&#x20;

{% hint style="success" %}
**Update Anytime**

Signing and deploying a new config file can be done outside of app releases, allowing you to update your app's behaviour without waiting for app reviews, or users to update their builds! This can be very useful for new announcement, outages, bugs, or other unexpected scenarios.

[Read the docs](/remote-control-service) ->&#x20;
{% endhint %}

{% hint style="warning" %}
Only the web-hosted/signed config file will be used in release builds. The local/unsigned config file is only for development/debug builds.

Be sure to deploy a signed+hosted config and set the URL before releasing your app to the app store.
{% endhint %}

### Config File Structure

The config file has the following top level entities:

* `configVersion` \[string, required]: always "v1"
* `appId` \[string, required]: your app's [bundle ID](https://developer.apple.com/documentation/appstoreconnectapi/bundle_ids)
* `actions` \[object, optional]: documented in the [Actions Overview](/actions-in-app-messaging/actions-overview)
* `conditions` \[object, optional]: documented in [Named Conditions](/feature-flags/named-conditions-config)
* `triggers` \[object, optional]: documented in [Triggers Overview](/actions-in-app-messaging/triggers)
* `themes` \[object, optional]: documented in [Themes Overview](/themes/theme-overview)
* `notifications` \[object, optional]: documented in [Notifications Spec](/notifications/notifications-spec)
* `minAppVersion` \[string, optional]: the minimum app version number this config file should be used on. If an older app version fetches this file, it will be ignored and Critical Moments will not start. Example: "v1.4.2"
* `minCMVersion` \[string, optional]: the minimum Critical Moments SDK version number this config file should be used on. If an older SDK version fetches this file, it will be ignored and Critical Moments will not start. Example: "v0.8.1"

Here is a sample of the typical root object in a config file:

```
{
    "configVersion": "v1",
    "appId": "YOUR_BUNDLE_ID",
    "actions: {...},
    "conditions: {...},
    "triggers": {...},
    "notifications": {...},
    "themes": {...}
}
```


# Demo App

Try it today!

Want to see Critical Moments in action? &#x20;

Download our demo app [**from TestFlight**](https://testflight.apple.com/join/uSwscwu0) or view the source code [on Github](https://github.com/CriticalMoments/CriticalMoments/tree/main/ios/sample_app).

<div align="left"><figure><img src="/files/gqhZNSgoJ2DRVGEPee9V" alt="" width="188"><figcaption></figcaption></figure></div>

### What's included

The demo app lets you try the following:

* Example use cases
  * User messaging
  * Smart Feature Flags
  * User Journey Exmaple
* Realtime conditional evaluation
* Actions
  * Modals
  * Banners
  * Alerts
  * Web links
  * Deeplinks
* Themes


# Homepage & Account

Looking for our homepage or your account login? Look no further!

* [**Homepage / Maketing page**](https://criticalmoments.io)
* [**Account home / Sign-In**](https://criticalmoments.io/login)&#x20;


# Mobile App Toolbox: 13 Features Most Apps Need

We've created a starter config file that includes 13 features most apps need at somepoing in their lifecycle.

Read [our blog post defining the features, and how to enable them](https://criticalmoments.io/blog/mobile_app_growth_plan_in_json).

Or [download the starter config from Github](https://github.com/CriticalMoments/CriticalMoments/blob/vnext/ios/sample_app/SampleApp/starterConfig.json).

### Features in the Template

* Notifications to improve day-1 retention and reduce churn.
* App rating prompts that appear at the perfect moment.
* Increase revenue by paywall targeting, and banner UI.
* Push app updates to users on an old or buggy release.
* Outage management: head off negative reviews and support tickets.
* “What's new” UI for major app updates.
* Announce important changes with banners, web links, or native UI.
* Feature flags that can target real-time context
* Themes to perfectly match your brand.


# Reduce App Churn with Notifications

Send notifications to your users at the right time to increase views and engagement.

This guide will walk you through using the Critical Moments SDK to send your users notifications, which can increase your app’s activation rates and reduce churn.

{% hint style="info" %}
This is a step by step developer guide with examples. Our technical docs for notifications can be [found here](/notifications/intro-to-notifications).
{% endhint %}

## Benefits over other notification systems

Critical Moments Notifications have several major benefits over other notification systems:

* [**Smart Notifications**](https://criticalmoments.io/features/notifications)**:** Delivered when users are actively holding their unlocked device, increasing visibility and click-through rates.
* **No Push Servers Required**: less infrastructure to manage and more reliable delivery with local notifications.
* **No Per-Message Cost**: unlimited notifications included
* **Notifications Without Code**: after integrating, new notifications and targeting can be done completely in config.
* [**Over the air updates**](/remote-control-service): change notification messaging, targeting and timing over the air.
* **Scheduling/Timezones**: deliver notifications at the perfect time, whether that's in the user's local time zone, specific times of the day, or certain days of the week.

## Step 1: Install Critical Moments

Follow our [quick start guide](/quick-start) to integrate Critical Moments into your app. The initial integration takes about 15 minutes.

## Step 2: Request Notification Permissions

Your app needs to get the user’s permission to show notifications.

You’ll want to request permission somewhere that feels natural, such as during onboarding flow. It’s important to ask early enough in your app’s user experience that most users see it. One of the primary use cases of notifications is to prevent early churn/abandonment; notifications can’t help reduce churn if the user churns before the permission request.

Requesting permission requires just one line of code:

{% tabs %}
{% tab title="Swift Example" %}

```swift
CriticalMoments.shared().requestNotificationPermission()
```

{% endtab %}

{% tab title="Objective-C Example" %}

```objectivec
[CriticalMoments.shared
        requestNotificationPermissionWithCompletionHandler:nil];
```

{% endtab %}
{% endtabs %}

## Step 3: Log important events and properties

[Instrument event](/events/recommended-events) and [set properties](/conditional-targeting/custom-properties) in your app so your notifications can fire at the right moment, and target the right users.

The following events and properties are particularly helpful for targeting notifications:

* `completed_onboarding` event: Trigger when the user completes onboarding, used to reduce abandonment (see below).
* `user_signed_in` property: is the user signed in (for apps that require accounts)
* `has_paid_subscription` property: is the user a subscriber (for apps with subscriptions)

## Step 4: Add Notifications to Reduce Churn

After integration, notifications can be added entirely in your [app’s config file](/config-file-structure), no additional code needed!

We suggest three notifications below that can be easily added to any app.

### Increase Activation Rate

Many users download an app, only to never launch it again. Timely reminders can help them remember to complete onboarding and discover what your app has to offer.

The [config](/config-file-structure) below will remind users to complete onboarding, 15 minutes after they leave. It will be canceled when the `completed_onboarding` event is fired. It will only be sent between 10am and 8pm local time, and will only be sent once.

```json
"notifications": {
  "onboardingReminder": {
    "title": "Complete APP_NAME Setup",
    "body": "It only takes 3 minutes to start APP_VALUE.",
    "cancelationEvents": [
      "completed_onboarding"
    ],
    "deliveryTimeOfDayStart": "10:00",
    "deliveryTimeOfDayEnd": "20:00",
    "deliveryTime": {
      "eventName": "app_entered_background",
      "eventInstance": "latest-once",
      "eventOffsetSeconds": 900
    }
  }
}
```

### Reduce Short-Term Churn

Some users never complete onboarding. Remind users to complete onboarding tasks within the first week to boost activation rates and reduce average active user acquisition cost.

The [config](/config-file-structure) below will remind users to complete onboarding between day 3 and day 7 after initial app use. It will use smart notifications to attempt delivery when the user is holding their device and their screen is on. It will be canceled when the `completed_onboarding` event is fired. It will only be sent between 10am and 8pm local time, and will only be sent once.

```json
"notifications": {
  "title": "Complete APP_NAME Setup",
  "body": "It only takes 3 minutes to start APP_VALUE.",
  "cancelationEvents": [
    "completed_onboarding"
  ],
  "deliveryTimeOfDayStart": "10:00",
  "deliveryTimeOfDayEnd": "20:00",
  "deliveryTime": {
    "eventName": "app_entered_background",
    "eventInstance": "latest-once",
    "eventOffsetSeconds": 259200
  },
  "idealDeliveryConditions": {
    "condition": "screen_brightness > 0.01 && device_orientation != 'face_up' && device_orientation != 'face_down'",
    "maxWaitTimeSeconds": 345600
  }
}
```

### Reduce Long-Term Churn

Some users might churn after using an app for a while. Setting up reminders after a few weeks or months of inactivity can keep bring them back and reduce long-term churn.&#x20;

The [config](/config-file-structure) below will remind users who have not launched the app in 30 days to return. It will use smart notifications to attempt delivery when the user is holding their device and their screen is on, and fall back to a standard delivery time if that condition is not met in 4 days. It will only be sent between 10am and 8pm local time. If the user uses the app, this notification is pushed out again (eventInstance: "latest").

```json
"notifications": {
  "churnNotification": {
    "title": "APP_VALUE",
    "body": "APP_VALUE_2",
    "deliveryTimeOfDayStart": "10:00",
    "deliveryTimeOfDayEnd": "20:00",
    "deliveryTime": {
      "eventName": "app_entered_foreground",
      "eventInstance": "latest",
      "eventOffsetSeconds": 2592000
    },
    "idealDeliveryConditions": {
      "condition": "screen_brightness > 0.01 && device_orientation != 'face_up' && device_orientation != 'face_down'",
      "maxWaitTimeSeconds": 345600
    }
  }
}
```

Some apps add a second churn notification, a few weeks after the first one.

## Step 5: Add Custom Notification

Each app has their own unique needs and user experience. Our robust targeting options can deliver notifications to the right users at the right time.

The [example config](/config-file-structure) below reminds users of a hypothetical workout app to train. It's sent 3 days after their last workout event (training\_session\_end). It uses smart notifications to attempt delivery when the user is holding their device and their screen is on. It delivers in the morning. It will recur after every workout (eventInstance: "latest").

```json
"notifications": {
  "churnNotification": {
    "title": "Time to Train!",
    "body": "It's been a few days since your last workout. Time to get back in shape!",
    "deliveryTimeOfDayStart": "7:00",
    "deliveryTimeOfDayEnd": "12:00",
    "deliveryTime": {
      "eventName": "training_session_end",
      "eventInstance": "latest",
      "eventOffsetSeconds": 259200
    },
    "idealDeliveryConditions": {
      "condition": "screen_brightness > 0.01 && device_orientation != 'face_up' && device_orientation != 'face_down'",
      "maxWaitTimeSeconds": 86400
    }
  }
}
```

## You're done! ✅

Everything should now work!

You’ll want to test that the notifications appears in your app at the expected times. It can be helpful to set the `eventOffsetSeconds` and `maxWaitTimeSeconds` to lower values (like 10 seconds) during testing. If notifications aren’t appearing, ensure the conditions you set are met.

{% hint style="warning" %}
By default notifications only appear when an app isn’t in the foreground. To test, you’ll want to trigger a notification with a delay via `eventOffsetSeconds`, then move the app to the background before that delay fires to see it work in testing.
{% endhint %}

## Optional Enhancement: Deeplinks

If you want your notifications to open a specific screen of your app instead of just launching it, implement [iOS deeplinks](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app), or [universal links](https://developer.apple.com/ios/universal-links/) to those parts of your app.

You can use the `actionName` [property of notifications](/notifications/notifications-spec) to trigger this screen when a notification is tapped.


# Improve your App Store Rating

How to target app review prompts to increase your App Store rating.

This guide will walk you through using [Critical Moments](https://criticalmoments.io) to prompt users to review your app at the optimal time to maximize your app store rating.

{% hint style="info" %}
You can also read our blog post series on app reviews, which which discusses these topics in more depth:

* [How to use targeting to improve app store rating](https://criticalmoments.io/blog/improve_app_ratings)
* [How to help users and improve your app rating through better UI](https://criticalmoments.io/blog/deceptive_app_rating_prompt)
  {% endhint %}

### Step 1: Install Critical Moments

Follow our [quick start guide](/quick-start) to integrate Critical Moments into your app.

### Step 2: Log important events&#x20;

[Instrument events](/events/event-overview) in your app so your app review prompt can fire at the right moment, and target the right users.

This includes 2 categories of events:

* Events that determine the timing of the app review prompt. For example, a Todo list app might want to ask for a review after a user completes a task, or a backup app might want to ask when the user completes a backup.&#x20;
* Events which impact if a user is a good candidate to ask. For example, have they used all the key features in your app? Have they hit a critical error that would make their experience less than stellar?&#x20;

{% tabs %}
{% tab title="Swift Example" %}

```swift
CriticalMoments.shared().sendEvent("completed_task")
```

{% endtab %}

{% tab title="Objective-C Example" %}

<pre class="language-objectivec"><code class="lang-objectivec"><strong>[CriticalMoments.shared sendEvent:@"completed_task"];
</strong></code></pre>

{% endtab %}
{% endtabs %}

### Step 3: Add a Smart App Review Action

Add the a smart app review action to the [config file](/config-file-structure) you created. The template below is a great starting point for any app. It checks the following before asking a user for a review:

* Don't show a review prompt if one has been shown in the last 21 days (504h)
* Check the min app version we want to ask for reviews on. Set to v1.0 here, but you can increase this over time, remotely disabling reviews on older or buggy builds.
* Check for times to avoid which are correlated to lower reviews: low power more, no network connection, low data mode, or a battery under 20%.
* Check the user isn't running very outdated version of iOS (before iOS 16)

```json
{
    "configVersion": "v1",
    "appId": "YOUR_APP_ID",
    "actions": {
        "namedActions": {
            "smartReviewAction": {
                "actionType": "review_prompt",
                "actionData": {},
                "condition": "(eventCount('system_app_review_requested') == 0 || latestEventTime('system_app_review_requested') < now() - duration('504h')) && versionGreaterThan(app_version, '1.0') && !device_low_power_mode && has_active_network && !low_data_mode && device_battery_level > 0.2 && !versionLessThan(os_version, '16.0')"
            }
        }
    }
}
```

When this action is fired, you'll see the system review prompt:

<figure><img src="/files/lGXg25tbJOZnjlIWEQBw" alt="" width="178"><figcaption><p>SKStoreReviewController</p></figcaption></figure>

### Step 4: Trigger the Smart Review

Each app has a special moment where the user feels its value: reaching a streak in Duolingo, completing a meditation in Calm, or finishing backing up your photos in Google Photos. In that moment the user feels maximal appreciation for the app, and its place in their life. This is the best time to prompt for an app review!

Here is an example [trigger section](/actions-in-app-messaging/triggers) to be added to your [config file](/config-file-structure). It will fire the `smartReviewAction` action when a `completed_task` event is fired in the app (done in step 2). We also add a conditional check here: wait until at least 3 tasks have been completed (optional).&#x20;

```
"triggers": {
    "namedTriggers": {
        "completedTaskReviewTrigger": {
            "eventName": "completed_task",
            "actionName": "smartReviewAction",
            "condition": "eventCount('completed_task') >= 3"
        }
    }
}
```

{% hint style="info" %}
If you app has multiple good moments to ask for a review, simply add more triggers in your config calling the smartReviewAction.
{% endhint %}

## You're done! :white\_check\_mark:

Everything should now work!&#x20;

Test that the app review appears in your app when you complete the trigger. During testing you may want to temporarily remove the condition which only shows the review prompt once every 21 days.

## Optional Improvements

The following steps are optional enhancements to further improve your app review logic.&#x20;

### Optional Step 1: Refine your app-review targeting condition

Prompting the right set of users to review your app is essential to improving your app rating. You want to pick users who have had sufficient experience with your app, who aren’t experiencing any negative conditions (like low battery or no network connection).&#x20;

We started with a base-template above. The list below offers an even longer set of options you can add to your `smartReviewAction.condition` string to tune app review timing.

```
/* Device condition checks we suggest for almost all apps */
device_battery_level > 0.2 && 
!device_low_power_mode && 
has_active_network && 
!low_data_mode &&
device_orientation != 'face_up' && 
device_orientation != 'face_down' && 
foreground && 

/* Optional device checks. You decide if these make sense for your app */
!other_audio_playing && 
!on_call && 
!has_car_audio &&

/* Check last prompt time was not in last 21 days. Note: this uses the 'ask_for_review' action we fire below.  */
(
  eventCount('ask_for_review') == 0 || 
  latestEventTime('ask_for_review') < now() - duration('504h')
) &&

/* Check the app wasn't installed in last week */
app_install_date < now() - duration('168h') &&

/* Check their app version isn't too old. Comment out until you have old clients you no longer want to request reviews on */
/* versionGreaterThan(app_version, '2.4.0') && */

/* Check the app version isn't in the 'known buggy versions' list. Commented out until you have versions you wish to add. */
/* app_version not in ['buggy_version_1', 'buggy_version_2'] && */

/* Out of date OS check */
!versionLessThan(os_version, '16.0') &&

/* only prompt users who's native language is supported in your UI. Uncomment once you set the correct values in the list. */
/* locale_language_code IN ['en', 'es', 'de'] && */

/* Trailing 'true' to make editing this template easier, as lines above end in '&&' */
true
```

We’ve written a [blog post](https://criticalmoments.io/blog/improve_app_ratings/) with additional ideas to improve the targeting and timing of your app rating prompt with the goal of improving your overall rating.

Read the [full blog post here](https://criticalmoments.io/blog/improve_app_ratings), or jump to a section that sounds relevant to your app:&#x20;

* [Exclude older devices](https://criticalmoments.io/blog/improve_app_ratings#excludeolderdevices): for memory/cpu/gpu intensive apps
* [Exclude outdated app versions](https://criticalmoments.io/blog/improve_app_ratings#avoidoutdatedappversions)
* [Excluded outdated operating systems](https://criticalmoments.io/blog/improve_app_ratings/#excludeolderosversions)
* [Exclude OS versions with bugs](https://criticalmoments.io/blog/improve_app_ratings#excludeosversionswithknownbugs)
* [Pause after major UX updates](https://criticalmoments.io/blog/improve_app_ratings#pause_for_updates)
* [Check essential app permissions](https://criticalmoments.io/blog/improve_app_ratings#check_permissions)
* [Avoid users with negative experiences](https://criticalmoments.io/blog/improve_app_ratings#avoidusershavingnegativeexperiences)
* [Pause prompting when appropriate](https://criticalmoments.io/blog/improve_app_ratings#pausepromptingwhenappropriate)
* [Other creative options to improve your rating](https://criticalmoments.io/blog/improve_app_ratings#becreative)

### Optional Step 2: Refine app-specific logic in your conditional targeting

The template above only considers factors that apply to every app (low battery, is the user on a phone call, etc). It’s also important to check the user has had adequate time to engage with your app's core features before asking for a review. This assures that they have a well-rounded experience to base their review on.&#x20;

Some examples:

* For a game: `max_level_reached >= 5`
* For a language learning app: `eventCount('picked_language') > 0 && eventCount('completed_written_lesson') > 2 && eventCount('completed_spoken_lesson') > 2`
* For a meditation app: `eventCount('completed_meditation') > 3`

The events you instrumented in step 2 can be used here, as well as the [properties built-in to Critical Moments](/conditional-targeting/built-in-properties) like app\_install\_date.

Add this conditional logic to the contition you defined in step 3.

### Optional Step 3: Consider a Pre-review Prompt UI

It may make sense to ask users if they need help/support before directing them to the review prompt. Directing users in need of help to your support channels can help improve their experience, while cutting down on negative reviews.

Here’s a visual example:

<figure><img src="/files/GTN3S9X5IxeiyadFdkja" alt="" width="188"><figcaption><p>Custom review pre-prompt</p></figcaption></figure>

{% hint style="info" %}
Our [blog post on app review UI](https://criticalmoments.io/blog/deceptive_app_rating_prompt) has a longer discussion on how to format this prompt to best help users and maximize ratings.
{% endhint %}

We can update our config file to create this UI entirely in config as follows. Be sure to replace the app name and help page link with your own:

```
"smartAppReview": {
    "actionType": "alert",
    "condition": "[CONDITION FROM EARLIER STEPS]",
    "actionData": {
        "title": "Enjoying [APP NAME]?",
        "message": "If you’re enjoying [APP NAME], we would greatly appreciate an app review!\n\nIf you are having an issue, our Help Docs and support team are here to help!",
        "showOkButton": false,
        "showCancelButton": false,
        "customButtons": [
            {
                "label": "Rate Us",
                "actionName": "system_app_review",
                "style": "primary"
            },
            {
                "label": "Help, Feedback & Contact Us",
                "actionName": "help_page"
            },
            {
                "label": "Not Now"
            }
        ]
    }
},
"system_app_review": {
    "actionType": "review_prompt",
    "actionData": {}
},
"help_page": {
    "actionType": "link",
    "actionData": {
        "url": "https://[YOUR SUPPORT URL]",
        "useEmbeddedBrowser": true
    }
},
```

You can get creative here. Explore different text in your alert, trying a fully native [modal UI](/actions-in-app-messaging/modals), a less intrusive [banner request](/actions-in-app-messaging/banners), or an inline UI asking for a review which still checks the condition using a [conditional feature flag](/feature-flags/conditional-feature-flags). You can also change this UI over time, [updating over-the-air without app updates](/remote-control-service)!

### Optional Step 4: A/B Test to Find Optimal Targeting

We've written [a blog post](https://criticalmoments.io/blog/ab_test_app_store_review_prompts) explaining how you can implement A/B testing of your app review targeting. This allows you to find the optimal target to improve your app store rating.


# Feature Flags Guide

Best practices for feature flags

This guide walks through how to quickly add feature flags to your app, and common use cases for feature flags such as randomized rollouts, AB testing, conditional targeting, rollbacks, and more.

Critical Moments has several advantages over other feature flag systems:

* Powerful [realtime conditional targeting](/conditional-targeting/intro-to-conditions): each flag's logic is evaluated locally on the client in realtime. It can update in real-time based on over 100 properties. Flags can automatically toggle  if the battery is low,  if the internet connection drops, they set their phone down, and more!
* Powerful conditional logic engine: you can write complex conditions combining realtime properties, randomized rollouts, conditional thresholding, and more. You're not bound by sliders and boolean flags.
* Free unlimited true/false feature flags: no charge for simple flags
* Free for [small businesses](https://criticalmoments.io/pricing/)
* Completely private [trustless deployments](/trustless-saas): you don't need to trust our server infrastrcture

### Install Critical Moments

If you haven't already, follow our [quick start guide](/quick-start) to integrate Critical Moments into your app.

### Client API

Use the [checkNamedCondition API](/feature-flags/conditional-feature-flags) in your app to check feature flags values in code.&#x20;

{% tabs %}
{% tab title="Swift Example" %}

```swift
// Optionally try/catch the for error
let result = try? await CriticalMoments.shared().checkNamedCondition("userNotDistracted")
if result {
   // Perform action
}
```

{% endtab %}

{% tab title="Objective-C Example" %}

```objectivec
[CriticalMoments.sharedInstance 
        checkNamedCondition:@"userNotDistracted" 
        handler:^(_Bool result, NSError * _Nullable error) {
    if (error != nil) {
        NSLog(@"Error: %@", error);
    } else if (result) {
        // Perform action
    }
}];
```

{% endtab %}
{% endtabs %}

### Remote Updates

You can update your feature flag state anytime from the cloud. This can include updating their state (true/false), updating their [rollout percentage](#progressive-rollouts), [rolling back](#rollbacks), or changing their [conditional logic](#conditional-targeting).&#x20;

Here's a sample config file format with two feature flags (see [docs](/feature-flags/named-conditions-config) for full details):

```
"conditions": {
  "namedConditions": {
    "feature_one": "true",
    "experiment_two": "false"
  }
}
```

The remote update process is detailed [here](#remote-updates).

### Progressive Rollouts

Progressive Rollouts (or randomized rollouts) are powerful part of using feature flags. When launching a new feature, you can start with a small subset of users, watch for issues. Over time you can rollout to a larger group if everything goes well, or rollback if serious issues are detected.

#### Implementing Progressive Rollouts

Our[ random number generating functions](/conditional-targeting/syntax-and-operators#random-number-generation) can be use to rollout to a small set of users, and grow it over time.&#x20;

Here is an example progressive rollout for a feature named `feature_name`:

* Start at 10%: `randForKey('feature_name', stableRand()) % 100 < 10`
* Increase to 25%: `randForKey('feature_name', stableRand()) % 100 < 25`
* Increase to 50%: `randForKey('feature_name', stableRand()) % 100 < 50`
* Increase to 100%: `true`

This is just an example. You can modify the condition using our powerful [conditions engine](/conditional-targeting/intro-to-conditions) to create your own targeting logic.

{% hint style="success" %}
The stableRand and randForKey functions can be used in unlimited feature flags, even on our free plan!
{% endhint %}

#### Understanding the Conditionals

The conditions above work as follows:

* `stableRand()`: generates a random number, which is always the same on this client, even after app restart.
* `randForKey(key, randSeed)`: generates a random number which is stable for the key/seed pair. Without adding a unique feature name key for subsequent rollouts, the same users will be selected the same order each time, which is typically not desired for randomized rollouts.&#x20;
* The modulo operator `% 100` reduces the large random number to a random number between 0 and 99. Combined with `< 25` the check has a 25% chance of being true. &#x20;

If you do want the users in the same order across different feature flags, simply use the same key in randForKey for those rollouts.

### Rollbacks

If you find issues when rolling out a feature or want to sunset a feature, you can rollback the flag at anytime with a remote update.&#x20;

#### Basic Rollbacks

Disabling a feature flag is as easy as pushing a conditional string that will evaluate to false, such as `false`.

#### Targeted Rollbacks

Unlike other feature flag systems, you can perform targeted rollbacks with over 100 realtime device properties. This is useful if the rollback is only needed for some users. Some examples:

* Rollback only iPad: `device_model_class != 'iPad'`
* Rollback only on devices running iOS 15 and earlier `versionNumberComponent(os_version, 0) <= 15`
* Rollback only on older devices (if feature requires higher CPU/GPU/memory): `((device_model_class == 'iPhone' && versionGreaterThan(device_model_version, '13.0') || (device_model_class == 'iPad' && versionGreaterThan(device_model_version, '11.0')))`
* We support [over 100 other options](/conditional-targeting/built-in-properties) which can be helpful for cases you didn't predict before releasing to the app store.

### AB Test Variant Selection

Feature flags can be used to implement an important part of AB testing: randomized variant assignment. Assigning a user to a AB test variant is very similar progressive rollouts:

* Initially 90% in A, 10% in B,  (assuming A is proven and B is new/risky): `randForKey('experiment_name', stableRand()) % 100 < 90`
* Scale up to 50/50: `randForKey('experiment_name', stableRand()) % 100 < 50`
* Rollout to winner: `true` if A won, `false` if B won

You'll need to add which test variant the user is in to any success analytics in order to measure your results.

### Conditional Targeting

Adding real-time conditional targeting is a powerful addition to typical server driven feature flags.

Some examples of benefits over typical randomized feature flag rollouts:

* A common tactic for testing social apps it to rollout to a specific geography so all users who interact see the same UI, eg `locale_country_code == 'CA'`&#x20;
* Target devices that are better tested on the new feature to start getting feedback sooner (iPhones to start, iPads later, only the latest iOS version)
* Target only users who have been using the app a while, as changing the UI on new users provides less valuable data and increases churn
* Targeted rollbacks: disable the feature if bugs are found, but only on the impacted devices, and without canceling the entire rollout. For example you can rollback: bugs with dark mode, bugs in specific locales, bugs when offline, bugs on specific devices, and much more.

{% hint style="info" %}
One conditional feature flag is included in our free plan, and small business qualify for free unlimited conditional feature flags. See our [pricing](https://criticalmoments.io/pricing/) for more details.
{% endhint %}

### Combining with Existing Feature Flag Tools

You might already be invested in another tool for feature flag tool. It's likely that tool doesn't support powerful [conditional targeting](#conditional-targeting) or [targeted rollbacks](#targeted-rollbacks) like Critical Moments does.

You can take advantage of both systems together if you want to keep your existing system, while also using Critical Moments Targeting with a simple `&&` evaluation.


# Intro to Notifications

Mobile notifications to reduce churn and activate users

Critical Moments can send notifications to your users even when they are outside your app. These can help reduce churn, activate users, and improve conversion rate.

{% hint style="info" %}
See our developer guide [Reduce App Churn with Notifications](/guides/reduce-app-churn-with-notifications) for more a step by step walkthrough of using CM notifications.
{% endhint %}

<figure><img src="/files/5HD81AalDqutENsrExo6" alt="" width="375"><figcaption><p>System Notifications</p></figcaption></figure>

### How Critical Moments Enhances Notifications

Critical Moments offers several major improvements over typical push notification campaigns:

* **Smart delivery timing**:&#x20;
  * Deliver notifications when users are actively using their devices, appearing at the top to catch their attention. Your notifications are never buried under a pile of others.
  * Target the right users for each message by considering their engagement history and current state.
  * Wait for ideal engagement conditions, such as holding the device, on Wi-Fi, or not low on battery.
* **Amazing developer experience**: No push servers to manage. Easily deliver in user-local timezones.
* **Higher deliverability**: Notifications are scheduled locally on the device, ensuring they are delivered regardless of server and connectivity conditions.
* **Update anytime**: Update notification content and targeting anytime, without app updates or new coding. Growth teams can push content and targeting updates over the air, with only configuration updates.

{% hint style="info" %}
Notification will only work if the user approves notifications for your app.

See the [Quick Start Guide](/quick-start#request-notification-permissions) for instructions on how to request permission.
{% endhint %}

### Detailed Docs

* [Notification Configuration Spec](/notifications/notifications-spec)
* [Smart Delivery Timing Spec](/notifications/smart-delivery)
* [Badges, aka App Icon Count](/notifications/badges)

### Notifications Demo

The Critical Moments [Demo App](/demo-app) provides sample notifications and various delivery options for you to test.


# Notifications Spec

How to add CM notifications to your app

### Notifications Configuration Example

Notifications are defined in your [config file](/config-file-structure).&#x20;

Here's an example notification config section. It creates one notification to remind users to complete the app's onboarding.&#x20;

* Content: deliver a message reminding the user to complete setup, with a CTA
* Schedule: this notification will appear 10 minutes after the user dismisses the app.&#x20;
* Cancelation: the notificaiton will be canceled when the user completes setup
* Timezone: it will be delivered between 9am and 9pm in the user's local timezone.

```json
 "notifications": {
    "completeSetupReminder": {
      "title": "Complete Setup",
      "body": "It only takes 2 minutes to start healthy habbits with LifeTracker.",
      "sound": "default",
      "cancelationEvents": ["completeSetup"],
      "deliveryTimeOfDayStart": "09:00",
      "deliveryTimeOfDayEnd": "21:00",
      "deliveryTime": {
        "eventName": "app_entered_background",
        "eventOffsetSeconds": 600
      }
    }
  }
```

### Notification Config Specification

A top level `notifications` object in your [config file](/config-file-structure) contains a set of named notification maps/objects. Each notification object has the following properties:

#### Notification Content Properties

* title \[optional, string]: the title shown on the notification. If omitted and a body is included, the app's name will be used as the title.
* body \[optional, string]: a string for the text content shown in the notification
* actionName \[optional, string]: the name of the [namedAction](/actions-in-app-messaging/actions-overview) to launch when the user taps this notification. If not set, it will launch the app without performing an action.
* sound \[optional, string]: the sound to play when the notification is delivered. Values are:
  * omitted: no sound is played
  * “default”: system default notification sound&#x20;
  * Custom string: a named custom sound embedded in your app binary; see [Apple Docs](https://developer.apple.com/documentation/usernotifications/unnotificationsound?language=objc).
* badgeCount \[optional, int]: sets the [app icon's badge](/notifications/badges) (count). Set 0 to clear  the badge. Badge updates can be scheduled in tandem with notification alerts, or simply omit the title/body to schedule a badge update without a notification alert.

#### Scheduling Notifications

We offer powerful config options to schedule when your users recieve notificaitons.

* idealDeliveryConditions: see our spec on [smart notification delivery](/notifications/smart-delivery)  for details
* deliveryTime \[required, object]: specifies when this notification should be delivered. There are two timing options, and exactly one of these must be provided on each notification.deliveryTime object:
  * Exact timestamp, is specified with `deliveryTime.TimestampEpoch` - an int [Unix epoch time](https://en.wikipedia.org/wiki/Unix_time) in seconds.
  * Relative to an event: the notification is delivered after an event. This is set by deliveryTime.eventName, with optional additional properties listed below.
* deliveryTime.eventName \[optional, string]: the [event name](/events/event-overview) to fire this notification after.
* deliveryTime.eventOffsetSeconds \[optional, int]: the delay between the event specified in eventName and the notificaiton delivery, in seconds.
* deliveryTime.eventInstance \[optional, string]: Behaviours are:
  * "latest-once" \[default]:  will deliver after the latest instance of the event. Once delivered, will not be deliver again. Without an eventOffsetSeconds, this is funtionally the same as "first".
  * "latest" will deliver after the latest instance of the event. This may deliver several times if the event reoccurs.&#x20;
  * "first": delivers notification once after the first instance of eventName in your app, and only the first.&#x20;
* deliveryDaysOfWeek \[optional, array of strings]: defaults to 7 days a week. This array specifies the days of week where this notification should be delivered in user-local timezone (example: `["Saturday","Sunday"]`). If the deliveryTime is not on one of these days, it will be pushed back to the first allowed day.
* deliveryTimeOfDayStart/deliveryTimeOfDayEnd \[optional, string]: the start and end time defining when to deliver the notificaiton, in "HH:MM" format, in the user local timezone. For example, start="09:00" and end="21:00" would deliver the notification between 9am and 9pm. If the deliveryTime is not in this range, it will be pushed back to the next time in the range.
* cancelationEvents \[optional, array of strings]: if any of the [event names](/events/event-overview) listed here are fired, this notification will be canceled, and will never again be scheduled.&#x20;
* scheduleCondition \[optional, string]: a Critical Moments [condition string](/conditional-targeting/intro-to-conditions), which is checked before the notification is scheduled. The notificaiton is not scheduled unless this condition returns true.
  * **Note**: this condition is checked at schedule time, not delivery time. The condition may no longer be true when the notification is delivered. It's helpful to check things unlikley to change (completed\_setup, app\_version > x), but less helpful for things that change often (has\_wifi, device\_battery\_level).&#x20;

#### Less Common Properties

These properties are only needed for more advanced notifications, and can typically be omitted.

* launchImageName \[optional, string]: The name of the image or storyboard to use when your app launches. [Apple docs](https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/launchimagename?language=objc).
* relevanceScore  \[optional, float]: The score the system uses to determine if the notification is the summary’s featured notification (from 0 to 1).
* interruptionLevel  \[optional, string]: one of "active", "critical", "passive", "timeSensitive". See [Apple docs](https://developer.apple.com/documentation/usernotifications/unmutablenotificationcontent/interruptionlevel?language=objc) for details. "timeSensitive" requires you to set the "Time Sensitive Notification" capability on your app, and "critical" require approval from Apple.

### Combining with Other Notification Systems

You can easily use your own notifications, such as push, along side Critical Moments. Our notification delegate will only handle notifications originating from Critical Moments, and will pass through any others to the notification delegete you set.


# Smart Delivery

Delivery notifications at the perfect moment

Our Smart Notification Delivery system allows timing the delivery of your notification based on realtime device conditions.

### Examples&#x20;

Here are some examples of how Smart Delivery can improve your notification click through rate:

* Wait until the the device is being held (not flat on a table), before showing a notification, to increase visibility
* Hold off on notifications if the user's battery is low or they are on low data mode
* Wait until the evening and the user is connected to WiFi for sending an re-engagment notification for a game

{% hint style="info" %}
**Important:** be sure to setup the background processing as described in our [Quick Start](/quick-start#enable-background-processing-for-smart-notifications) guide, or smart notifications can't detect the ideal moment to deliver.
{% endhint %}

### Example Config

This object would be added to a notification object, to create a smart notification.

```
"idealDeliveryConditions": {
    "condition": "device_orientation != 'face_up' && device_orientation != 'face_down'",
    "maxWaitTimeSeconds": 8640000
}
```

### Spec

Start by setting up up a notification in your config, as described in the notification[ spec](/notifications/notifications-spec). Once the standard notification is setup, add a idealDeliveryConditions json object to the notification config. It contains the following properties:

* `idealDeliveryConditions.condition`: an ideal delivery condition in the form of a [conditional statement](/conditional-targeting/intro-to-conditions), defining the ideal conditions you'd like the notification delivered under. For example `device_orientation != 'face_up' && device_orientation != 'face_down'`&#x20;
* `idealDeliveryConditions.maxWaitTimeSeconds` : defines how long you're willing to wait for ideal conditions, before giving up and delivering anyways. An integer number of seconds, or -1 for forever.

The notification will still wait until at least the appropriate deliveryTime as defined in your notification. However, it will also check the ideal delivery condition. If the condition isn't met, it will keep checking until it's met or maxWaitTimeSeconds has elapsed to delivery the notification.&#x20;

{% hint style="info" %}
**Understanding iOS Background Processing**

Your app will occasionally get short periods of background runtime, and Critical Moments will use those times to check if any notifications meet their ideal delivery conditions.

However, the times when your app is run are controlled by Apple and are not deterministic. Don't expect a notification for the condition `device_battery_state == 'charging'` to fire the moment you plug in your phone. Factors like low battery and app-usage-frequency can impact how often Apple gives each app runtime.&#x20;

We suggest using a reasonable maxWaitTimeSeconds as a fallback, to ensure your messages are still delivered after a fallback amount of time.
{% endhint %}


# Badges

Add a count to your app icon to drive app launches

A badge is a small, filled oval containing a number. It can appear on an app icon to indicate outstanding content in an app. They are frequently used to drive users to tap the app icon and complete an in-app action.

<figure><img src="/files/XYRh1f2wlVW5f0qyrEId" alt="App badge example" width="101"><figcaption><p>An app "badge"</p></figcaption></figure>

You can schedule badge updates using the Critical Moments notification system. Simply set a badgeCount and schedule the update as specified in the [notification spec](/notifications/notifications-spec).&#x20;

See [Apple's human interface guide](https://developer.apple.com/design/human-interface-guidelines/notifications#Badging) for more details on badges.


# Intro to Conditions

Powerful conditions for targeting, with over 100 properties

Conditions are boolean string expressions that can be evaluated with the current state of the users device. A few examples show the range of possiblities:

* UI State: `interface_orientation == 'landscape' || dark_mode || screen_brightness < 0.1`
* Device state: `user_interface_idiom == 'tablet' || device_orientation == 'face_up' || device_model_version == '13.3' || device_battery_level > 0.25`&#x20;
* Locale: `locale_language_code IN ['en', 'es'] && locale_currency_code == 'USD'`&#x20;
* App info: `versionGreaterThan(app_version, '1.4.2') && app_install_date < date('2021-06-06')`&#x20;
* Networking state: `network_connection_type != 'cellular' && has_active_network && !low_data_mode && !expensive_network`
* Location: `location_approx_city == 'Toronto' || location_region == 'TX'`
* And much more: `!on_call && (has_bt_headset || has_wired_headset) && !has_watch && contacts_permission && canOpenUrl('spotify:')`&#x20;

### Properties

Conditions can evaluate over 100 properties:

* [Built in properties](/conditional-targeting/built-in-properties): our SDK includes over 100 realtime device conditions from device orientation to "is it raining where the user is". See a [complete list](/conditional-targeting/built-in-properties) of built in properties.
* [Custom properties](/conditional-targeting/custom-properties): add your own data like "is this a power user" or "date of original account creation". Anything that makes sense for your app or use case.
* [Property history database](/conditional-targeting/conditional-guides/event-and-property-history): check the history of any property to see if it has ever held a specific value, when it occurred, or how often it's happened. Allowed for powerful personalized decision making. The database is completely private: data is stored on the user's device, evaluated locally, and never sent off device.

### Functions

Conditions allow for inline functions like [date parsing](/conditional-targeting/conditional-guides/working-with-dates), [random number generation](/conditional-targeting/syntax-and-operators#random-number-generation), [parsing version numbers](/conditional-targeting/syntax-and-operators#version-number-operators), URL support (`canOpenURL`), and more.

[Event history](/conditional-targeting/conditional-guides/event-and-property-history) functions can query the built in database of event history, counting how many times a user has performed an action, or when the last time it was performed.

### Powerful Syntax

Conditional expressions support a wide range of operators including order of operation (`()`), arithmetic (`+, -, %, /, *, ^, **`), strings (`startsWith, endsWith, contains`), regex, ranges, and much more.&#x20;

This powerful set of features can be used to

* Multi-variable thresholding: `device_battery_level > 0.2 && eventCount('app_start') > 5`
* Multi-variable scoring: `(eventCount('completed_task_1') * 2) + (eventCount('completed_task_2') * 3) > 15`&#x20;
* Combine different use cases: for example, a feature flag with progress rollout, while only targeting new iPhones for the initial cohort (exclude iPads and old OSs)

See the [full syntax docs](/conditional-targeting/syntax-and-operators) for details.

### Realtime Evaluation

Conditions are evaluated realtime, and will update their result as device state changes.

### Integration

Conditions are connected to the rest of the system through the [config file](/config-file-structure). The following parts of the config file can include an condition strings:

* [Actions](/actions-in-app-messaging/actions-overview) and [Triggers](/actions-in-app-messaging/triggers) may optionally contain a condition, and only perform the action if the condition is true
* [Conditional actions](/actions-in-app-messaging/conditional-actions): are actions which check a condition, and fire one action by name if true, and another if false
* [Feature Flags](/feature-flags/conditional-feature-flags) can return their value based on a conditional string, and be updated over time with [remote updates](/config-file-structure).
* [Named conditions](/feature-flags/named-conditions-config) can evaluate conditions in code. Use them instead of manually writing complex conditional logic in code.


# Built-in Properties

Critical moments comes with many properties and functions built in, which you can use for your conditional targeting

Topics for built in properties include:

* Device information: model name, manufacturer, OS version, etc
* Screen information: size, scale and orientation
* User locale infomation: language, country, currency
* App information: app ID, version number, etc
* Battery state: charging, percent remaining, etc
* Audio information: is the user on a call? Listening to music? Do they have headphones connected?
* Network connectivity: has connection, connection type, etc
* Permissions the user has granted (camera roll, contacts, microphone, etc)
* And more...

<details>

<summary>Free Plan Property List</summary>

The following properties are included in our free plan. The remaining properties require a Pro plan.

```
"platform",
"app_id",
"app_version",
"os_version",
"cm_version",
"device_battery_state",
"device_battery_level",
"device_model",
"device_model_class",
"device_orientation",
"device_manufacturer",
"app_install_date",
"has_active_network",
"screen_brightness",
"user_interface_idiom",
"interface_orientation",
"dark_mode",
"session_start_time",
"app_start_time",
"foreground",
"app_state",
"timezone_gmt_offset",
"locale_country_code",
"locale_language_code",
"locale_language_direction",
"screen_height_points",
"device_model_version",
"screen_width_pixels",
"screen_height_pixels",
"screen_width_points",
"screen_scale",
```

</details>

<table data-full-width="true"><thead><tr><th width="177">Name</th><th width="80">Type</th><th width="129">Example Values</th><th>Details</th></tr></thead><tbody><tr><td>platform</td><td>string</td><td>"iOS", "iPadOS"</td><td><p>The platform this is running on.<br></p><p>Note: for consistency we return "iPadOS" on all iPads, even OS versions before Apple started using the marketing name "iPadOS".<br></p><p>Note: iPad apps running on Mac  return "iPadOS", not MacOS.</p></td></tr><tr><td>user_interface_idiom</td><td>string</td><td>"phone", "tablet", "tv", "car", "computer", "unknown"</td><td><p>The type of device. Always one of the listed example values.</p><p></p><p>Note: iPad apps running on Mac  return "tablet", not "computer".</p></td></tr><tr><td>os_version</td><td>version string</td><td>"16.4.1"</td><td><p>The OS version number as a string.</p><p></p><p>Operators<a href="/pages/5x4sbjONEL2yQDjxk52F#version-number-operators"> are available</a> to compare version strings.</p><p></p><p>Note: iPad apps run on Mac will return the iPadOS simulator version they are running on, not the MacOS version of the host OS.</p></td></tr><tr><td>cm_version</td><td>version string</td><td>"1.2.3"</td><td><p>The version number of the CriticalMoments library on this client.</p><p></p><p>Operators<a href="/pages/5x4sbjONEL2yQDjxk52F#version-number-operators"> are available</a> to compare version strings.</p></td></tr><tr><td>device_manufacturer</td><td>string</td><td>Apple</td><td>The device manufacturer</td></tr><tr><td>device_model_class</td><td>string</td><td>"iPhone", "iPad" "iPod"</td><td>The manufacturer's description of the device type/class. See <code>user_interface_idiom</code> for a more general description of device type.</td></tr><tr><td>device_model</td><td>string</td><td>"iPhone13,3", "simulator", "iPad13,10"</td><td><p>The manufacturer's description string of this device. </p><p></p><p>List of Apple device models <a href="https://everyi.com/by-identifier/ipod-iphone-ipad-specs-by-model-identifier.html">here</a>. </p></td></tr><tr><td>device_model_version</td><td>nullable version string</td><td>"13.3", "14.2", nil</td><td><p>The floating point portion of device_model as a string. For example, for device_model "iPhone13,3" this field holds "13.3"<br><br>Operators<a href="/pages/5x4sbjONEL2yQDjxk52F#version-number-operators"> are available</a> to compare version strings.<br><br>Useful to find sets of devices, for example, to find iPhones newer than the iPhone 12: "device_model_class == "iPhone" &#x26;&#x26; versionNumberComponent(device_model_version, 0) > 13". Yes, the "iPhone 12" has 13 in it’s device_model string… ask Apple not me.<br></p><p>Usually used in conjunction with device_model_class as value comparisons typically only makes sense within a class.</p><p></p><p>Nil on simulators, but populated on real hardware.</p></td></tr><tr><td>dark_mode</td><td>bool</td><td>true, false</td><td>True if the device is set to use the system’s "dark mode" style.</td></tr><tr><td>interface_orientation</td><td>string</td><td>"landscape", "portrait"</td><td><p>The orientation the main screen’s user interface is rendered in.   </p><p></p><p>This may not match the physical orientation if the app only supports some orientations, or the user has rotation lock on.  </p><p></p><p>For the orientation of the physical device regardless of interface rendering, see device_orientation.</p></td></tr><tr><td>device_orientation</td><td>string</td><td>"landscape", "portrait", "face_up", "face_down", "unknown"</td><td><p>The device’s physical orientation, regardless of the orientation the UI is rendered.</p><p></p><p>  Returns face up/down if the device is nearly flat, such as sitting on a table. </p><p></p><p>Returns "unknown" on simulator or devices without accelerometers (computers). </p><p></p><p> For the orientation of the user interface rendering, see interface_orientation.</p></td></tr><tr><td>screen_width_points &#x26; screen_height_points</td><td>integer</td><td>853</td><td><p>The device screen size in "points" - <a href="https://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/">https://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/</a><br></p><p>The height is always the greater of the two; these are rotation independent. For current orientation, see interface_orientation and device_orientation.</p></td></tr><tr><td>screen_width_pixels &#x26; screen_height_pixels</td><td>integer</td><td>2556</td><td><p>The device screen size in pixels. </p><p></p><p>The height is always the greater of the two; these are rotation independent. For current orientation, see interface_orientation and device_orientation.</p></td></tr><tr><td>screen_scale</td><td>float</td><td>1.0, 2.0, 3.0</td><td><a href="https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale">https://developer.apple.com/documentation/uikit/uiscreen/1617836-scale</a></td></tr><tr><td>screen_brightness</td><td>float</td><td>0.1, 0.9</td><td>The current brightness of the user's screen. Ranges from 0.0 to 1.0 inclusive.</td></tr><tr><td>screen_captured</td><td>bool</td><td>true, false</td><td>True if the user's screen is currently being captured (Airplay mirroring, screen recording, etc).</td></tr><tr><td>locale_language_code</td><td>string</td><td>"en", "es"</td><td>The language from the user-preferred locale</td></tr><tr><td>locale_country_code</td><td>string</td><td>"CA", "US"</td><td>The country from the user-preferred locale</td></tr><tr><td>locale_currency_code</td><td>string</td><td>"CAD", "USD", "EUR", "JPY"</td><td>The currency code from the user-preferred locale.</td></tr><tr><td>locale_language_direction</td><td>string</td><td>"RTL", "LTR"</td><td>Is the device's language right-to-left or left-to-right.</td></tr><tr><td>app_id</td><td>string</td><td>"io.criticalmoments.sampleapp"</td><td><p>The app’s ID. </p><p></p><p>On Apple devices this is the bundle identifier.</p></td></tr><tr><td>app_version</td><td>version string</td><td>"1.2.3.4"</td><td><p>The app version number as a string, such as "1.2.2".   </p><p></p><p>Operators<a href="/pages/5x4sbjONEL2yQDjxk52F#version-number-operators"> are available</a> to compare version strings.</p></td></tr><tr><td>app_install_date</td><td>timestamp</td><td>See <a href="/pages/18VGmiy8YtvWYpZTkqBO">Working with Dates</a>.</td><td><p>The timestamp this app was installed by the user. </p><p></p><p>If deleted and reinstalled, this will reflect the latest install.</p></td></tr><tr><td>app_start_time</td><td>timestamp</td><td>See <a href="/pages/18VGmiy8YtvWYpZTkqBO">Working with Dates</a>.</td><td>The timestamp the app was launched. This is set to when the app started up, and does not get reset to a newer time if the app cycles into the background, then back to forground. If the app is terminated and restarted, it will be set to the latest app start time.</td></tr><tr><td>session_start_time</td><td>timestamp</td><td>See <a href="/pages/18VGmiy8YtvWYpZTkqBO">Working with Dates</a>.</td><td>The time the current user session started. A session start both when the app is initially launced, and if the app enters the foreground after 10 mintues or more in the background.  </td></tr><tr><td>is_debug_build</td><td>bool</td><td>true, false</td><td>True if this app was built as a debug build</td></tr><tr><td>device_battery_level</td><td>version string</td><td>0.88</td><td><p>Battery level ranges from 0.0 (fully discharged) to 1.0 (100% charged).<br></p><p>Returns 1.0 on devices that don’t have batteries (simulators, wired computers).</p></td></tr><tr><td>device_battery_state</td><td>string</td><td>"charging", "full", "unplugged", "unknown"</td><td><p>The battery state. Always one of the example values.</p><p></p><p>"full" indicates full and connected to power. To check if it's plugged in, you should check that it's "charging" or "full".<br></p><p>May return unknown on devices that don’t have batteries (simulators, wired computers)</p></td></tr><tr><td>device_low_power_mode</td><td>bool</td><td>true, false</td><td>Returns true if the user has low power mode enabled (typically enabled to save battery).   If this system doesn’t have a low power mode, false is returned.</td></tr><tr><td>network_connection_type</td><td>string</td><td>"wifi", "cellular", "wired", "unkown"</td><td><p>Indicates the type of network connection the device currently uses. </p><p></p><p>Does not imply an working internet connection (see has_active_network).   Device could be connected to an AP that isn't connected to the internet.</p><p></p><p>"unknown" may mean no network connection, but also may be returned when there is an active connection but the type is not known. Check has_active_network to determine if there’s a working internet connection.</p></td></tr><tr><td>has_active_network</td><td>bool</td><td>true, false</td><td>True if the device has an working internet connection</td></tr><tr><td>low_data_mode</td><td>nullable bool</td><td>true, false, nil</td><td><p>Indicates if the user has low data mode enabled, indicating they want to save data.<br></p><p>This value may be nil on devices that do not have this mode (for example, iOS 12 and earlier). If you only want to check for truthiness you can simply check (<code>low_data_mode ?? false)</code> using the nil coalesing operator <code>??</code>.</p><p></p><p><a href="https://support.apple.com/en-us/HT210596">https://support.apple.com/en-us/HT210596</a></p></td></tr><tr><td>expensive_network</td><td>bool</td><td>true, false</td><td><p>True if the network is considered to be expensive by the operating system, such as a cellular network, or wifi connection to a cellular hotspot. False if the OS doesn't have a concept of expensive networks or the network is "cheap".</p><p></p><p><a href="https://developer.apple.com/documentation/network/2976891-nw_path_is_expensive?language=objc">https://developer.apple.com/documentation/network/2976891-nw_path_is_expensive</a></p></td></tr><tr><td>has_wifi_connection</td><td>bool</td><td>true, false</td><td><p>Indicates if the device is connected to a wifi network.<br></p><p><strong>IMPORTANT</strong>: this does not indicate that wifi is the primary network (it may be using cellular — see network_connection_type), or that the wifi connection has an internet connection (see has_active_network). This only indicated that the device is connected to a wifi access point.</p></td></tr><tr><td>has_cell_connection</td><td>bool</td><td>true, false</td><td><p>True if the device is connected to a cellular network.<br></p><p><strong>IMPORTANT</strong>: this does not indicate that cellular is the primary network (it may be using wifi — see network_connection_type), or that the cellular network has an internet connection (see has_active_network). Only that the device is connected to a cellular network.</p></td></tr><tr><td>on_call</td><td>bool</td><td>true, false</td><td>True if the user is currently on a call (including phone, voip, facetime, etc).</td></tr><tr><td>other_audio_playing</td><td>bool</td><td>true, false</td><td>True if audio is playing from another app on the system (example: Spotify, Youtube).</td></tr><tr><td>has_headphones</td><td>bool</td><td>true, false</td><td><p>True if the device is connected to headphones (regardless if audio is playing). Can be bluetooth or wired. </p><p></p><p>Does not include BT headsets used for phone calls; higher end headphones may temporarily report as headsets during a call, but will report as headphones at other times.</p></td></tr><tr><td>has_bt_headphones</td><td>bool</td><td>true, false</td><td><p>True if the device is connected to bluetooth headphones (regardless if audio is playing).</p><p></p><p>BT Headphones differentiate from BT headsets based on audio quality (headphones = BT A2DP, headsets = BT HFP). BT headphones being used for a phone call may temporarily report as headsets during a call, but will report as headphones at other times.</p></td></tr><tr><td>has_bt_headset</td><td>bool</td><td>true, false</td><td><p>True if the device is connected to a bluetooth headset.</p><p></p><p>Headphones differentiate from headsets based on audio quality (headphones = BT A2DP, headsets = BT HFP). BT headphones being used for a phone call may temporarily report as headsets during a call, but will report as headphones at other times.</p></td></tr><tr><td>has_wired_headset</td><td>bool</td><td>true, false</td><td>True if the device is connected to a wired headset or headphones (wired microphone, wired headphones, or both).</td></tr><tr><td>has_car_audio</td><td>bool</td><td>true, false</td><td><p>True if the device is connected to a car audio system.</p><p></p><p>Some cars may report as bluetooth headphones/headsets (and not car audio), depending on connection method.</p></td></tr><tr><td>has_watch</td><td>bool</td><td>true, false</td><td>True if the user has an watch paired to this device.</td></tr><tr><td>app_state</td><td>string</td><td>active, inactive, background, unknown</td><td>The applications state. See <a href="https://developer.apple.com/documentation/uikit/uiapplication/1623003-applicationstate">Apple Documentation</a> on meaning of each state.</td></tr><tr><td>foreground</td><td>bool</td><td>true, false</td><td>True if the app is rendering in the foreground. False if the app is running in the background.</td></tr><tr><td>Random number generation</td><td>na</td><td>na</td><td>Random number generation is available as <a href="/pages/5x4sbjONEL2yQDjxk52F#random-number-generation">operators</a>.</td></tr><tr><td>timezone_gmt_offset</td><td>int</td><td>-18000</td><td>The device's timezone, expressed as an offset in seconds from GMT. For example, -5h from GMT returns -18000</td></tr><tr><td>location_permission</td><td>bool</td><td>true, false</td><td>True if the user has granted permission for this app to access location data.</td></tr><tr><td>location_permission_detailed</td><td>string</td><td>authorized_always, denied</td><td><p>The permission level this app has to access location data.</p><p></p><p>One of: not_determined, restricted, denied, authorized_always, authorized_when_in_use, unknown. See <a href="https://developer.apple.com/documentation/corelocation/clauthorizationstatus?language=objc">Apple docs for details</a>.</p></td></tr><tr><td>location_latitude</td><td>nullable float</td><td>43.642567</td><td>The latitude of this device. Can be nil of location not authorized. <a href="/pages/VAp0d7wQZfL6G869lHIK">See details</a>.</td></tr><tr><td>location_longitude</td><td>nullable float</td><td>-79.387054</td><td>The longitude of this device. Can be nil of location not authorized. <a href="/pages/VAp0d7wQZfL6G869lHIK">See details</a>.</td></tr><tr><td>location_city</td><td>nullable string</td><td>Toronto</td><td>The name of the city, from the device's current location. Can be nil if location not authorized, or if not in a city. <a href="/pages/VAp0d7wQZfL6G869lHIK">See details</a>.</td></tr><tr><td>location_region</td><td>nullable string</td><td>"ON" (for Ontario)</td><td>The short name of the province/state, from the device's current location. Can be nil if location not authorized, or if not in a region. <a href="/pages/VAp0d7wQZfL6G869lHIK">See details</a>.</td></tr><tr><td>location_country</td><td>nullable string</td><td>"CA" (for Canada)</td><td>The ISO country name, from the device's current location. Can be nil if location not authorized, or if not in a country. <a href="/pages/VAp0d7wQZfL6G869lHIK">See details</a>.</td></tr><tr><td>location_approx_latitude</td><td>nullable float</td><td>43.642567</td><td>The approximate latitude of this device, inferred from the device's IP. Does not require location permissions. Less accurate than GPS location, and may be incorrect.</td></tr><tr><td>location_approx_longitude</td><td>nullable float</td><td>-79.387054</td><td>The approximate longitude of this device, inferred from the device's IP. Does not require location permissions. Less accurate than GPS location, and may be incorrect.</td></tr><tr><td>location_approx_city</td><td>nullable string</td><td>Toronto</td><td>The name of the city, from the device's approximate location inferred from the device's IP. Does not require location permissions. Less accurate than GPS location, and may be incorrect.</td></tr><tr><td>location_approx_region</td><td>nullable string</td><td>"ON" (for Ontario)</td><td>The name of the short name of the province/state, from the device's approximate location inferred from the device's IP. Does not require location permissions. Less accurate than GPS location, and may be incorrect.</td></tr><tr><td>location_approx_country</td><td>nullable string</td><td>"CA" (for Canada)</td><td>The ISO country name, from the device's approximate location inferred from the device's IP. Does not require location permissions. Less accurate than GPS location, and may be incorrect.</td></tr><tr><td>weather_temperature</td><td>nullable float</td><td>22.5</td><td><p>The temperature in the user's location in celsius.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>weather_apparent_temperature</td><td>nullable float</td><td>23.5</td><td><p>The apparent (feel like) temperature in the user's location in celsius.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>weather_condition</td><td>nullable string</td><td>"Drizzle", "Clear", "Snow", etc</td><td><p>A string describing the weather conditions in the user's current location. </p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details, restrictions, and possible return values.</p></td></tr><tr><td>weather_cloud_cover</td><td>nullable float</td><td>0.05, 0.99</td><td><p>The percentage of the sky covered with clouds in the user's location.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>is_daylight</td><td>string</td><td>"unknown", "daylight", "not_daylight"</td><td><p>Is it daylight in the user's location. </p><p></p><p>Uses the weather service to lookup local sunrise/sunset times. Consider using <a href="/pages/18VGmiy8YtvWYpZTkqBO">local time of day</a> if you only need time-of-day.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>weather_approx_location_temperature</td><td>nullable float</td><td>22.5</td><td><p>The temperature in the user's approximate location, in celsius.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>weather_approx_location_apparent_temperature</td><td>nullable float</td><td>23.5</td><td><p>The apparent (feel like) temperature in the user's approximate location, in celsius.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>weather_approx_location_condition</td><td>string</td><td>"Drizzle", "Clear", "Snow", etc</td><td><p>A string describing the weather conditions in the user's approximate location. <a href="https://developer.apple.com/documentation/weatherkit/weathercondition">Possible values listed here</a>.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details, restrictions, and possible return values.</p></td></tr><tr><td>weather_approx_location_cloud_cover</td><td>nullable float</td><td>0.05, 0.99</td><td><p>The percentage of the sky covered with clouds in the user's approximate location.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>approx_location_is_daylight</td><td>string</td><td>"unknown", "daylight", "not_daylight"</td><td><p>Is it daylight in the user's approximate location.</p><p></p><p>Uses the weather service to lookup local sunrise/sunset times. Consider using <a href="/pages/18VGmiy8YtvWYpZTkqBO">local time of day</a> if you only need time-of-day.</p><p></p><p>See the <a href="/pages/VAp0d7wQZfL6G869lHIK#weather">Weather Guide</a> for details and restrictions.</p></td></tr><tr><td>contacts_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to contacts. Possible values: not_determined, restricted, denied, authorized, limited, unknown. <a href="https://developer.apple.com/documentation/contacts/cnauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>camera_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to video capture. Possible values: not_determined, restricted, denied, authorized, unknown. <a href="https://developer.apple.com/documentation/avfoundation/avauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>microphone_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to audio capture. Possible values: not_determined, restricted, denied, authorized, unknown. <a href="https://developer.apple.com/documentation/avfoundation/avauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>notifications_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized the app to schedule and receive local and remote notifications. Possible values: not_determined, denied, authorized, provisional, ephemeral, unknown. <a href="https://developer.apple.com/documentation/usernotifications/unauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>photo_library_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to read/write the photo library. Possible values: not_determined, denied, authorized, restricted, limited, unknown. <a href="https://developer.apple.com/documentation/photokit/phauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>add_photo_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to add to the photo library. Possible values: not_determined, denied, authorized, restricted, limited, unknown. <a href="https://developer.apple.com/documentation/photokit/phauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>calendar_permission</td><td>string</td><td>authorized_full, denied, etc</td><td>Has the user authorized access to calendar/events. Possible values: not_determined, denied, authorized_full, authorized_write_only, restricted, unknown. <a href="https://developer.apple.com/documentation/eventkit/ekauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>reminders_permission</td><td>string</td><td>authorized_full, denied, etc</td><td>Has the user authorized access to reminders. Possible values: not_determined, denied, authorized_full, authorized_write_only, restricted, unknown. <a href="https://developer.apple.com/documentation/eventkit/ekauthorizationstatus?language=objc">Apple docs</a>.</td></tr><tr><td>bluetooth_permission</td><td>string</td><td>authorized, denied, etc</td><td>Has the user authorized access to bluetooth. Possible values: not_determined, denied, authorized, restricted, unknown. <a href="https://developer.apple.com/documentation/corebluetooth/cbmanagerauthorization?language=objc">Apple docs</a>.</td></tr><tr><td>canOpenUrl(string)</td><td>bool</td><td>true, false</td><td><p>This function returns if this app can open the provided url, which is useful for checking if other apps are installed. For example <code>canOpenUrl('spotify:')</code>. </p><p></p><p>See <a href="/pages/5x4sbjONEL2yQDjxk52F">full documentation</a>.</p></td></tr><tr><td>propertyEver(string, value)</td><td>bool</td><td>true, false</td><td><p>Returns if a property has ever had a given value, using a local database tracking prior observed values. </p><p></p><p>The value can be a string, int, float, bool or date. The value must match the named property's type or it will return false.</p><p></p><p><a href="/pages/5x4sbjONEL2yQDjxk52F#propertyever-propnamestring-value">See docs for details</a>.</p></td></tr></tbody></table>

This product includes GeoLite2 data created by MaxMind, available from <https://www.maxmind.com>. Specifically, the GeoLite2 data is used for the `location_approx_*` properties.


# Custom Properties

Bring your own data to the party!

You can add your own custom properties for use in the conditional targeting system. Adding more data about your users/app can provide more powerful options for targeting and optimization.

### Well Known vs Custom Properties

There are two types of custom properties:

* Well known properties: A set of properties we define, and you set. These are common things that most apps have like "user\_signup\_date". By using this well known set, our optimization system can incorporate these properties (when provided) and make smarter optimization decisions. We suggest you set as many of these that make sense for your app's user experience.
* Custom properties can be any key/value pairs which you might find useful for targeting

### Well Known Property List

| Property Name           | Type              | Description                                                                                                                             |
| ----------------------- | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| user\_signup\_date      | datetime (NSDate) | The timestamp of when this user signed up for your app/service.                                                                         |
| user\_signed\_in        | bool              | Is the user signed into an account                                                                                                      |
| have\_user\_email       | bool              | Do we have an email address tied to this user's account.                                                                                |
| user\_email\_validated  | bool              | Has the user validated their email, confirming they own it.                                                                             |
| have\_user\_phone       | bool              | Do we have phone number tied to this user's account.                                                                                    |
| user\_age               | int               | The user's exact age, in years. Use user\_approx\_age if you only know the approximate age of the user.                                 |
| user\_approx\_age       | int               | The user's approximate age, in years. Use user\_age if you know the exact age of the user.                                              |
| user\_pronouns          | string            | The user's preferred pronouns. Example: she\_her, he\_him, they\_them, other, declined\_to\_say                                         |
| user\_gender            | string            | The user's gender. Examples: male, female, nonbinary, declined\_to\_say.                                                                |
| user\_inferred\_gender  | string            | The user's gender, if inferred and not user specified.                                                                                  |
| has\_paid\_subscription | bool              | Does this user have a paid subscription.                                                                                                |
| ever\_subscribed        | bool              | True if the user has ever been a paid subscriber, even if they no longer currently subscribe.                                           |
| has\_purchased          | bool              | Has the user ever made a purchase.                                                                                                      |
| purchase\_count         | int               | Count of all purchases made.                                                                                                            |
| total\_purchase\_value  | float             | Total purchase value (excluding subscriptions), this user had made. Unit/currency is up to you, but should be consistent for all users. |
| referral\_source        | string            | The name of how this user was referred to your app (fb\_ad, google\_ad, organic, search, user\_referral, etc).                          |
| referral\_id            | string            | An ID relating to the referral source (example: campaign ID)                                                                            |
| user\_was\_referred     | bool              | Was this user referred by another user?                                                                                                 |
| user\_referral\_count   | int               | How many users has this user referred.                                                                                                  |
| session\_source         | string            | How did the user launch the app for this app session? Examples: deeplink, notification, manual\_launch, email\_link, web\_link, etc.    |

## Setting Properties in Code

You can set properties in code using the following functions of the `CriticalMoments.sharedInstance` object.

{% hint style="info" %}
Ideally set properties before calling `start` on CriticalMoments. Some events like `app_start` occur very early, and you'll want properties to be available.&#x20;

If your properties are async, consider firing an `app_loaded` event after setting them, which you can use as a trigger for conditions requiring them.
{% endhint %}

{% tabs %}
{% tab title="Swift Example" %}

```swift
// Optionally try/catch for errors
let cm = CriticalMoments.sharedInstance()
try? cm.setBoolProperty(true, forKey: "has_pro_account")
try? cm.setIntegerProperty(42, forKey: "max_game_level")
try? cm.setStringProperty("creator", forKey: "user_persona")
try? cm.setFloatProperty(42.42, forKey: "total_spending")
try? cm.setTimeProperty(Date.now, forKey: "account_upgrade_timestamp")
```

{% endtab %}

{% tab title="Objective-C Example" %}

```objectivec
# Optionally check return `success` value and error
CriticalMoments* cm = CriticalMoments.sharedInstance;
[cm setBoolProperty:true forKey:@"has_pro_account" error:nil];
[cm setIntegerProperty:42 forKey:@"max_game_level" error:nil];
[cm setStringProperty:@"creator" forKey:@"user_persona" error:nil];
[cm setFloatProperty:42.42 forKey:@"total_spending" error:nil];
[cm setTimeProperty:[NSDate now] forKey:@"account_upgrade_timestamp" error:nil];
```

{% endtab %}
{% endtabs %}

## Setting Many Properties via JSON

You can set a set of properties from JSON formatted data. This can be very helpful, allowing your to add properties to shipping clients without updating the app.

Properties must be set before calling `[CriticalMoments.sharedInstance start]`. Properties set after calling start will be ignored.

Generally this API is used like follows:

* You create a server endpoint (for example `/user_properties`), which is authenticated and retrieves information about this user in JSON format.
* Call this server API on launch, and then pass the resulting JSON to `registerPropertiesFromJson`
* Optionally cache this result for faster startup (used cache results on launch, refresh cache for next launch).

{% tabs %}
{% tab title="Swift Example" %}

```swift
let jsonData = """
{
  "bool_property": true,
  "int_property": 42,
  "float_property": 42.42,
  "string_property": "hello world"
}
""".data(using: .utf8)!

do {
    try CriticalMoments.sharedInstance().setPropertiesFromJson(jsonData)
} catch {
    print("JSON parsing error: \(error).")
}
```

{% endtab %}

{% tab title="Objective C Example" %}
{% code overflow="wrap" fullWidth="true" %}

```objectivec
NSData* jsonData = [@"\
{\
  \"bool_property\": true,\
  \"int_property\": 42,\
  \"float_property\": 42.42,\
  \"string_property\": \"hello world\"\
}" dataUsingEncoding:NSUTF8StringEncoding];

NSError* jsonError;
[CriticalMoments.sharedInstance registerPropertiesFromJson:jsonData error:&jsonError];
if (jsonError != nil) {
    NSLog(@"There was an issue with the JSON: %@", jsonError.localizedDescription);
}
```

{% endcode %}
{% endtab %}
{% endtabs %}


# Syntax and Operators

Details on the syntax and operators available in conditional strings

{% hint style="info" %}
Critical Moments uses the excellent [expr library](https://expr.medv.io) for conditional evaluation. Portions of this document are from their documentation, and those portions are under a MIT licence. Their original documentation is available [here](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md), and licensed [here](https://github.com/antonmedv/expr/blob/master/LICENSE).

Portions of this document are extensions specific to Critical Moments, all rights reserved.
{% endhint %}

### Literals

| Comment | `/* */` or `//`      |
| ------- | -------------------- |
| Boolean | `true`, `false`      |
| Integer | `42`, `0x2A`         |
| Float   | `0.5`, `.5`          |
| String  | `"foo"`, `'bar'`     |
| Array   | `[1, 2, 3]`          |
| Map     | `{a: 1, b: 2, c: 3}` |
| Nil     | `nil`                |

### Operators

| Arithmetic  | `+`, `-`, `*`, `/`, `%` (modulus), `^` or `**` (exponent) |
| ----------- | --------------------------------------------------------- |
| Comparison  | `==`, `!=`, `<`, `>`, `<=`, `>=`                          |
| Logical     | `not` or `!`, `and` or `&&`, `or` or `\|\|`               |
| Conditional | `?:` (ternary), `??` (nil coalescing)                     |
| Membership  | `[]`, `.`, `?.`, `in`                                     |
| String      | `+` (concatenation), `contains`, `startsWith`, `endsWith` |
| Regex       | `matches`                                                 |
| Range       | `..`                                                      |
| Slice       | `[:]`                                                     |
| Pipe        | `\|`                                                      |

Examples:

```expr
user.Age in 18..45 and user.Name not in ["admin", "root"]
```

```expr
foo matches "^[A-Z].*"
```

```expr
a_list | filter(.Size < 280) | map(.Content) | join(" -- ")
```

```expr
filter(posts, {now() - .CreatedAt >= 7 * duration("24h")})
```

#### Membership Operator

Fields of structs and items of maps can be accessed with `.` operator or `[]` operator. Elements of arrays and slices can be accessed with `[]` operator. Negative indices are supported with `-1` being the last element.

The `in` operator can be used to check if an item is in an array or a map.

```expr
user.Name in ["admin", "owner"]
```

**Optional chaining**

The `?.` operator can be used to access a field of a struct or an item of a map without checking if the struct or the map is `nil`. If the struct or the map is `nil`, the result of the expression is `nil`.

```expr
author?.User?.Name
```

**Nil coalescing**

The `??` operator can be used to return the left-hand side if it is not `nil`, otherwise the right-hand side is returned.

```expr
author?.User?.Name ?? "Anonymous"
```

#### Slice Operator

The slice operator `[:]` can be used to access a slice of an array.

For example, if `array` is `[1, 2, 3, 4, 5]`:

```expr
array[1:4] == [2, 3, 4]
array[1:-1] == [2, 3, 4]
array[:3] == [1, 2, 3]
array[3:] == [4, 5]
array[:] == array
```

#### Pipe Operator

The pipe operator `|` can be used to pass the result of the left-hand side expression as the first argument of the right-hand side expression.

For example, expression `split(lower(user.Name), " ")` can be written as:

```expr
user.Name | lower() | split(" ")
```

### String Functions

#### trim(str\[, chars])

Removes white spaces from both ends of a string `str`. If the optional `chars` argument is given, it is a string specifying the set of characters to be removed.

```expr
trim("  Hello  ") == "Hello"
trim("__Hello__", "_") == "Hello"
```

#### trimPrefix(str, prefix)

Removes the specified prefix from the string `str` if it starts with that prefix.

```expr
trimPrefix("HelloWorld", "Hello") == "World"
```

#### trimSuffix(str, suffix)

Removes the specified suffix from the string `str` if it ends with that suffix.

```expr
trimSuffix("HelloWorld", "World") == "Hello"
```

#### upper(str)

Converts all the characters in string `str` to uppercase.

```expr
upper("hello") == "HELLO"
```

#### lower(str)

Converts all the characters in string `str` to lowercase.

```expr
lower("HELLO") == "hello"
```

#### split(str, delimiter\[, n])

Splits the string `str` at each instance of the delimiter and returns an array of substrings.

```expr
split("apple,orange,grape", ",") == ["apple", "orange", "grape"]
split("apple,orange,grape", ",", 2) == ["apple", "orange,grape"]
```

#### splitAfter(str, delimiter\[, n])

Splits the string `str` after each instance of the delimiter.

```expr
splitAfter("apple,orange,grape", ",") == ["apple,", "orange,", "grape"]
splitAfter("apple,orange,grape", ",", 2) == ["apple,", "orange,grape"]
```

#### replace(str, old, new)

Replaces all occurrences of `old` in string `str` with `new`.

```expr
replace("Hello World", "World", "Universe") == "Hello Universe"
```

#### repeat(str, n)

Repeats the string `str` `n` times.

```expr
repeat("Hi", 3) == "HiHiHi"
```

#### indexOf(str, substring)

Returns the index of the first occurrence of the substring in string `str` or -1 if not found.

```expr
indexOf("apple pie", "pie") == 6
```

#### lastIndexOf(str, substring)

Returns the index of the last occurrence of the substring in string `str` or -1 if not found.

```expr
lastIndexOf("apple pie apple", "apple") == 10
```

#### hasPrefix(str, prefix)

Returns `true` if string `str` starts with the given prefix.

```expr
hasPrefix("HelloWorld", "Hello") == true
```

#### hasSuffix(str, suffix)

Returns `true` if string `str` ends with the given suffix.

```expr
hasSuffix("HelloWorld", "World") == true
```

### Date Functions

The following operators can be used to manipulate dates. See more examples in [Working with Dates](/conditional-targeting/conditional-guides/working-with-dates).

```expr
date("2023-08-14") + duration("1h")
date("2023-08-14") - duration("1h")
date("2023-08-14") - date("2023-08-13") == duration("24h")
```

#### now()

Returns the current date and time.

```expr
createdAt > now() - duration(1h)
```

#### duration(str)

The duration function takes a string such as `1.5h`, `24h`, or `-30s`. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".

Durations can be added to datetimes like so: `now() - duration('12h')`

#### date(str\[, format\[, timezone]])

Converts the given string `str` into a date.

* str \[required, string]: required, the date string to parse
* format \[optional, string]: the format of the date string. If omitted we'll try a variety of formats to try to parse the given string. Example: `date('2024-01-15T22:00:00-08:00', RFC3339)`. The following variables can be provided to parse for common standards.
  * date\_format: `YYYY-MM-DD` format
  * date\_and\_time\_format: `YYYY-MM-DD HH:MM:SS.SSSSSS` format
  * date\_with\_tz\_format: A `YYYY-MM-DDZ` date with a timezone. Example `2021-01-01Z` for UTC or `2021-01-01+05:00`for a 5 hour offset
  * RFC3339 (allowing fractional seconds)
  * RFC822
  * RFC850
  * RFC1123
  * RFC822Z: RFC822 with numeric zone like `-700`
  * RFC1123Z: RFC1123 with numeric zone like `-700`
  * Custom strings: supported using the golang time package time format [described here](https://pkg.go.dev/time)
* timezone \[optional, string]: The timezone to parse in. Possible values include:
  * "UTC", empty string, or not specified - UTC
  * "Local": the user's local timezone
  * A location name corresponding to a file in the IANA Time Zone database, such as "America/New\_York"

See examples in [Working with Dates](/conditional-targeting/conditional-guides/working-with-dates).

#### formatTime(timestamp, format, \[timezone])&#x20;

This function can be used to format timestamps, and extract components of a date. It returns the provided timestamp, formatted in the requested format. It returns an integer when possible (hour of day, month of year, etc) and a string when not possible (name of the month of the year).

Example: `formatTime(now(), 'year') == 2024`

* Timestamp: a timestamp such as `now()`, the result of a call to `date(...)`, or the result of a call to a `unixTime*(...)`call
* Format: a string format. One of the following strings:
  * "dow": the int day of week. Sunday is 0, Saturday is 6.
  * "dow\_short": the string short form day of week. Mon, Tue, etc
  * "dow\_long": the string full form day of week. Monday, Tuesday, etc
  * "dom": the int day of month (1-31)
  * "hod": the int hour of the day (0-23)
  * "moh": the int minut of the hour (0-59)
  * "ampm": "AM" or "PM"
  * "month": the int month. Jan is 1, Dec is 12.
  * "month\_short": the short form name of month. Example: Jan
  * "month\_long": the full form name of month. Example: January.
  * "year": the int year. Example, 1999
  * [A golang formatted time layout string](https://pkg.go.dev/time#pkg-constants)
* Timezone (optional): the timezone to apply to the timestamp before formatting. If not provided, defaults to the user's local timezone. Can be "Local" (user local), "UTC" or a location name corresponding to a file in the IANA Time Zone database, such as "America/Toronto".

#### unixTimeSeconds(int), unixTimeMilliseconds(int), unixTimeNanoseconds(int) Functions

These three functions take in a unix timestamp (time since January 1, 1970 UTC) and return a datetime for that point in time. Negative values are supported for times before 1970.

{% hint style="info" %}
Be sure you've call the appropriate function. Different libraries provide unix time in seconds, milliseconds or nanoseconds.&#x20;
{% endhint %}

### Number Functions

#### max(n1, n2)

Returns the maximum of the two numbers `n1` and `n2`.

```expr
max(5, 7) == 7
```

#### min(n1, n2)

Returns the minimum of the two numbers `n1` and `n2`.

```expr
min(5, 7) == 5
```

#### abs(n)

Returns the absolute value of a number.

#### ceil(n)

Returns the least integer value greater than or equal to x.

```expr
ceil(1.5) == 2.0
```

#### floor(n)

Returns the greatest integer value less than or equal to x.

```expr
floor(1.5) == 1.0
```

#### round(n)

Returns the nearest integer, rounding half away from zero.

```expr
round(1.5) == 2.0
```

### Array Functions

#### all(array, predicate)

Returns **true** if all elements satisfies the predicate. If the array is empty, returns **true**.

```expr
all(a_list, {.Size < 280})
```

#### any(array, predicate)

Returns **true** if any elements satisfies the predicate. If the array is empty, returns **false**.

#### one(array, predicate)

Returns **true** if *exactly one* element satisfies the predicate. If the array is empty, returns **false**.

```expr
one(participants, {.Winner})
```

#### none(array, predicate)

Returns **true** if *all elements does not* satisfy the predicate. If the array is empty, returns **true**.

#### map(array, predicate)

Returns new array by applying the predicate to each element of the array.

```expr
map(a_list, {.Size})
```

#### filter(array, predicate)

Returns new array by filtering elements of the array by predicate.

```expr
filter(users, .Name startsWith "J")
```

#### find(array, predicate)

Finds the first element in an array that satisfies the predicate.

```expr
find([1, 2, 3, 4], # > 2) == 3
```

#### findIndex(array, predicate)

Finds the index of the first element in an array that satisfies the predicate.

```expr
findIndex([1, 2, 3, 4], # > 2) == 2
```

#### findLast(array, predicate)

Finds the last element in an array that satisfies the predicate.

```expr
findLast([1, 2, 3, 4], # > 2) == 4
```

#### findLastIndex(array, predicate)

Finds the index of the last element in an array that satisfies the predicate.

```expr
findLastIndex([1, 2, 3, 4], # > 2) == 3
```

#### groupBy(array, predicate)

Groups the elements of an array by the result of the predicate.

```expr
groupBy(users, .Age)
```

#### count(array, predicate)

Returns the number of elements what satisfies the predicate.

Equivalent to:

```expr
len(filter(array, predicate))
```

#### join(array\[, delimiter])

Joins an array of strings into a single string with the given delimiter. If no delimiter is given, an empty string is used.

```expr
join(["apple", "orange", "grape"], ",") == "apple,orange,grape"
join(["apple", "orange", "grape"]) == "appleorangegrape"
```

#### reduce(array, predicate\[, initialValue])

Applies a predicate to each element in the array, reducing the array to a single value. Optional `initialValue` argument can be used to specify the initial value of the accumulator. If `initialValue` is not given, the first element of the array is used as the initial value.

Following variables are available in the predicate:

* `#` - the current element
* `#acc` - the accumulator
* `#index` - the index of the current element

```expr
reduce(1..9, #acc + #)
reduce(1..9, #acc + #, 0)
```

#### sum(array)

Returns the sum of all numbers in the array.

```expr
sum([1, 2, 3]) == 6
```

#### mean(array)

Returns the average of all numbers in the array.

```expr
mean([1, 2, 3]) == 2.0
```

#### median(array)

Returns the median of all numbers in the array.

```expr
median([1, 2, 3]) == 2.0
```

#### first(array)

Returns the first element from an array. If the array is empty, returns `nil`.

```expr
first([1, 2, 3]) == 1
```

#### last(array)

Returns the last element from an array. If the array is empty, returns `nil`.

```expr
last([1, 2, 3]) == 3
```

#### take(array, n)

Returns the first `n` elements from an array. If the array has fewer than `n` elements, returns the whole array.

```expr
take([1, 2, 3, 4], 2) == [1, 2]
```

#### sort(array\[, order])

Sorts an array in ascending order. Optional `order` argument can be used to specify the order of sorting: `asc` or `desc`.

```expr
sort([3, 1, 4]) == [1, 3, 4]
sort([3, 1, 4], "desc") == [4, 3, 1]
```

#### sortBy(array, key\[, order])

Sorts an array of maps by a specific key in ascending order. Optional `order` argument can be used to specify the order of sorting: `asc` or `desc`.

```expr
sortBy(users, "Age")
sortBy(users, "Age", "desc")
```

### Map Functions

#### keys(map)

Returns an array containing the keys of the map.

```expr
keys({"name": "John", "age": 30}) == ["name", "age"]
```

#### values(map)

Returns an array containing the values of the map.

```expr
values({"name": "John", "age": 30}) == ["John", 30]
```

### Type Conversion Functions

#### type(v)

Returns the type of the given value `v`. Returns on of the following types: `nil`, `bool`, `int`, `uint`, `float`, `string`, `array`, `map`. For named types and structs, the type name is returned.

```expr
type(42) == "int"
type("hello") == "string"
type(now()) == "time.Time"
```

#### int(v)

Returns the integer value of a number or a string.

```expr
int("123") == 123
```

#### float(v)

Returns the float value of a number or a string.

#### string(v)

Converts the given value `v` into a string representation.

```expr
string(123) == "123"
```

#### toJSON(v)

Converts the given value `v` to its JSON string representation.

```expr
toJSON({"name": "John", "age": 30})
```

#### fromJSON(v)

Parses the given JSON string `v` and returns the corresponding value.

```expr
fromJSON('{"name": "John", "age": 30}')
```

#### toBase64(v)

Encodes the string `v` into Base64 format.

```expr
toBase64("Hello World") == "SGVsbG8gV29ybGQ="
```

#### fromBase64(v)

Decodes the Base64 encoded string `v` back to its original form.

```expr
fromBase64("SGVsbG8gV29ybGQ=") == "Hello World"
```

#### toPairs(map)

Converts a map to an array of key-value pairs.

```expr
toPairs({"name": "John", "age": 30}) == [["name", "John"], ["age", 30]]
```

#### fromPairs(array)

Converts an array of key-value pairs to a map.

```expr
fromPairs([["name", "John"], ["age", 30]]) == {"name": "John", "age": 30}
```

### Miscellaneous Functions

#### len(v)

Returns the length of an array, a map or a string.

#### get(v, index)

Retrieves the element at the specified index from an array or map `v`. If the index is out of range, returns `nil`. Or the key does not exist, returns `nil`.

```expr
get([1, 2, 3], 1) == 2
get({"name": "John", "age": 30}, "name") == "John"
```

### Random Number Generation

Critical moments provides 4 methods for random number generation.

All 4 return a 63-bit non-negative integer. Typically you want to use modulo operator (%) on the result of these functions. Example condition which returns true 5% of the time `rand() % 100 < 5`

#### rand()

rand() returns a random number. It returns a new random number every time it is invoked.

#### sessionRand()

sessionRand() returns a random number which is stable for the duration of this session (held in memory until app restarted).

#### stableRand()

stableRand() returns a random number that is stable for the lifetime of this app install. It is generated on first use, and stored to disk. It's useful if you want to have a random number consistently return the same value for a user (example: opting into an AB test).

#### randForKey(stringSeed, intSeed)

randForKey returns pseudo random number given a key seed, and int seed. It returns the same number if provided the same inputs. It's useful in a number of cases:

* For generating a stable random number across many devices using an input string or int: `randForKey('', user_id)` or `randForKey(user_email, 0)`
* For opting into AB tests, but shuffling for different experiments. For example, `randForKey('experiment1', stableRand()) % 100 == 0` and `randForKey('experiment2', stableRand()) % 100 == 0` will each return a consistent random int over time, but a different one than the other experiment.
* A combination of use cases (stable across device, and shuffling per use case): `randForKey(user_email + 'experiment1', 1)`

### Version Number Operators

Critical moments has a built in condition functions to help you compare version numbers.&#x20;

{% hint style="info" %}
Typically you don't want to use string comparisons on version numbers except for exact match checks. The check `"11.0" > "2.0"`will evaluate to false with string comparisons since "1" < "2".
{% endhint %}

#### Version Number Format

Version numbers are represented as strings of semantic version numbers:

* The simpliest format is `1.2.3`. This is our prefered style, and is used for built-in properties. There's no limit to the number of sub-versions you include.
* You can optionally prefix them with a v: `v1.2.3`&#x20;
* You can optionally postfix them with a dash followed by text `v1.2.3-beta`

#### versionGreaterThan , versionLessThan , versionEqual

These three functions can be used to compare 2 version number strings, and return a boolean result.

Version numbers with a postfix are considered less than otherwise equal version numbers without a postfix (v2.1-beta < v2.1), but we don't compare the values of the postfix strings (v2.1-alpha == v2.1-beta == v2.1-helloworld).

{% hint style="warning" %}
All three functions return false if passed an invalid version number. Be sure to pick the right operator to fail as intended if your version number strings might be nil or invalid.
{% endhint %}

<details>

<summary>API Reference: <code>versionGreaterThan(a,b)</code> , <code>versionLessThan(a,b)</code> , <code>versionEqual(a,b)</code></summary>

#### Parameters

* a **\[string]**: a version number string
* b **\[string]**: a second version number string

#### Return Value

Boolean result of comparing a to b. For example `versionGreaterThan('v1.1', 'v1.0')` returns true. False if either string is not a valid version number.

</details>

<details>

<summary>Examples</summary>

* `versionGreaterThan(os_version, '16')` returns true if they are running OS version > 16.0, excluding 16.0 exactly
* `!versionLessThan(os_version, '16')` returns true if they are running OS version >= 16.0, including 16.0 exactly (note the not/!)
* `versionLessThan(app_version, '2.0')` the app version is less than 2.0
* `versionEqual(os_version, '16.4.1')` they are running exactly OS 16.4.1
* `versionGreaterThan('2.0+invalid', '1.0')` returns false since passed an invalid version number

</details>

#### function versionNumberComponent(versionString, componentInt)&#x20;

This function can be used in condition strings to extract individual components of a version number as integers. For example `versionNumberComponent("1.2.3", 1)` returns `2`.

<details>

<summary>API Reference: versionNumberComponent(versionString, index)</summary>

#### Parameters

* versionString **\[string]**: a version number string
* index **\[int]**: the index of the component to extract

#### Return Value

This function returns an integer if the versionString is valid, and the index exists in the version string. Otherwise it returns nil.

</details>

<details>

<summary>Examples of versionNumberComponent</summary>

* `versionNumberComponent("1.2.3", 0)` returns `1`
* `versionNumberComponent("1.2.3", 2)` returns `3`
* `versionNumberComponent("1.2.3", 4)` returns `nil`
* `versionNumberComponent("1.2.3.invalid_version", 0)` returns `nil`
* `versionNumberComponent(os_version, 0) >= 15` - true if the operating system version is v15 or over, regardless of sub-versions
* `versionNumberComponent(os_version, 0) >= 16 || (versionNumberComponent(os_version, 0) == 15 && versionNumberComponent(os_version, 1) ?? 0 >= 2)` - true if the operating system version is v15.2 or greater. Uses the nil coalescing operator `??` in case the version string is simply "15".

</details>

#### Exact version checks

In some cases, like checking for a known buggy version number, exact string comparisons may  be more succinct than `versionEqual(a,b)`. Examples:

* `os_version == "16.4.1"` returns true if the os version is exactly 16.4.1 (string comparison)
* `app_version in ["1.2.3", "2.3.1"]` returns true if the app version is in a set of exact strings

### Query Event History

#### eventCount(eventName)

Fetches the number of times an event has occurred from the events database.

#### eventCountWithLimit(eventName, limitInt)

Fetches the number of times an event has occurred from the events database, but the max count retuned will be limitInt. This provides a small performance improvement if checking a minimum count on a high volume event.

#### latestEventTime(eventName)

Returns the latest datetime when this event occurred.&#x20;

Function will return nil if that event was never fired. Example nil handling:

* Nil coalescing, using now as default value `(lastEventTime('event_name') ?? now())`&#x20;
* Ternary operator: `lastEventTime('event_name') == nil ? false : lastEventTime('event_name') < now() - duration('1h')`

#### propertyEver(propNameString, value)

Returns a boolean indicating if a property has ever had a given value, using a local database tracking prior observed values.&#x20;

The value can be a string, int, float, bool or date. The value must match the named property's type or it will return false.

For performance reasons, not all built in properties are tracked in the history, and they may sample at different times (app startup for some, lazily on access for others). Check this works for your use case locally before deploying.

### Checking Installed Apps

#### canOpenUrl(urlString)

This function returns a boolean indicating if this app can open the provided url, which is useful for checking if other apps are installed. For example `canOpenUrl('spotify:')` returns true if Spotify is installed.&#x20;

{% hint style="warning" %}
Non-standard URL schemes [need to be included in your app's info.plist](https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/LaunchServicesKeys.html#//apple_ref/doc/plist/info/LSApplicationQueriesSchemes). For URLs schemes not included, this function will return false.&#x20;
{% endhint %}

### Predicate

The predicate is an expression. It takes one or more arguments and returns a boolean value. To access the arguments, the `#` symbol is used.

```expr
map(0..9, {# / 2})
```

If items of the array is a struct or a map, it is possible to access fields with omitted `#` symbol (`#.Value` becomes `.Value`).

```expr
filter(a_list, {len(.Value) > 280})
```

Braces `{` `}` can be omitted:

```expr
filter(a_list, len(.Value) > 280)
```


# Conditional Guides

* [Working with dates ](/conditional-targeting/conditional-guides/working-with-dates)
* [Working with locations](/conditional-targeting/conditional-guides/locations-and-weather)
* [Event and Property History](/conditional-targeting/conditional-guides/event-and-property-history)


# Working with Dates

Helper functions for working with dates and times

Critical Moments provides a set of helper functions to make working with conditions strings with dates easy! Well, at least as easy as working with dates can be...

## Date Functions

* [Now function](/conditional-targeting/syntax-and-operators#now)
* [Parse date function](/conditional-targeting/syntax-and-operators#date-str-format-timezone)
* [Duration function](/conditional-targeting/syntax-and-operators#duration-str)
* [Format date function](/conditional-targeting/syntax-and-operators#formattime-timestamp-format-timezone)
* [Unix time functions](/conditional-targeting/syntax-and-operators#unixtimeseconds-int-unixtimemilliseconds-int-unixtimenanoseconds-int-functions)

## Example Conditions With Dates

* Did the user downloaded this app in the last 12 hours?
  * `app_install_date > now() - duration('12h')`
* Did the user install the app more than 30 days ago?
  * `app_install_date < now() - duration('720h')`
* Did the user downloaded the app before a given absolute datetime?
  * `app_install_date > date('2023-01-25T12:00:00+00:00')`
* Is the current device time after a date, in the user's local timezone? For example: launching new feature at midnight local time.
  * `now() > date('2024-04-05', date_format, 'Local')`
* Is the time between 10pm PST and 11pm PST on a given date? Example: you're taking your service down for maintenance and what to show a "we'll be back online soon" message:
  * `now() > date('2024-01-15T22:00:00-08:00') && now() < date('2024-01-15T23:00:00-08:00')`&#x20;
* Was the app was installed before a given unix timestamp?
  * `app_install_date < unixTimeMilliseconds(1136189045000)`
* Is it currently a Wednesday in the user's local timezone
  * `formatTime(now(), 'dow') == 3` or `formatTime(now(), 'dow_long') == 'Wednesday'`
* Is it currently the evening (5pm-9pm) in the user's local timzeone
  * `formatTime(now(), 'hod') >= 17 || formatTime(now(), 'hod') <= 21`&#x20;


# Locations and Weather

Here and now

## Location

Critical Moments offers two forms of location: system and approximate.

### System location

Most location lookups uses the system location framework (onboard GPS and cellular trilateration). System location is used for any location parameter without `_approx` in the name. When using these be aware:

* Location properties will return nil if the user has not granted permission to access to location. Your app should manage requesting this permission. Please follow this guide from [Apple](https://developer.apple.com/documentation/corelocation/requesting_authorization_to_use_location_services?language=objc). Your conditions should consider the possibility of a nil response. You can check if they have granted permision via the `location_permission` property.
* The SDK will initiate new location request at most once every 5 minutes to save battery. If your app (or the system) is requesting location more often it will use the latest cached value. As a result, the location may be up to 5 minutes stale.
* Evaluating conditions with location properties will show a ["location services" indicator](https://support.apple.com/en-us/HT207354) on the user's device.

### Approximate Location

Critical Moments provides a Geo-IP lookup service for inferring approximate location. Things to be aware of using approximate:

* Unlike system location, approximate location can be used without the system location permission
* Requests are being made to a Critical Moments server with the end user's IP when using the approximate location. Please ensure your terms of service and privacy policy allows this before using Geo-IP data.
* Approximate location will not work when offline.
* Approximate location may be incorrect as IPs can move between cities/regions over time. Approximate location should only be used for scenarios where incorrect data is tolerable.
* The lat/long returned should be considered approximate, and not used for use cased where precision is needed.
* Approximate location is cached for 20 minutes
* Approximate location uses GeoLite2 data created by MaxMind, available from <https://www.maxmind.com.&#x20>;

{% hint style="info" %}
**Rate Limit**:  During the private beta, approximate location requests have a soft-limit of 10,000 requests per month per app . Need more? [Contact us](/support)! Repeatedly or excessively exceeding your limit without contacting us for a higher limit, may result in approximate location services being disabled for that client/app.
{% endhint %}

### Reverse Geocoding / Place Names

"Reverse geocoding" looking up the name of a place, given a location (lat/long). Critical Moments offers helpful reverse geocoding helpers to get the city name, region name (state/province), and country name of a user's location. Some technical notes on these:

* Reverse geocoding results (city, region, and country), are cached for the entirety of the user's session.
* Reverse geocoding results may be nil if the device is not currently in a city/province/state/country (example, on the ocean).

## Weather Usage Restrictions

Critical Moments allows you to easily access the weather at your user's current location.

{% hint style="danger" %}
Weather uses Critical Moments servers, which are no longer actively maintained and require an API Key. We don't suggest new users integrate with weather.
{% endhint %}

The weather properties built into Critical Moments use an external weather service to provide local weather data. There are restructions about how you use this data:

* **Rate Limit**:  During the private beta, weather requests have a soft-limit of 3000 requests per month per app . Need more? [Contact us](/support)! Repeatedly or excessively exceeding your limit without contacting us for a higher limit, may result in weather services being disabled for that client/app.
* **Usage Limits**: You can use our weather data to make conditional decisions in your app, but you may not build an app or service which directly displays weather data to your users or any third parties. Violatiung these usage restrictions may result in weather services being disabled for the violoating app/client.
  * **Acceptable use examples**:
    * A travel app which features a section called "Escape the cold" with Caribbean vacations, when the temperature in the users location is less than -5 celsius
    * An in insurance app that displays a promotion for "Add flood coverage to your home plan" when it's raining.
    * A food delivery app which "Stay dry, order delivery" header when it's raining
  * **Unacceptable use examples**:&#x20;
    * A UI interface element which shows the user the temperature in the user's location to the user
    * A UI interface element which shows the current weather conditions (snow, rain) to the user in any form (textual, icon, etc)

#### Technical Notes

* Weather is cached for 20 minutes.
* Any weather value may return nil (or "unknown"), if the user's location can't be found, if they don't have a network connection, or weather data is not available for that location. Please ensure you conditionals can handle nil values for weather properties gracefully.
* Weather properties each have 2 options, one using [system location](#system-location), and another using [approximate location](#approximate-location). System location is less likely to be incorrect (using GPS lat/long instead of location inferred from IP), but will fail when the user has not given the app location permissions and will result in the location-privacy indicator appearing at the top of their phone. Consider conditionally using `location_permission` and the ternary operator to check if the user has granted location permission, falling back to approximate location; for example for temperature: `location_permission ? weather_temperature : weather_approx_location_temperature`
* We add a random offset to location when using the system location, before leaving the device (up to 1km, avg 500m). This prevents precise user location being sent to our servers, and 1km is close enough for weather data.
* Privacy impact: using weather properties requires making an external web services call to Critical Moments to fetch weather data. We make this as private as possible, only transmitting the minimal amount of data needed to provide weather data. Techically the process involves:
  * When fetching, Critical Moments will have access to the data passed, including the end user's latitude, longitude, and IP address, and the app's ID and API key. Critical Moments logs may capture this data in short term logs, but all user-level data will be deleted in time (currently 7 days).&#x20;
  * Aggregate usage data is tracked by Critical Moments long term at the app level to enforce rate limits; this data does not include user location data or user IP addresses.&#x20;
  * Critical Moments will call a third party weather service with latitude and longitude to retrieve weather data, but no data tying the location to a given user is passed to that service (including the user's IP address or app ID).&#x20;
  * Please ensure your terms of service and privacy policy allows this before using weather data.
* Weather data is currently provided from Apple Weather, and includes data from a variety of [data sources](https://developer.apple.com/weatherkit/data-source-attribution/).

#### Condidtion Values

The "condition" property may have the following values:

<details>

<summary>All Weather Condition String Values</summary>

```
BlowingDust
Clear
Cloudy
Foggy
Haze
MostlyClear
PartlyCloudy
Smoky
Breezy
Windy
Drizzle
HeavyRain
IsolatedThunderstorms
Rain
SunShowers
ScatteredThunderstorms
StrongStorms
Thunderstorms
Frigid
Hail
Hot
Flurries
Sleet
Snow
SunFlurries
WintryMix
Blizzard
BlowingSnow
FreezingDrizzle
FreezingRain
Hurricane
TropicalStorm
```

</details>


# Event and Property History

Critical Moments builds a local database of all events, and a sampling of the history of each property. This database can be queried in conditions using functions described below. This unlocks powerful use cases combining the user's current state, with their usage and device history.

{% hint style="success" %}
**Privacy Built In**

This database is local to the user's device, not server based. All queries and evaluation happen locally on the user's device.
{% endhint %}

### Event History

Every event is stored in the event history database, and can be queried with the following functions:

* [eventCount](/conditional-targeting/syntax-and-operators#eventcount-eventname)
* [eventCountWithLimit](/conditional-targeting/syntax-and-operators#eventcountwithlimit-eventname-limitint)
* [latestEventTime](/conditional-targeting/syntax-and-operators#latesteventtime-eventname)

#### Example

Presenting an important call to action like "rate us" or "subscribe" has the best chance of success after the user gets value from the app, but you typically want to wait until they have completed the action a few times before asking. Here's an example for a reminders app, which asks them to rate, but only after marking a task as complete a few times, and waiting 60 days between asks:

`eventCount('completed_task') > 4 && (latestEventTime('asked_to_subscribe')  == nil || latestEventTime('asked_to_subscribe') < now() - duration('60d'))`&#x20;

### Property History

You can access property history via the [propertyEver](/conditional-targeting/syntax-and-operators#propertyever-propnamestring-value) function in conditions.

#### Example

Checking if the user isn't on low\_power\_mode can improve the chance a call to action like "rate us" or "subscribe" succeed. However some users leave it on permanently and you don't want to ignore them forever. Your conditional check can be improved by ensuring they have had it off at some time in the past:

`(!device_low_power_mode && propertyEver('device_low_power_mode', false))`&#x20;

#### Sampling Rate

Properties are sampled at different times:

* Custom properties are sampled when set
* Most properties are sampled on app startup
* Some properties that are power or time intensive are only sampled when accessed.&#x20;

See [here](https://github.com/CriticalMoments/CriticalMoments/blob/65c3bc7f8804cb959952b2e01fc21b635123435b/go/cmcore/data_model/properties.go#L85) for how each propety is sampled, and take into account the sampling style when using this function.


# Event Overview

The more Critical Moments knows about your user's behaviours, the better you can target and optimize.&#x20;

{% hint style="success" %}
Be sure to log the important events in your user's journey for improved targeting and optimization.
{% endhint %}

There are three types of events in Critical Moments:

* Recommended events are events like "sign\_up", "in\_app\_purchase" that many apps have in common. We maintain a list of [well known / recommended events](/events/recommended-events), and suggest you log as many of these as possible (except where a concept doesn't apply to your app).&#x20;
* Custom events: you can log any event you like. Be sure to instument the important events for your app's primary use cases as these are critical for understanding your users and optimizing. For example a reminders app would probably need events like `created_list`, `add_item`, `completed_item`, at minimum.
* [Built in events](/events/built-in-events) (like app\_start, action:action\_name) are tracked automatically, and don't require additional integration in code.

### Watching Events

It's often useful to have Critical Moments log all events during development. To log events only when debugging, add this to your AppDelegate before calling the `start` method of Critical Moments:

{% tabs %}
{% tab title="Swift" %}

```swift
#if DEBUG
CriticalMoments.sharedInstance().setLogEvents(true)
#endif
```

{% endtab %}

{% tab title="Objective C" %}

```objectivec
#ifdef DEBUG
[CriticalMoments.sharedInstance setLogEvents:true];
#endif
```

{% endtab %}
{% endtabs %}

### Sending Events

You can send events from anywhere in your codebase in 1 line:

{% tabs %}
{% tab title="Swift" %}

```swift
// In your imports section add: 
import CriticalMoments

// Send a recommended event
CriticalMoments.sharedInstance().sendEvent("signed_in")

// Send a custom event
CriticalMoments.sharedInstance().sendEvent("created_list")
```

{% endtab %}

{% tab title="Objective C" %}

<pre class="language-objectivec"><code class="lang-objectivec"><strong>// In your imports section add: 
</strong>@import CriticalMoments;

// Send a well known event
[CriticalMoments.sharedInstance sendEvent:@"signed_in"];

// Send a custom event
<strong>[CriticalMoments.sharedInstance sendEvent:@"created_list"];
</strong></code></pre>

{% endtab %}
{% endtabs %}

{% hint style="info" %}
If you already use an analytics framework like Firebase Analytics, consider making a wrapper which distributes your events to both your it and Critical Moments. This will minimize work, and ensure new events are automatically captured in both platforms.&#x20;
{% endhint %}

{% hint style="success" %}
Events are tracked in a local database, not sent to a server. They work offline, and are great for user privacy.
{% endhint %}

### Using Events

Events can be used in several ways:

* [Triggers](/actions-in-app-messaging/triggers) fire actions when an event occur
* [Conditions can query the event history database](/conditional-targeting/conditional-guides/event-and-property-history)&#x20;


# Recommended Events

We suggest you implement as many of these recommended event as make sense for your app's user experience.&#x20;

Over time, we plan on adding built-in optimization options, which take into account user engagement history including these events. By tracking these events from the beginning of your integration, you'll have more data for your own optimizations/conditions, and will be able to take advantage of our new optimizations as they become available.

<table><thead><tr><th width="237">Name</th><th>Description</th></tr></thead><tbody><tr><td>signed_in</td><td>When the user logs into your app with existing credentials.</td></tr><tr><td>log_out</td><td>When the user logs out of your app.</td></tr><tr><td>sign_up</td><td>When the user completes the sign-up flow for a new account.</td></tr><tr><td>in_app_purchase</td><td>When the user completes an in app purchase of a digital good (unlock feature, etc)</td></tr><tr><td>subscribe</td><td>When the user subscribes to a recurring payment  plan / subscription.</td></tr><tr><td>unsubscribe</td><td>When the user cancels a recurring payment  plan / subscription.</td></tr><tr><td>search</td><td>When the user searches in your app.</td></tr><tr><td>tutorial_complete</td><td>When the user completes your app's tutorial or onboarding.</td></tr><tr><td>add_to_cart</td><td>For shopping apps with a cart.</td></tr><tr><td>physical_purchase</td><td>For when the user makes a purchase of a physical item (not in app digitual purchase).</td></tr><tr><td>level_up</td><td>For games, when the user levels up.</td></tr></tbody></table>


# Built-In Events

The following events are automatically fired by Critical Moments, and can be used for triggers or conditions.

[Watching events](/events/event-overview#watching-events) in debug mode can help you vizualize all the built-in events as they fire.

### App Lifecycle Events

* `app_start`: fired immediatly after the SDK starts. There may be a small delay while we fetch the latest config over the network.
* `session_start`: fired when a user session starts. This is both when the app is initially launced, and if the app enters the foreground after 10 mintues or more in the background. &#x20;
* `app_entered_foreground`: fired when the app enters the forground, including the initial launch
* `app_entered_background`: fired when the app enters the background
* `app_terminated`: fired right before the app is terminated (evicted from memory). Note: the OS will not fire this event in some cases including if the user manually closes the app, or if the app was suspended prior to termination.&#x20;

### Feature Flag Events

* `ff_true:[condition_name]`: Fired when a feature flag is evaluated with [checkNamedCondition](/feature-flags/conditional-feature-flags) and returns true. Latter half of the string is the condition name which was evaluated.
* `ff_false:[condition_name]`: Fired when a feature flag is evaluated with [checkNamedCondition](/feature-flags/conditional-feature-flags) and returns true. Latter half of the string is the condition name which was evaluated.
* `ff_error:[condition_name]`: Fired when a feature flag is evaluated with [checkNamedCondition](/feature-flags/conditional-feature-flags) and returns true. Latter half of the string is the condition name which was evaluated.

### Action Events

* `action:[action_name]`: Fired when a named action is performed. For example, if you fired an action named `gdpr_banner`, the event log would have an event for `action:gdpr_banner`
* `action_error:[action_name]`: Fired when we attempt to perform a named action, but there is an error.

#### App Review Action

* `system_app_review_requested`: fired when any [app review action](/actions-in-app-messaging/app-reviews) is fired.&#x20;

#### Banner Actions

* `sub-action:[action_name]:tapped`: fired when a banner is tapped.
* `sub-action:[action_name]:closed`: fired when a banner is closed.

#### Alert Actions

* `sub-action:[action_name]:button:[button_label]`: fired when a button is tapped on an alert. The button\_label is the string label of the button. The "OK" and "Cancel" strings are not localized in events, unlike UI, so the database is consistent across locales.
* `sub-action:[action_name]:button_index:[button_index]`: fired when a button is tapped on an alert. The button\_index is the index of the button tapped. The custom button array starts at index 0, the OK button has a constant value -1, and the Cancel button has a constant value -2.

#### Modal Actions

* `sub-action:[action_name]:closed`: fired when a modal is closed.
* `sub-action:[action_name]:button:[button_label]`: fired when a button is tapped on an modal. The button\_label is the string label of the button.
* `sub-action:[action_name]:button_index:[button_index]`: fired when a button is tapped on an modal. The button\_index is the index of the button tapped. Not fired for the "close" button (see the `closed` event above).

### Notification Events

* `notifications:tapped:[notification_id]`: fired when a notification is tapped. The notification id is the [UNNotificationRequest identifier](https://developer.apple.com/documentation/usernotifications/unnotificationrequest/identifier) of the notification. For notifications created in Critical Moments the ID format is `io.criticalmoments.notifications.[notification_name]`.&#x20;


# Actions Overview

Trigger actions when events occur, or conditions are met

### Action Types

Our SDK comes with a number of powerful actions built in:

* [Announcement banner:](/actions-in-app-messaging/banners) display a banner across the top/bottom of your app, optionally linking to other actions when tapped
* [Modals Popovers / Custom Messaging](/actions-in-app-messaging/modals): show a fullscreen popover with fully native UI. Create beautiful detailed UI with support for images, titles, text, buttons and theming. Buttons can connect to other actions.
* [App Review Prompt](/actions-in-app-messaging/app-reviews): show the system native UI asking a user to rate your app
* [Alerts and Action sheets](/actions-in-app-messaging/alerts): launch native alerts, with buttons connecting to other actions.
* [Open web links](/actions-in-app-messaging/open-link): in an embedded brower displaying a webpage inside your app, or opening in the user's default browser
* [Custom actions](/actions-in-app-messaging/custom-actions): Launch custom actions/deeplinks into other parts of your app&#x20;
* [Open other apps](/actions-in-app-messaging/open-link): Launch other apps, optional deeplinking to specific features
* [Conditional actions](/actions-in-app-messaging/conditional-actions): check a condition, and fire one action if true, and another if false

An alternative to actions are our [feature flags](/feature-flags/conditional-feature-flags), which allow deeper integration into your codebase.

### Themes

Our action UI can be themed to match the design of your app. [See docs](#themes).

### Configuration Example

Actions are created in your [config file](/config-file-structure). A top level `actions` object contains an `namedActions` map/object, with subitems for each action.

Each action requires an `actionType` string and `actionData` object. The contents of each are described in the docs for the each action type.

Actions can optionally include a `condition` string, which will be evaluated before triggering the action, and will stop the action from being performed if it evaluates to false (or errors).

Actions can also specify a `fallback`, a string name of an action to perform if this action can't be performed for any reason. Reasons an action might fail vary based on the action type, but as an example,  attempting to launch an app deep link into an app which isn't installed will fail, and can fallback to another action, like launching an app store link to get that app.

Example:

```
"actions": {
     "namedActions": {
         "landscapeAlert": {
             "actionType": "alert",
             "condition": "interface_orientation == 'landscape'",
             "actionData": {
                 "title": "Launched Landscape",
                 "message": "The app was launched while in landscape orientation."
             }
         },
         "portraitAlert": {
             "actionType": "alert",
             "condition": "interface_orientation == 'portrait'",
             "actionData": {
                 "title": "Launched Portrait",
                 "message": "The app was launched while in portrait orientation."
             }
         }
     }
 }
```

### Triggering Actions

Actions are connected to the rest of the system through the [config file](/config-file-structure), by the name specified in the `namedActions` map. The following parts of the config file can trigger an action:

* [Triggers](/actions-in-app-messaging/triggers) connect actions to events. You can fire an action when an event occurs.
* Nested Actions: actions like alerts, modals and banners can have buttons which can be connected to other actions which are fired when the user taps them. For example, a "more info" button could launch an open link action to your webpage for more info.
* [Conditional actions](/actions-in-app-messaging/conditional-actions): are actions which check a condition, and fire one action by name if true, and another if false


# Modals

Announcements, decisions, and more -- powerful and beautiful messaging with modal UI

Modals are a pop-over UIs that can be used to present your users with information, or several options in the form of buttons. The modal UI covers the entire app.&#x20;

* Powerful configuration-driven UI with support for typography, buttons, images, and symbols &#x20;
* Theme support to make the modal feel native to your app/brand look, including dark mode
* Rich "action" system: buttons can launch other CriticalMoments actions (links, banners) or deep link into native actions in your app

### Examples

<figure><img src="/files/PnJnvOU6J78435Lda0MS" alt=""><figcaption><p>Dark mode with purple accent color, light mode, and a custom theme</p></figcaption></figure>

These examples are all available in our Sample App, including config.

### Config Structure

Modals are configured as [named actions](/actions-in-app-messaging/actions-overview) of the type `modal` .

The `actionData` block has the following options:

* `showCloseButton` \[bool, optional, default true]: if explicitly set to false, will hide the close button of the modal, and disable any system gestures to dismiss the modal. This forces the user to use a button to dismiss. If used, you should have at least 1 button so it's possible to close the modal.
* `themeName` \[string, optional]: a theme name to use for this modal, overriding the default theme. See named themes in [Themes](/themes/theme-overview). Note: typically you should set one "defaultTheme" in the theme config to be shared by all UI, and not need a customThemeName per modal; this option is available if you need one modal to have a different style.
* `content` \[object, required]
  * `pageType` \[string, required]: always the value `stack`. In the future more layouts may be added.
  * `pageData` \[object, required]
    * `sections` \[array, required]: Sections define the content of your modal's view (above the buttons). At least one section is required. See [documentation for the sections available](/actions-in-app-messaging/modals/modal-content-sections).
    * `buttons` \[array, optional]: If specified, these buttons will appear on the bottom of the modal. See [the documentation for the buttons](/actions-in-app-messaging/modals/modal-buttons).

### Sample Config

Sample confguration for the left most example above, including a "More info" button which launches the brower to provide more information. Our sample app has the config for all of the examples above, and more.

```json
"simpleModalAction": {
    "actionType": "modal",
    "actionData": {
        "content": {
            "pageType": "stack",
            "pageData": {
                "sections": [
                    {
                        "pageSectionType": "image",
                        "pageSectionData": {
                            "imageType": "sf_symbol",
                            "height": 60.0,
                            "imageData": {
                                "symbolName": "coloncurrencysign.square.fill",
                                "weight": "light",
                                "mode": "hierarchical"
                            }
                        }
                    },
                    {
                        "pageSectionType": "title",
                        "pageSectionData": {
                            "title": "Important Announcement"
                        }
                    },
                    {
                        "pageSectionType": "title",
                        "topSpacingScale": 1.5,
                        "pageSectionData": {
                            "title": "New pricing coming soon.",
                            "scaleFactor": 0.64
                        }
                    },
                    {
                        "pageSectionType": "body",
                        "topSpacingScale": 4,
                        "pageSectionData": {
                            "bodyText": "Your free plan won't be impacted! However, if you want to upgrade to 'Pro' now is your last chance to lock in our current pricing."
                        }
                    }
                ],
                "buttons": [
                    {
                        "title": "Got it!",
                        "style": "large"
                    },
                    {
                        "title": "More info",
                        "preventDefault": true,
                        "style": "info",
                        "actionName": "web_link_action"
                    }
                ]
            }
        }
    }
}
```


# Modal Content Sections

The content for a modal UI!

The array of `sections` content are rendered in a vertical stack in a modal. You can stack as many content sections as you like.&#x20;

{% hint style="info" %}
No worries if your content is too tall. As it grows, it becomes scrollable, and will scroll under the button section of the modal.
{% endhint %}

### Content Types

* [Title](#title-section-config): Title formatted text
* [Body](#body-section-config): Body formatted text
* [Images](#image-section-config): either symbols from the system (SF Symbols), or image files built into your app bundle.

{% hint style="info" %}
Buttons are **not** valid in the `sections` array in the modal config, and should only be in the `buttons` section.
{% endhint %}

### Visual Breakdown

<figure><img src="/files/7By7fmV4V0PCAS43GteW" alt="" width="375"><figcaption><p>Visual breakdown of how an array 4 of content sections renders</p></figcaption></figure>

### Section Config

The following parameters are valid in any section.

* `topSpacingScale` \[float, optional]: How much space to include above this section, as a multiple of the system default spacing. Defaults to 1.0 if omitted.
* `pageSectionType` \[string, required]: The type of this section. One of `title`, `body` or `image`.
* `pageSectionData` \[object, required]: The data object specific to the type specified in `pageSectionType`. See below for specififications.

### Title Section Config

Title sections are larger bold text sections, centered by default, and in the primaryTextColor. The following parameters are available to customize a title section:

* `title` \[string, required]: The title text
* `scaleFactor` \[float, optional]: A float to scale the text size up or down from the default title text size. Defaults to 1.0 of omitted (no scaling).
* `bold` \[bool, optional]: Specifies if the text bold or regular style. Defaults to true for titles if omitted.
* `centerText` \[bool, optional]: Specifies if the text is centered. Defaults to true for titles if omitted.
* `usePrimaryColor` \[bool, optional]: Specifies if the text uses the primary or secondary text color from the theme. Defaults to true for title sections if omitted.
* `width` \[float, optional]: Specifies the max width of the text element in points. Text won't exceed margins, regardless of value. Optional and will choose reasonable values if omitted.

### Body Section Config

Body sections are smaller non-bold text sections, centered by default, and in the secondaryTextColor The following parameters are available to customize a body section:

* `bodyText` \[string, required]: The body text
* `scaleFactor` \[float, optional]: A float to scale the text size up or down from the default title text size. Defaults to 1.0 of omitted (no scaling).
* `bold` \[bool, optional]: Specifies if the text bold or regular style. Defaults to false for body sections if omitted.
* `centerText` \[bool, optional]: Specifies if the text is centered. Defaults to true for body sections if omitted.
* `usePrimaryColor` \[bool, optional]: Specifies if the text uses the primary or secondary text color from the theme. Defaults to false for body sections if omitted.
* `width` \[float, optional]: Specifies the max width of the text element in points. Text won't exceed margins, regardless of value. Optional and will choose reasonable values if omitted.

### Image Section Config

Images have their [own documentation page](/actions-in-app-messaging/modals/modal-images).

### Example Content Config

See the [sample config](/actions-in-app-messaging/modals#sample-config) for an example of all content types in an array, forming a content stack.


# Modal Buttons

Buttons and actions for modals

Modals have an optional stack of buttons on the bottom of the page. They are sticky, and the content (sections) will scroll under the top button if too tall.&#x20;

Buttons are not required.

### Button Actions

By default all buttons dismiss the modal, and only dismiss the modal.&#x20;

To add another action, you can specify an `actionName` which can fire any other CriticalMoments action, including a deep link to trigger native code in your app.&#x20;

To prevent a button from dismissing the modal when tapped set the `preventDefault` option on the config.

{% hint style="warning" %}
If no buttons are included, or all have `preventDefault` set, be careful not to set `showCloseButton=false` on the modal or the user won't be able to dismiss the modal!
{% endhint %}

### Button Parameters

* `title` \[string, required]: The title of the button
* `style` \[string, optional]: One of styles described [below](#button-styles). If omitted, defaults to `normal`.
* `actionName` \[string, optional]: If specified, the action by this name will be triggered when this button is tapped. Can be used to make the&#x20;
* `preventDefault` \[bool, optional, default false]: prevents the button's default action. In the case of modals, this prevents the modal from being dismissed. ActionName is not impacted by this parameter.

### Button Styles

The following styles are available. The color is derived from the `primaryColor` of the theme for this content (in this case, systemPurple).

Valid values are: "large", "normal", "secondary", "tertiary", "info", "info-small".

![](/files/Jicz5R32KHizg9Z9pema)

### Sample Config

Here is a sample config for a style=secondary button which launches a web link action, and does not dismiss the modal when tapped.

```json
{
    "title": "More info",
    "preventDefault": true,
    "style": "secondary",
    "actionName": "web_link_action"
}
```

### Automatic Button Events

Button taps are tracked as events by default. See [Modal Built-In Events](/events/built-in-events#modal-actions) for details.


# Modal Images

How to include images in your modal UIs

### Shared Properties

* `height` \[float, optional]: the height of the image in system "points" (where  a standard phone is about 390 points wide). Defaults to 40.0 if not specified.
* `imageType`: \[string, required]: one of "local" or "sf\_symbol".&#x20;
* `imageData`: \[object, required]: properties that vary depending on the type. See "Image Type" below.
* `fallback`: \[object, optional]: a fallback image if the image described in this object can not be rendered on this system. Same format as root image object. Example of situations where we can't render: SF-Symbol objects can't be rendered on iOS 12, your symbol wasn't included in earlier versions of iOS, or local image path is not found. It's common to nest several fallbacks; we'll continue to search until one we find one that works.&#x20;

If the image (and fallbacks) can not be rendered, the image will not be shown.

### Image Type: Local Image

Local images are images that are included in your app bundle.&#x20;

* `path`: \[string, required]: the path to the image in the asset bundle. Typically this is the name of the image (eg: "logo.png"). The image must be an asset included in your app's bundle, or it won't be found.

<details>

<summary>Example Local Image Config</summary>

```json
{
    "imageType": "local",
    "height": 60.0,
    "imageData": {
        "path": "logo.png"
    },
}
```

</details>

### Image Type: SF Symbols

SF Symbols are only avilable on iOS 13+.

* `symbolName`: \[string, required]: the name of the symbol. Example "coloncurrencysign.square.fill". Download the [SF Symbols](https://developer.apple.com/sf-symbols/) app to explore options. If the symbol you choose isn't available on the earliest version of iOS your app supports, be sure to include a fallback image (another symbol available on earlier iOS, or a local image)
* `weight`: \[string, optional]: the visual weight of the symbol. One of the following (ascending weight): "ultralight", "thin", "light", "regular", "medium", "semibold", "bold", "heavy",  "black". Defaults to "regular".
* `mode`: \[string, optional]. Which render mode to use. Options include
  * `mono`: monochromatic
  * `hierarchical`: one color shade, but various opacities/strengths of the color to give it a feeling of depth
  * `palette`: a color palette of 2 colors
* `primaryColor`: \[string, optional]: a string in hex color format (`#ff0000`), specifying the primary color of the symbol. If omitted, will use your default tint color from the modal's theme, your default theme, or the system theme.
* `secondaryColor`: \[string, optional]: a string in hex color format (`#ff0000`), which will be used as the second color in `palette` mode. No effect in other modes.

<details>

<summary>Example SF Symbol Config</summary>

```json
{
    "imageType": "sf_symbol",
    "height": 80.0,
    "imageData": {
        "symbolName": "cloud.moon.bolt",
        "weight": "light",
        "mode": "palette",
        "primaryColor": "#3366ff",
       "secondaryColor": "#ffcc00"
    }
}
```

</details>

### More Examples

See the action `imageStylesModal` , in "config.json", in the sample app for more examples.


# Banners

Announcements that appear across every screen in your app

Banners are a powerful way to push critical information to your users.

Tapping a banner can trigger other actions, like showing a modal with more information, opening a web-link to an announcement, or showing an alert.

{% hint style="warning" %}
Banners are only supported in iOS 13 or newer. If your app supports iOS 12, a banner action won't have any effect. You can use a [Conditional Action](/actions-in-app-messaging/conditional-actions) checking OS version to show an alternative message format on iOS 12 ([Alert](/actions-in-app-messaging/alerts) or [Modal](/actions-in-app-messaging/modals)) if needed.
{% endhint %}

### Examples

<figure><img src="/files/09seZy5kaQLBLNo8TVFc" alt="" width="563"><figcaption><p>A bottom banner, top banner with cycle-message button, and a themed bottom banner</p></figcaption></figure>

### Banner Action Config

Banners have actionType `banner` and an actionData object with the following keys:

* `body` \[string, required]: the string to show in the banner
* `showDismissButton` \[bool, optional]: if true, a close button ("X") is shown, allowing the user to dismiss this banner. If false, the banner can not be dismissed. Defaults to true if omitted.
* `maxLineCount` \[int, optional]: if set, limits the number of lines. If omitted, defaults to 4 lines. If the max line count is reached, the string will be truncated with an ellipsis ("..."). If set to 0, the line count isn't limited (although the banner is limited to 20% of screen height).
* `tapActionName` \[string, optional]: the name of an action to trigger if the user taps the banner message. No action is fired if omitted.&#x20;
* `themeName` \[string, optional]: if provided this banner will use the specified theme (overriding the default theme). See themeing information below.
* `preferredPosition` \[string, optional]: one of "top" or "bottom". For a given app, it's best to not mix positions. If both are used the banner area will use the position of the last banner presented.

### Design Considerations

Banners show a message across every screen of your app, and should be used sparingly for important announcements.

Only convey the most critical information in the banner text itself, keeping it as short as possible. Provide more detailed information if they tap the banner with a modal, alert or web-link in embedded browser launched via `tapActionName`. If you have a tap action, consider ending your body string with "Tap for details.", or a similar hint.

Banners should almost always be user dismissable, allowing users to reclaim the screen space. Exceptions to this rule should be exceedingly rare (example: company is shutting down and users need to export data).

If you present multuple banners at the same time, the user will see a "<>" button on the left of the banner to cycle through the messages. You can try this in our Sample App.

{% hint style="danger" %}
Be sure to test banners in your app before deploying them. Our banner area is injected into the main window of your app, and shifts your app's content using [additionalSafeAreaInsets](https://developer.apple.com/documentation/uikit/uiviewcontroller/2902284-additionalsafeareainsets) on the root view controller. This typically works well, and Apple controllers like UITabBarController or UINavigationController handle this automatically. If you have your own root view controller which has hardcoded insets or ignores layout guides, the banners may cover important parts of your app UI. To fix issues, make sure your root view controller respects [safe insets / layout guides](https://developer.apple.com/design/human-interface-guidelines/layout#).
{% endhint %}

### Theming Banners

Banners respect your default theme, or you can a theme for a specific banner with the `themeName` property.

There are 2 specific properties in themes for banners:

* `bannerBackgroundColor`: the banner background color
* `bannerForegroundColor`: the foreground color for text and buttons

By default, banners are yellow with black text, in the system font.

Banners will respect shared theme properties, like the fontName. See the [full themes documentation](/themes/theme-overview) for details.

### Example Config

The config below from the Sample App, and generated the banners in the screenshots above.&#x20;

```
"short_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "A short banner!",
        "tapActionName": "nested_alert"
    }
},
"long_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "Welcome to critical moments! App wide banners can give your users crucial information.",
        "tapActionName": "nested_alert"
    }
},
"very_long_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "Welcome to critical moments! App wide banners can give your users crucial information. This one happens to be really really long, and will probably be truncated eventually. It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of light, it was the season of darkness, it was the spring of hope, it was the winter of despair",
        "tapActionName": "nested_alert"
    }
},
"custom_theme_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "This banner has a custom theme+action set in config. Tap me!",
        "themeName": "blackAndWhiteTheme",
        "tapActionName": "nested_alert"
    }
},
"top_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "This banner will appear at the top! Any banner can specify top or bottom position.",
        "preferredPosition": "top",
        "tapActionName": "nested_alert"
    }
},
"undismissable_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "You are stuck with me... unless you use the \"Clear all banners\" action",
        "showDismissButton": false,
        "tapActionName": "nested_alert"
    }
},
"single_line_banner": {
    "actionType": "banner",
    "actionData": {
        "body": "This message will truncate after the first line, unlike the default of 4 lines.",
        "maxLineCount": 1,
        "tapActionName": "nested_alert"
    }
}
```


# Alerts

Provide information or give user's a choice

### Example Alerts

<figure><img src="/files/y9QaylRx6Z2PhuT7Xd7W" alt=""><figcaption><p>One button, two button, red button, blue button (Dialog, two button dialog, "large" style action sheet, and a many-option dialog)</p></figcaption></figure>

### How alerts work

Tapping any button dismisses the alert. If an action name is associated with the tapped button, Critical Moments triggers that action.

### Alert Action Config

An alert has actionType `alert` and an actionData object with the following keys:

* `title` \[string, optional]: The title of the alert. May be empty, but you must have a title and/or a message, both can't be omitted.
* `style` \[sting, optional]: one of "dialog", or "large". Defaults to dialog if omitted. See above for visual examples of each style.
* `message` \[string, optional]: The message body of the alert. May be empty, but you must have a title and/or a message, both can't be omitted.
* `showOkButton` \[bool, optional]: If true, show an "OK" button. Defaults to true if omitted. Defaults to no action, but an action can be set with `okButtonActionName`
  * You should use this property over a custom button with an "OK" string because it follows system conventions for OK buttons: placement in button order, and bold treatment.&#x20;
  * It will localize using the system standard "OK" string.
* `okButtonActionName` \[string, optiona]: the name of an action, which is fired when the user taps the okay button. No action is fired if this is omitted.
* `showCancelButton` \[bool, optional]: If true, show an "Cancel" button which dismisses the alert. Defaults to false if omitted.
  * You should use this property over a custom button with an "Cancel" string because it follows system conventions for Cancel buttons regarding placement in the alert.&#x20;
  * It will localize using the system standard "Cancel" string.
* `customButtons` \[array, optional]: an array of custom button objects with the following keys:
  * `label` \[string, required]: the label of the button
  * `actionName` \[string, optional]: the action to fire when the button is tapped
  * `style` \[string, optional]: the style of the alert button. One of "default", "destructive" (red) or "primary" (bold). Only one button can be primary on iOS - the last button you specify as primary (including OK) will get the primary treatment. See above for visual examples. Defaults to "default" if omitted.

### Design Considerations

Alerts have more design considerations than you'd expect! Critical Moments is designed to make it easy to comply with the [Apple Human Interface guide for alerts](https://developer.apple.com/design/human-interface-guidelines/alerts).

* A Title and/or body is required. Neither is not allowed. Both are suggested.
* You must have at least 1 button: OK, or a custom button. No buttons or a single cancel button are not valid
* The Apple suggested position and font-weight of the OK and Cancel buttons changes based on platform (phone vs iPad), layout (dialog vs large) and which other buttons are present. Using the options showOkayButton/showCancelButton will ensure these buttons follow platform standards, and is highly recommended over custom buttons with the label "OK"/"Cancel".
* There is no theme support for alerts, they use the system native alert look

### Example Config

The config below from the Sample App generated the alerts in the screenshots above.

```
"show_notice_alert": {
    "actionType": "alert",
    "actionData": {
        "title": "A simple notice alert",
        "message": "Title, message, okay button. You get the idea!"
    }
},
"show_cancelable_alert": {
    "actionType": "alert",
    "actionData": {
        "title": "A cancelable alert",
        "message": "An alert with a cancel button... not exactly rocket science.\n\nTry both buttons to see what they do.",
        "showCancelButton": true,
        "okButtonActionName": "nested_alert"
    }
},
"custom_button_alert_large": {
    "actionType": "alert",
    "actionData": {
        "title": "Action sheet deep dive",
        "message": "This is an action sheet with several options.\n\nEach can launch a custom action!",
        "showOkButton": false,
        "showCancelButton": true,
        "style": "large",
        "customButtons": [
            {
                "label": "Option 1",
                "actionName": "nested_alert"
            },
            {
                "label": "Option 2",
                "actionName": "nested_alert"
            },
            {
                "label": "Option 3",
                "actionName": "nested_alert"
            },
            {
                "label": "Option 4",
                "actionName": "nested_alert"
            },
            {
                "label": "Scary option",
                "actionName": "nested_alert",
                "style": "destructive"
            }
        ]
    }
},
"custom_button_alert_dialog": {
    "actionType": "alert",
    "actionData": {
        "title": "Lots of options",
        "message": "This is an alert with several buttons of different styles.",
        "showOkButton": false,
        "showCancelButton": true,
        "customButtons": [
            {
                "label": "Normal button",
                "actionName": "nested_alert"
            },
            {
                "label": "Primary button",
                "actionName": "nested_alert",
                "style": "primary"
            },
            {
                "label": "Scary button",
                "actionName": "nested_alert",
                "style": "destructive"
            }
        ]
    }
},
```


# App Reviews

Request App Store reviews from your users

The app-review action launches the system control for asking your users to review your app.

<figure><img src="/files/XPNLZ2QLSNJhguus0PeN" alt="" width="250"><figcaption><p>Apple's SKStoreReviewController, launched from a CM App Review Action</p></figcaption></figure>

{% hint style="success" %}
Our [Developer Guide for Improving App Reviews](/guides/improve-your-app-store-rating) has everything you need to quickly get up and running with an optimal app-review prompt.&#x20;

It details optimizations that will improve your averate app store rating including:

* Showing the review prompt at the optimial moment
* Avoiding negative conditions, such as: low battery, no internet connection, user distractions (e.g., being on a call or driving), and more
* Excluding users who are running buggy releases of your app, or are using an interface languages not supported by your app's UI
* Avoiding users who have blocked permissions crucial to your app
* And many more!
  {% endhint %}

### App Review Config

An app review action has actionType of `review_prompt` and an empty actionData.

### Example Configs

#### Simple Example

```
"reviewAction": {
    "actionType": "review_prompt",
    "actionData": {}
}
```

#### Smart Conditional Prompt

This example uses a condition to ask users to rate you at the right moment.&#x20;

It will only ask users who haven't been asked in the last 21 days (504 hours), if they are using a recent app version, their device isn't in a bad state (low battery, no network, etc), and they are running a somewhat recent version of iOS. See our [blog post](https://criticalmoments.io/blog/improve_app_ratings) for more ideas on how to improve when you ask for app reviews.

```
"smartReviewAction": {
    "actionType": "review_prompt",
    "actionData": {},
    "condition": "(
                     eventCount('system_app_review_requested') == 0 || 
                     latestEventTime('system_app_review_requested') < now() - duration('504h')
                  ) 
                 && versionGreaterThan(app_version, '2.2') 
                 && !device_low_power_mode 
                 && has_active_network 
                 && !low_data_mode 
                 && device_battery_level > 0.2 
                 && !versionLessThan(os_version, '17.0')'"
}
```

{% hint style="info" %}
Be aware that iOS will only display the prompt to a user a maximum of three times within a 365-day period. See [Apple's documentation](https://developer.apple.com/documentation/storekit/requesting_app_store_reviews?language=objc) on app reviews for limits and best practices.&#x20;
{% endhint %}


# Open Link

Launch web links or app deep links

`link` actions open a variety of links. This includes:&#x20;

* Web links (http/https) in Safari, or an in-app browser
* Deep links into [parts of you app, or triggering native code](/actions-in-app-messaging/custom-actions)
* System links, like linking to settings with `app-settings:`&#x20;
* Deep linking into other apps, like `spotify:album:4oktVvRuO1In9B7Hz0xm0a`&#x20;

<figure><img src="/files/kCILpelH6Vh94MiqtDMb" alt="" width="160"><figcaption><p>Opening a link in the in-app browser and Safari</p></figcaption></figure>

### Link Action Config

Links have the actionType `link` and an actionData object with the following keys:

* `url` \[string, required]: the URL to open
* `useEmbeddedBrowser` \[bool, optional]: If true, will open the link in an in-app browser instead of launching to Safari. Only valid for http/https links.

### Config Examples

```
"web_link_action": {
    "actionType": "link",
    "actionData": {
        "url": "https://criticalmoments.io"
    }
},
"web_link_embedded_action": {
    "actionType": "link",
    "actionData": {
        "url": "https://criticalmoments.io",
        "useEmbeddedBrowser": true
    }
},
"settings_link_action": {
    "actionType": "link",
    "actionData": {
        "url": "app-settings:"
    }
},
"main_screen_deeplink_action": {
    "actionType": "link",
    "actionData": {
        "url": "critical-moments-sampleapp:main"
    }
}
```


# Custom Actions

Trigger native code from Critical Moments

To create an action which calls into native code, use a `link` action type with a deeplink into your app.&#x20;

An example link to your "edit profile experience could be: `you-app-scheme:profile/photo?mode=edit`. It has the the following components:

* App scheme `you-app-scheme:`: the app scheme you[ setup and define for your app](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app)
* The path `profile/photo`: a specific screen or action in your app you are linking to
* A query string `?mode=edit`: Any serialized data you want to pass through and handle in your code.

To implement you can use [iOS deeplinks](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app), or [universal links](https://developer.apple.com/ios/universal-links/).

To not set `useEmbeddedBrowser` for deeplinks. This option is only supported for http/https links.

### Example Custom Action Config

```json
"edit_profile_photo_deeplink_action": {
    "actionType": "link",
    "actionData": {
        "url": "you-app-scheme:profile/photo?mode=edit"
    }
},
```


# Conditional Actions

If this, then that

Conditional actions are a meta action type which check a [condition](/conditional-targeting/intro-to-conditions) and dispatch one action if it's true, and another if it's false.

{% hint style="info" %}
Example use case: If the app\_version >= 3.0, show a modal message about new v3 features. If not, show alert suggesting they upgrade, including a button which links to the app store.
{% endhint %}

### Configuration

The following fields are available in the action configuration:

* `condition` \[string, required]: the [condition](/conditional-targeting/intro-to-conditions) to check. Example `(device_battery_state == 'charging' || device_battery_state == 'full')`.
* `passedActionName` \[string, required]: the name of the action to fire if the condition is true.
* `failedActionName` \[string, optional]: the name of the action to fire if the condition is false.

{% hint style="info" %}
If you don't have an "else"/`failedActionName` action and just want to check a condition before firing an action, you don't need to wrap your action in a conditional action. Simply specify a [`condition` string on the action](/actions-in-app-messaging/actions-overview) and the action will only proceed if the condition passes.
{% endhint %}

### Example Config

```
{
    "actionType": "conditional_action",
    "actionData": {
        "condition": "(device_battery_state == 'charging' || device_battery_state == 'full')",
        "passedActionName": "true_action",
        "failedActionName": "false_action"
    }
}
```


# Triggers

Connect events to actions

Simply defining a action in your config won't actually do anything. You need to trigger it somehow. The most common way is using a trigger, which connects events to actions.

### Trigger Config

Triggers are defined in your [config file](/config-file-structure). A top level `triggers` object contains an `namedTriggers` map/object, with subitems for each trigger.

Each trigger object contains:

* `eventName` \[string, required]: the event name which triggers this trigger
* `actionName` \[string, required]: the name of the action to perform when that event occurs
* `condition` \[string, optional]: a [condition string](/conditional-targeting/intro-to-conditions) to evaluate when the event occurs. The action will only be fired if it evaluates to true.

### Example Config

```
"triggers": {
    "namedTriggers": {
        "suggestWatchAppTrigger": {
            "eventName": "app_start",
            "actionName": "showSuggestWatchAppDialog",
            "condition": "hasWatch && eventCount('shown_suggest_watch_dialog') == 0"
        },
        "congratsMessageTrigger": {
            "eventName": "completed_list_item",
            "actionName": "showCongratsModal"
        }
    }
}
```


# Theme Overview

Align Critical Moments UI to your brand and app's style

Themes control the style, colors and font of the Critical Moments UI. By specifying themes, you can align our UI to match the rest of your app, and fit in seamlessly.

<figure><img src="/files/8e0E7UAIbDMjc1CTQ0kn" alt=""><figcaption><p>Three modals using different themes</p></figcaption></figure>

### Themes Configuration Example

Themes are defined in your [config file](/config-file-structure).&#x20;

A top level `themes` object contains a `defaultThemeName` string and a `namedThemes` map/object, with subitems.&#x20;

* defaultThemeName \[optional] defines a default theme. If can be one of the `namedThemes` names, or one of the [built in](/themes/built-in-themes) theme names:&#x20;
* namedThemes \[optional] define custom themes which can be referenced by name from invividual actions, used as a fallback theme, or set as the defaultThemeName. See [Custom Theme Config](broken://pages/z77rxriPmSxZ44baCbE6) for details on how to create your own theme.

The Critical Moments [Demo App](/demo-app) can be used to experiment with themes.

Here's an example theme config specifying one custom theme, and setting it as the default via `defaultThemeName`:

```json
"themes": {
    "defaultThemeName": "blackAndWhiteTheme",
    "namedThemes": {
        "blackAndWhiteTheme": {
            "bannerBackgroundColor": "#000000",
            "bannerForegroundColor": "#ffffff",
            "fontName": "AvenirNext-Regular",
            "boldFontName": "AvenirNext-Bold",
            "backgroundColor": "#ffffff",
            "primaryTextColor": "#000000",
            "primaryColor": "#da1212",
            "secondaryTextColor": "#424242",
            "darkModeTheme": {
                "bannerBackgroundColor": "#ffffff",
                "bannerForegroundColor": "#000000",
                "fontName": "AvenirNext-Regular",
                "boldFontName": "AvenirNext-Bold",
                "backgroundColor": "#000000",
                "primaryTextColor": "#ffffff",
                "primaryColor": "#da1212",
                "secondaryTextColor": "#a1a1a1"
            }
        }
    }
}
```

### Custom Theme Spec

A theme is specified through a set of variables:

<table data-full-width="true"><thead><tr><th width="272">Variables</th><th width="91">Type</th><th width="135">Example</th><th>Description</th></tr></thead><tbody><tr><td>backgroundColor</td><td>string</td><td>"#ffffff"</td><td><p>The color used for the background of  views. Should be a string of the hex color prefixed with <code>#</code>.</p><p></p><p>Defaults to the system standard color (respecting dark mode).</p></td></tr><tr><td>primaryColor</td><td>string</td><td>"#ff0000"</td><td><p>The primary brand color. This is used for buttons, icons, links and more. Should be legible if used on top of backgroundColor. If you aren't setting a manual backgroundColor, check it is legible on the system's light and dark mode background.</p><p></p><p>If nil, will use system standard behaviour. On iOS will inherit tint from at runtime from the app wide AccentColor (defaults to "system blue").</p></td></tr><tr><td>primaryTextColor</td><td>string</td><td>"#000000"</td><td><p>The color used for primary content labels.</p><p></p><p>Defaults to the system standard color (respecting dark mode).</p></td></tr><tr><td>secondaryTextColor</td><td>string</td><td>"#111111"</td><td><p>The color used for secondary content labels.</p><p></p><p>Defaults to the system standard color (respecting dark mode).</p></td></tr><tr><td>bannerBackgroundColor</td><td>string</td><td>"#ffffff"</td><td><p>The background color of banner messages. Should be a string of the hex color prefixed with <code>#</code>.</p><p></p><p>Defaults to a yellow color suitable for banners.</p></td></tr><tr><td>bannerForegroundColor</td><td>string</td><td>"#ffffff"</td><td><p>The foreground color of banner messages. Should be a string of the hex color prefixed with <code>#</code>.</p><p></p><p>Defaults to a black.</p></td></tr><tr><td>fontScale</td><td>float</td><td>1.1</td><td><p>Scales the font size for all CM UI controls. Defaults to 1.0. Useful if your app's style uses font sizes consistently smaller or larger than the system default sizes.</p><p></p><p>Defaults to 1.0</p></td></tr><tr><td>fontName</td><td>string</td><td>"AvenirNext-Regular"</td><td><p>The default font to use. If nil, this theme will use the system font. </p><p></p><p>Check out <a href="https://iosfontlist.com/">https://iosfontlist.com</a> for options, or use your own app-installed font.</p></td></tr><tr><td>boldFontName</td><td>string</td><td>"AvenirNext-Bold"</td><td><p>The default font to use for bold text. If nil, this theme will use the system font. </p><p></p><p>Check out <a href="https://iosfontlist.com/">https://iosfontlist.com</a> for options, or use your own app-installed font.</p></td></tr><tr><td>scaleFontForUserPreference</td><td>bool</td><td>true</td><td>If true, will scale the font size based on the user settings in "Settings" app or control center. Defaults to true. Ideally leave this enabled for accessibility.</td></tr><tr><td>darkModeTheme</td><td>json object</td><td><a href="#example-theme-configuration">see below</a></td><td><p>If specified, a theme to use if the user has enabled "dark mode" for UI in settings. This object has the same structure as the root theme object.</p><p></p><p>If not set, we will use the root theme for both light and dark mode.</p></td></tr><tr><td>fallback</td><td>string</td><td>"oldTheme"</td><td><p>A theme name to fallback if this build does not support this theme. </p><p></p><p>For future use, if the theme spec grows and you continue to use the same config file for older builds using prior SDK version.</p></td></tr></tbody></table>


# Built In Themes

Choose from pre-built themes

Critical Moments comes with 21 built in themes, made up of 7 named-styles, each with a light, dark and auto variant. You can refer to these by name in your config file. The base/auto varient will automatically adapt to the user system dark-mode setting, while the `_light` and `_dark` varients will force one color pallet regardless of the user setting.

* system/system\_light/system\_dark \[default]: system standard color pallet and font. If your app has an [accent color](https://developer.apple.com/documentation/xcode/specifying-your-apps-color-scheme) set, that will be the primary color (defaults to systemBlue).
* elegant/elegant\_light/elegant\_dark: a black and white theme, using a modern font (Avenir Next on iOS).
* aqua/aqua\_light/aqua\_dark: A blue/green theme, using default system font.
* jazzy/jazzy\_light/jazzy\_dark: A purple/pink theme, using default system font.
* sea/sea\_light/sea\_dark: A deep blue theme, using default system font.
* honey/honey\_light/honey\_dark: A yellow/black theme, using default system font.
* terminal/terminal\_light/terminal\_dark: A theme mirroing the look of a computer terminal, using a monospace font.

<figure><img src="/files/NIoJdwDmekdSdCKSZV3A" alt=""><figcaption><p>Built in themes</p></figcaption></figure>


# Conditional Feature Flags

How to use condition strings in code to enable or disable features

{% hint style="info" %}
Be sure to read our [Feature Flag Guide](/guides/feature-flags-guide), which covers common Feature Flag implementations.
{% endhint %}

[Feature flags](https://www.martinfowler.com/articles/feature-toggles.html) are great way to enable new features, stage rollouts, and disable features that are end of life.&#x20;

However, static flags and randomized rollout often aren't powerful enough. What happens if you find out your new feature has a bug on iPad, issues on cellular networks, or bugs in some locales? What if you want to target only long standing users initially to prevent data from new users clouding the results of AB test?

You can't think of every condition you might need ahead of time, but with our conditional checks you can used advanced targeting, and update your targeting after you ship.

### checkNamedCondition API

This API is simple, but powerful. It evaluates a condition string, and returns if it's true or false.&#x20;

The condition strings are defined in your [config file](/feature-flags/named-conditions-config), allowing you to update them remotely without app updates.

{% hint style="info" %}
Be sure to provide a unique name to each use case. Don't reuse names even if the current conditional logic is equivalent; that will make it impossible to override each usage independently from your remote config file.
{% endhint %}

{% tabs %}
{% tab title="Swift API" %}

```swift
// Optionally try/catch the for errors
let result = try? await CriticalMoments.shared().checkNamedCondition("userNotDistracted")
if result {
   // Perform action
}
```

{% endtab %}

{% tab title="Objective C API" %}

```objectivec
[CriticalMoments.sharedInstance 
        checkNamedCondition:@"userNotDistracted" 
        handler:^(_Bool result, NSError * _Nullable error) {
    if (error != nil) {
        NSLog(@"Error: %@", error);
    } else if (result) {
        // Perform action
    }
}];
```

{% endtab %}
{% endtabs %}

#### Parameters and Return Value

* `name`: A name for this condition. Must be provided and can not be an empty string. This name is used to look up the conditional string from your [config file's named conditions](/feature-flags/named-conditions-config).
* `error`: Any errors returned from evaluating the condition.

#### Return Value

Returns the result of evaluating the condition. Always false for any error.

### Remote updates

You can update your conditions anytime from the cloud. The config file format to do so is described [here](/feature-flags/named-conditions-config).&#x20;

### Advanced Usage

See our [feature flag guide](/guides/feature-flags-guide) for tips on how to implement staged rollouts, rollback, and other feature flag strategies.


# Named Conditions Config

Update conditions from the cloud

If you use feature flags, you'll need to define the conditional strings in your config file.&#x20;

Defining these in remote configuration instead of hardcoded allows you to update them over time (for rolling out, rolling back, or target refinement), or to remotely disable bugs (disable a feature, but only targeting impacted users).

For how to remotely update your config, see [here](/remote-control-service).&#x20;

### Example Config

Here's an example config file showing two named conditions:

```
{
    "configVersion": "v1",
    "appId": "YOUR_BUNDLE_ID",
    "conditions": {
      "namedConditions": {
        "isNewPhone": "user_interface_idiom == 'phone' && versionNumberComponent(device_model_version, 0) >= 14",
        "hasWatchAndIos15Plus": "has_watch && versionNumberComponent(os_version, 0) >= 15"
      }
    }
}
```


# Trustless SaaS

You can trust us, but you shouldn't have to

Critical Moments is built as what we call "Trustless SaaS": software designed to be remotely controlled from services, without needing to trust a 3rd party service provider for correctness, security, or uptime SLAs.

Read our blog for a [complete overview of Trustless SaaS](https://criticalmoments.io/blog/trustless_saas).

### Trustless SaaS Model

These three policies together form our Trustless SaaS framework:

#### **Source-Available**

Our SDK's source code is [available on Github](https://github.com/CriticalMoments/CriticalMoments). You can read and compile any of the code you integrate into your app.

#### **Self Hosting**

[Self hosting the SDK configuration](/remote-control-service) means we can't change your app's behaviour, even if we wanted to.&#x20;

It also means you can use the access control systems already embedded into your company. Since you don't rely on our servers, you can't be impacted by outages in our services.

#### **No User Tracking**

In a default install, Critical Moments install does not call out to any servers owned by us. It only calls to the config-service you self host. We can't track your users.

Some optional features require an external service to function. Currently, service requests are necessary for Geo-IP location, and weather data. Our docs clearly indicate when a service call is necessary, and explain how we protect end user data.

## How it works

We are a business, and still need to check that you've registered your app, and only use the features in your subscription level. Instead of having every client call out to a centralized service like many other SaaS companies do, our systems use public key cryptography to verify these goals.&#x20;

The client can verify these signatures locally, quickly, offline, and without calling any service.

Read more details [on our blog](https://criticalmoments.io/blog/trustless_saas/#how_we_implement).


# Support

We're here to help

Can't find an answer in [our docs](/readme)? Need help? Find a bug? Feel free to reach out!

* Contact us: <support@criticalmoments.io>&#x20;
* Bug tracker: [GitHub Issues](https://github.com/CriticalMoments/CriticalMoments/issues)


# SDK API Reference

The [developer guide](/quick-start) offers the most detailed instructions on integrating Critical Moments, and we suggest following it.

If needed, iOS SDK Reference can be found [here](https://criticalmoments.github.io/CriticalMoments/index.html).


