Unity In-App messages

Starting from Unity SDK 3.1.0+, we have introduced additional support allowing you to display In-App Messages in Unity Native and the Unity Editor. This support improves the experience of integrating the SDK by easing the friction of In-App Message testing by having to run a build each time. This will help you decrease your development and testing time with Leanplum.

👍

In-App Message Support Note

All versions of the Unity SDK supports In-App Messages on iOS and Android devices.
The new release of Leanplum Unity SDK 3.1.0+ adds additional functionality and support to display messages in Unity Editor/Native.

Unity Editor/Native In-App Messages

We have introduced support that includes all the following:

✓ In-App message Preview from Dashboard (only available in Dev mode)
✓ Trigger (Display When) support on the Leanplum Start, Event, and State calls
✓ Confirm, Open URL support for Unity Editor, Generic Definition for other messages/actions
✓ In-App Message Priority support
✓ Show message by ID

🚧

In-App Message Support to Come

Triggering In-App messages based on event/state parameters and on User attribute changes is not supported in Unity Editor/Native.

Supported Methods

The below methods are introduced to allow you to display In-App messages in your Unity Native/Editor

/*  Enables/Disables performing actions on triggers -
     whether In-app Messages and Actions should be triggered and executed.
If Enabled, will show in-app messages or actions in Unity Native/Editor.
 Disabled by default.
<param name="value"> Perform Actions value. </param> */

LeanplumNative.ShouldPerformActions(true);


/* Sets whether the API should return default ("defaults in code") values
or only the overridden ones.
Used only in Development mode. Always false in production.
<param name="includeDefaults"> The value for includeDefaults param. </param> */

Leanplum.SetIncludeDefaultsInDevelopmentMode(true);


/* Defines Confirm action definition, works in Editor only.
 Presents EditorDialog with the Confirm message values.*/

EditorMessageTemplates.DefineConfirm();


/* Defines Open URL  action definition
 Opens the URL defined, uses Application.OpenURL */

EditorMessageTemplates.DefineOpenURL();


/* Defines Generic Action Definition, works in Editor only.
Presents EditorDialog with the message values, 
including message/action name, message id, and the message fields */

EditorMessageTemplates.DefineGenericDefinition();

/*  Manually Trigger an In-App Message. Supported in Unity only.
 The user must be eligible for the message and 
 the message must be present on the device (requires a Start call).
 <param name="id"> The message Id. </param> */
public static bool ShowMessage(string id)

// Example
Leanplum.ShowMessage("4940934800277123"); // messageId here is from the Dashboard URL

Example Messages

Enabled the performing of actions and register the default action definitions provided by Leanplum, inside the LeanplumWrapper.cs, before Leanplum Start:

#if UNITY_EDITOR
        EditorMessageTemplates.DefineConfirm();
        EditorMessageTemplates.DefineOpenURL();
        EditorMessageTemplates.DefineGenericDefinition();
        LeanplumNative.ShouldPerformActions(true);
        Leanplum.SetIncludeDefaultsInDevelopmentMode(true);
#endif

Confirm Message on Event Trigger

Trigger: Leanplum.Track("confirm3")

Dashboard Message:

Unity Editor:

Clicking the Accept button, Opens the URL in the default browser on your machine.

Rich Interstitial Message on Start

Trigger: Leanplum.Start()

Dashboard Message:

The Rich Interstitial uses the Generic Action Definition:

Chained Message

Chained Messages in a Campaign.

Unity Editor:

Clicking the Accept button, Opens the Chained Alert - the Alert uses the Generic Action Definition.

Define Custom Message Template

Starting from Unity SDK 3.1.0, you can define Custom Message Template entirely from Unity. The Action Context, Action Args, and Action responder are now available in Unity.

You can define your own message template with the desired fields and actions you can control from the Dashboard, and entirely custom app-specific UI for the message.
Leanplum handles the audience, triggering, priority, limits etc., and calls your Action Responder code only when the message should be shown. We trigger the responder with the Action Context for the message with all values.

The Action Arguments support Strings, Numbers, Booleans, Collections - arrays and Dictionaries, Colors and Files.

Steps to Define a Custom Message

Define the custom message using the Leanplum.DefineAction method, providing the desired arguments.
A full code example is available at the bottom.

Arguments
Define the Action Arguments. Each argument requires a name and a default value. The default value is the one that will be shown in the Dashboard when creating a message of that custom type. The default value is set for the argument unless modified for the message in the Dashboard Composer.

