AppSuite:Upsell

From Open-Xchange
Upsell

Abstract. This article is mainly for UI developers and introduces the concept of upsell from a technical point of view. In short: End-user has a set of capabilities. UI offers features beyond that for promotion purposes. Actions, e.g. inline links, that require missing capabilities trigger an in-app upsell. This process leads to a trial period or a new subscription. Technical challenge for the UI developer is to check what the end-user has, what can be shown beyond that, and how to handle upsell. It is also possible for hosting companies to easily integrate their own online shop into OX Upsell, since the internal mechanisms are loosely coupled via events.

Events

Whenever the user starts an app or clicks on an inline-action, a capability-check is performed. For example, all inline actions have native support for such checks:

 
new Action('io.ox/calendar/detail/actions/sendmail', {
    // this action requires the capability "webmail"
    capabilities: 'webmail',
    action: function (baton) {
        // send mail
    }
});

If the end-user does not have "webmail" (e.g. in a files-only setup) but calls this action, a proper event is fired:

 
// if any action misses a capability
ox.trigger('upsell:requires-upgrade');
// which provides the following data for apps:
{
  type: "app",
  id: "io.ox/mail/main",
  missing: "webmail"
}
// and for inline-actions:
{
  type: "inline-action",
  id: "io.ox/calendar/detail/actions/sendmail",
  missing: "webmail"
}

Capabilities

There are lots of different capabilities. They are defined on the server-side and basically they are just strings. Let's keep it simple and understand them as either services (e.g. mobility), specific functionalities (e.g. multiple_mail_accounts) or applications (e.g. calendar). Some obvious examples:

Capability Description Upsell trigger (if capability is missing)
calendar User has "Calendar" app
  • Mail/All recipients: Invite to appointment
  • Top bar
  • Launch pad
contacts User has "Address Book" app
  • Mail/App recipients: Save as distribution list
  • Calendar: Save participants as distribution list
  • Top bar
  • Launch pad
infostore User has "Files" app
  • Mail: Save in infostore
  • Top bar
  • Launch pad
portal User has "Portal" app
  • Mail: Add to portal
  • Contacts: Add to portal
  • Files: Add to portal
  • Top bar
  • Launch pad
tasks User has "Tasks" app
  • Mail: Remind me
  • Top bar
  • Launch pad
webmail User has "Mail" app
  • Calendar: Send mail to all participants
  • Contacts: Send mail
  • Contacts: Send vCard
  • Files: Send as link
  • Files: Send by mail
  • Top bar
  • Launch pad
 
// list all available capabilities
_(ox.serverConfig.capabilities).pluck('id').sort();

An example: Free-mail users might just have webmail and contacts. If infostore is enabled for upsell, end-users will see the link to store mail attachments. But since this capability is missing, the event "upsell:requires-upgrade" is triggered which starts the upsell process. Upon successful completion this process should unlock the capability infostore for the end-user.

The advantage of using rather atomic capabilities as the foundation for upsell is that developers don't have to consider and implement sales programs or marketing matrices in UI code.

Example dialog

Whenever the event "upsell:requires-upgrade" is triggered there should be some response for the end-user. Usually an upsell dialog should open. This can be implemented as follows:

 
function showUpgradeDialog(e, options) {
    require(['io.ox/core/tk/dialogs'], function (dialogs) {
        new dialogs.ModalDialog({ easyOut: true })
            .build(function () {
                this.getHeader().append(
                    $('<h4>').text('Upgrade required')
                );
                this.getContentNode().append(
                    $.txt('This feature is not available.'),
                    $.txt('You need to upgrade your account now.'),
                    $.txt(' '),
                    $.txt('The first 90 days are free.')
                );
                this.addPrimaryButton('upgrade', 'Get free upgrade');
                this.addButton('cancel', 'Cancel');
            })
            .setUnderlayStyle({
                opacity: 0.70,
                backgroundColor: '#08C'
            })
            .on('upgrade', function () {
                ox.trigger('upsell:upgrade', options);
            })
            .on('show', function () {
                ox.off('upsell:requires-upgrade', showUpgradeDialog);
            })
            .on('close', function () {
                ox.on('upsell:requires-upgrade', showUpgradeDialog);
            })
            .show();
    });
}

function upgrade(e, options) {
    console.debug('upgrade', options);
    alert('User decided to upgrade! (global event: upsell:upgrade)');
}

ox.on('upsell:requires-upgrade', showUpgradeDialog);

/*
 * convention: 'upsell:upgrade' is used to trigger final upsell
 * the current user and user_id can be found in global variables ox.user and ox.user_id
 */
ox.on('upsell:upgrade', upgrade);

Hint: For simple demo purposes, you can enable an internal upsell configuration by appending "&demo=upsell" to the URL. Needs to reload page, of course.

The second event "upsell:upgrade" can be understood as the final imperative to request the upsell server-side.

Example portal widget

Besides waiting for the user to click on such links, it's always a good idea to offer explicit controls to trigger an upsell. One option is creating a portal widget that advertises a premium subscription:

 
/**
 * This work is provided under the terms of the CREATIVE COMMONS PUBLIC
 * LICENSE. This work is protected by copyright and/or other applicable
 * law. Any use of the work other than as authorized under this license
 * or copyright law is prohibited.
 *
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * © 2013 Open-Xchange Inc., Tarrytown, NY, USA. info@open-xchange.com
 *
 * @author Matthias Biggeleben <matthias.biggeleben@open-xchange.com>
 */

define('plugins/portal/upsell/register',
    ['io.ox/core/extensions',
     'io.ox/files/api',
     'gettext!plugins/portal'], function (ext, api, gt) {

    'use strict';

    var title = gt('Upgrade to premium');

    ext.point('io.ox/portal/widget/upsell').extend({

        title: title,

        preview: function (baton) {

            this.addClass('hide-title').append(
                $('<div class="content centered" style="cursor: pointer; padding-top: 3em;">').append(
                    $('<h2>').append(
                        $.txt(title + ' '),
                        $('<i class="icon-star">')
                    ),
                    $('<div>').text(gt('Click here for free trial.'))
                )
                .on('click', function () {
                    ox.trigger('upsell:upgrade', {
                        type: 'widget',
                        id: 'io.ox/portal/widget/upsell',
                        missing: ''
                    });
                })
            );
        }
    });
});

Accessing upsell settings

The upsell configuration is located in the namespace "io.ox/core", the path is "upsell/enabled". Example:

 
// get all capabilities that can trigger upsell
require('settings!io.ox/core').get('upsell/enabled');

// contains data like this
{
  infostore: true,
  portal: true,
  tasks: true
}

If upsell is not enabled and the end-user lacks specific capabilities, the app or the inline-action is not shown. If upsell is enabled by the upper configuration, inline-actions are shown and trigger the upsell event "upsell:requires-upgrade" if clicked (but do not execute the action itself).

 
/* 
 * if you want to create your own controls, you can use the following helpers 
 */
var upsell = require('io.ox/core/upsell');

// check capabilities (space-separated) 
upsell.has('portal webmail');

// get missing capabilities (would return "calendar" in demo mode) 
upsell.missing(['portal webmail', 'contacts', 'calendar']);

/* checks if upsell is enabled for a set of capabilities 
 * true if at least one set matches 
 */
upsell.enabled(['portal webmail', 'webmail calendar']);

/* convenience function: "visible" 
 * checks if something should be visible depending on required capabilities 
 * true if any item matches requires capabilities 
 * true if any item does not match its requirements but is enabled for upsell 
 * this function is used for any inline link, for example, to decide whether or not showing it 
 */
upsell.visible(['portal webmail', 'contacts', 'calendar']);

// likewise if neither capability set nor enabled for upsell, we get a false 
upsell.visible(['foo']);

// in case something weird happens (usually bad configuration) debug() helps
upsell.debug();

// and this one
_(ox.serverConfig.capabilities).pluck('id').sort();

Server settings

In order to configure this server-side, just create a new file upsell.properties or append to existing appsuite.properties (mind the double-slash; this in not a typo! plus: changing such settings requires a backend restart):

 
io.ox/core//upsell/enabled/infostore=true
io.ox/core//upsell/enabled/portal=true
io.ox/core//upsell/enabled/tasks=true

Upsell Wizard

Hosters usually want to offer context-sensitive content in an IFRAME. Therefore, App Suite comes with an integrated optional plugin that takes care of this. Just enable plugins/upsell/simple-wizard. This plugin registers for the event "upsell:requires-upsell", opens a modal popup, and loads a custom URL in an embedded IFRAME.

Custom content in an IFRAME

Wizard settings

In order to configure this server-side, just create a new file upsell.properties or append to existing appsuite.properties (mind the double-slash; this in not a typo! plus: changing such settings requires a backend restart):

plugins/upsell/simple-wizard//url=blank.html?user=$user,user_id=$user_id,context_id=$context_id
plugins/upsell/simple-wizard//overlayOpacity=0.5
plugins/upsell/simple-wizard//overlayColor=black
plugins/upsell/simple-wizard//zeroPadding=true
plugins/upsell/simple-wizard//width=750
plugins/upsell/simple-wizard//height=390
Settings Description
url Custom URL that is loaded in IFRAME; can contain special variables.
overlayOpacity CSS opacity value for overlay; default is 0.5
overlayColor CSS background color for overlay; default is black
zeroPadding If true (default) there is no inner padding inside modal dialog, i.e. the IFRAME covers the popup
width Width of outer popup (not IFRAME) in pixel
height Height of IFRAME in pixel

Custom URL variables

The plugin offers a set of variables that help providing context-sensitive content. $missing is probably the most prominent one. Other variables help identifying the user. An example:

upsell.php?user_id=$user_id&context_id=$context_id&language=$language&missing=$missing
Variable Description
$context_id context_id of current user
$hostname hostname of current session, e.g. www.one-of-countless-virtual-hosts.com
$id The trigger's identifier, e.g. "io.ox/files". Can refer to an app or to an inline action. See $type
$language The current user's language, e.g. de_DE or en_US
$missing The set of missing capabilities, comma separated, e.g. "files"
$session The current user's session id
$type Either "app" or "inline-action". Describes what triggered the upsell. See $id
$user The current user's login name
$user_id The current user's numeric id

Debugging

While experimenting or developing with this plugin, you can use the following helpful functions:

 
// get plugin (this muse be properly loaded, otherwise you get a runtime error)
var wizard = require('plugins/upsell/simple-wizard/register');

// get variables (optional: options from upsell:require-upgrade event)
wizard.getVariables({ type: 'app', id: 'io.ox/files', missing: 'files' });

// get URL (optional: options from upsell:require-upgrade event)
wizard.getURL({ type: 'app', id: 'io.ox/files', missing: 'files' });

// extend IFRAME constructor (see http://underscorejs.org/#compose)
var custom = function (iframe) {
  return iframe.css({ border: '5px solid #08c', boxSizing: 'border-box' });
};
wizard.getIFrame = _.compose(custom, wizard.getIFrame);

// global upsell events
ox.on('upsell:show-simple-wizard:before', _.inspect);
ox.on('upsell:show-simple-wizard', _.inspect);
ox.on('upsell:close-simple-wizard', _.inspect);

// open wizard manually
wizard.open();

// close wizard manually
wizard.close();

Upsell in OX6

Please also consider this article as it also covers backend aspects.