void DefineMyCustomMessage()
    {
        string name = "My Custom Message";
        string TitleArg = "Title";
        string ColorArg = "Font Color";
        string OnMainPageArg = "isOnMainPage";
        string PriceArg = "Price";
        string DictionaryArg = "Product Dimensions";
        string ImageArg = "Background Image";
        string AcceptActionArg = "Accept action";
        string CancelActionArg = "Cancel action";

        var dimensions = new Dictionary<string, double>
        {
            { "Height", 160.8 },
            { "Length", 78.1 },
            { "Width", 7.4 },
            { "Diagonal", 6.5 }
        };

        ActionArgs args = new ActionArgs();
        args.With<string>(TitleArg, "Hello");
        args.WithColor(ColorArg, new Color32(49, 77, 121, 255));
        args.With<bool>(OnMainPageArg, false);
        args.With<double>(PriceArg, 99.98);
        args.With<Dictionary<string, double>>(DictionaryArg, dimensions);

        args.WithFile(ImageArg);

        args.WithAction<object>(AcceptActionArg, null);
        args.WithAction<object>(CancelActionArg, null);
...

Dictionary arguments can be 'extended' from the Dashboard - new key/value pairs can be added to the collection.
Lists and Arrays are of static size - only the values can be modified.

File Argument
Note in the above example, that only the File Arg args.WithFile(ImageArg); does not have a default value. Uploading files to the Dashboard for a default value of Custom Messages is not supported yet.
This field will appear empty when composing messages on the Dashboard.

Selecting files and images for the field from the Dashboard is fully supported. When editing the field, the default Image Dialog is opened, where you can select or upload an image.

Defining the Message or Action
Specify the name of the Message Type (shown under In-app message templates), the type - Message or Action, the arguments, and an Action Responder - the code that will be executed when the message should be presented.

Leanplum.DefineAction(name, Constants.ActionKind.MESSAGE, args, new Dictionary<string, object>(), (actionContext) =>
    {
        ...
    });

Getting the Values for the Message
The correct values for each of the fields are fetched from the Action Context which Responder is invoked with.
Actions can be run using the arg name.

string title = actionContext.GetStringNamed(TitleArg);
bool features = actionContext.GetBooleanNamed(OnMainPageArg);
double price = actionContext.GetNumberNamed<double>(PriceArg);
var itemsdict = actionContext.GetObjectNamed<Dictionary<string, double>>(DictionaryArg);
Color col = actionContext.GetColorNamed(ColorArg);

string mimg = actionContext.GetFile(ImageArg);

actionContext.RunTrackedActionNamed(AcceptActionArg);
actionContext.RunActionNamed(CancelActionArg);

📘

actionContext.GetFile returns different value depending on the platform run on.
Unity Native and Editor
It returns a URL to the file when on Unity Native and Editor, for example, "http://lh3.googleusercontent.com/2cqEOX1eJ...".

Android or iOS
It returns a file path to the file on the OS file system when on Android or iOS, for example, "/var/mobile/Containers/Data/Application/89FCF107-.../Library/.../myImage.jpg".

Full Example

Composed In-app Message of the type we have created.

Unity Editor

Mobile

Code

void DefineMyCustomMessage()
    {
        string name = "My Custom Message";
        string TitleArg = "Title";
        string ColorArg = "Font Color";
        string OnMainPageArg = "isOnMainPage";
        string PriceArg = "Price";
        string DictionaryArg = "Product Dimensions";
        string ImageArg = "Background Image";
        string AcceptActionArg = "Accept action";
        string CancelActionArg = "Cancel action";

        var dimensions = new Dictionary<string, double>
        {
            { "Height", 160.8 },
            { "Length", 78.1 },
            { "Width", 7.4 },
            { "Diagonal", 6.5 }
        };

        ActionArgs args = new ActionArgs();
        args.With<string>(TitleArg, "Hello");
        args.WithColor(ColorArg, new Color32(49, 77, 121, 255));
        args.With<bool>(OnMainPageArg, false);
        args.With<double>(PriceArg, 99.98);
        args.With<Dictionary<string, double>>(DictionaryArg, dimensions);

        args.WithFile(ImageArg);

        args.WithAction<object>(AcceptActionArg, null);
        args.WithAction<object>(CancelActionArg, null);

        Leanplum.DefineAction(name, Constants.ActionKind.MESSAGE, args, new Dictionary<string, object>(), (actionContext) =>
        {
            StringBuilder sb = new StringBuilder();

            sb.AppendLine($"Featured: {actionContext.GetBooleanNamed(OnMainPageArg)}");
            sb.AppendLine($"Price: {string.Format("{0:#.00}", actionContext.GetNumberNamed<double>(PriceArg))}");

            var itemsdict = actionContext.GetObjectNamed<Dictionary<string, double>>(DictionaryArg);

            if (itemsdict != null && itemsdict.Count > 0)
            {
                string[] size = itemsdict.Where(k => k.Key != "Diagonal").Select(kv => $"{kv.Key}: {kv.Value}mm").ToArray();
                sb.AppendLine($"{DictionaryArg}: {string.Join(" x ", size)}");
                sb.AppendLine($"Screen Size: {itemsdict["Diagonal"]} inches");
            }

            Color col = actionContext.GetColorNamed(ColorArg);
          
            string mimg = actionContext.GetFile(ImageArg);

            // Game Object Setup
            // Your Custom Logic to Present the Message
            GameObject obj = new GameObject($"CustomMessageDialog-{actionContext.GetStringNamed(TitleArg)}");
            obj.AddComponent<CustomMessageDialog>();

            var cm = obj.GetComponent<CustomMessageDialog>();
            cm.Title = actionContext.GetStringNamed(TitleArg);
            cm.Message = sb.ToString();

            cm.Color = col;
            cm.FilePath = mimg;

            cm.OnAccept += () =>
            {
                actionContext.RunTrackedActionNamed(AcceptActionArg);
            };

            cm.OnCancel += () =>
            {
                actionContext.RunTrackedActionNamed(CancelActionArg);
            };

            obj.transform.SetParent(gameObject.transform.parent);
        });
    }

Game Object logic

In this example, a prefab is used which has Title and Message Text elements, 2 Buttons - Accept and Cancel and they are all wrapped in a Panel. The Panel has an Image Component that is used for the background image.

public class CustomMessageDialog : MonoBehaviour
{
    internal string Title { get; set; }
    internal string Message { get; set; }
    internal Color Color { get; set; }

    internal string FilePath { get; set; }

    internal event Action OnAccept;
    internal event Action OnCancel;

    // Use this for initialization
    void Start()
    {
// Instantiate the prefab
      ...
        GameObject currentMessage = Instantiate(messagePrefab, gameObject.transform);
        currentMessage.name = $"CustomMessageDialog: {Title}";
        var message = currentMessage.GetComponentInChildren<CustomMessagePrefab>();

        message.Title.text = Title;
        message.MessageText.text = Message;

        // Set Message and Buttons Text Color
        message.MessageText.color = Color;
        message.AcceptButton.GetComponentInChildren<Text>().color = Color;
        message.CancelButton.GetComponentInChildren<Text>().color = Color;

        if (!string.IsNullOrEmpty(FilePath))
        {
            // Set background as white, so image appears correctly (without a color mask) 
            message.MessagePanel.GetComponent<Image>().color = Color.white;
            if (FilePath.StartsWith("http"))
            {
                StartCoroutine(
                GetText((tex)=>
                {
                    message.MessagePanel.GetComponent<Image>().sprite = Sprite.Create(tex,
                        new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f));
                }));
            }
            else
            {
                if (File.Exists(FilePath))
                {
                    var tex = LoadImage(new Vector2(500, 500), FilePath);
                    message.MessagePanel.GetComponent<Image>().sprite = Sprite.Create(tex,
                        new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f));
                }
            }
        }

        message.AcceptButton.onClick.AddListener(() =>
        {
            OnAccept?.Invoke();
            Destroy(gameObject);
        });

        message.CancelButton.onClick.AddListener(() =>
        {
            OnCancel?.Invoke();
            Destroy(gameObject);
        });
    }

    private static Texture2D LoadImage(Vector2 size, string filePath)
    {
        byte[] bytes = File.ReadAllBytes(filePath);
        Texture2D texture = new Texture2D((int)size.x, (int)size.y, TextureFormat.RGB24, false);
        texture.filterMode = FilterMode.Trilinear;
        texture.LoadImage(bytes);

        return texture;
    }

    IEnumerator GetText(Action<Texture2D> callback)
    {
        using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(FilePath))
        {
            yield return uwr.SendWebRequest();

            if (!uwr.isNetworkError && !uwr.isHttpError)
            {
                // Get the downloaded image
                var tex = DownloadHandlerTexture.GetContent(uwr);
                callback(tex);
            }
        }
    }
}