Skip to content

gravity-ui/onboarding

Repository files navigation

@gravity-ui/onboarding · npm package CI

Create hint based onboarding scenarios and manage promo activities in your service. Use with React, any other frameworks or vanilla JS.

  • Small. Onboarding ~4Kb, promo-manager ~8kb. Zero dependencies
  • Well tested. ~80% test coverage
  • Small code footprint. Create onboarding scenario config and connect each step to UI with couple lines of code.
  • No UI. Use your own components
  • Good TypeScript support

Table of Contents

Install

npm i @gravity-ui/onboarding

Package contents

This package contains 2 tools:

  • Onboarding - tool for creating hint based onboarding for service users. Create presets with steps, bind to elements and call methods. Onboarding will keep user progress and calculate next hint to show.

  • Promo manager - tool for managing any promo activities you push into user: banners, informers, advertisements of new features, UX surveys, educational popup. Put all promos in config and specify the conditions. Promo manager will keep user progress and calculate next promo to show.

Onboarding guide

How to use onboarding

Basic react example
// todo-list-onboarding.ts
import {createOnboarding, createPreset} from '@gravity-ui/onboarding';

export const {
  useOnboardingStep,
  useOnboardingPreset,
  useOnboardingHint,
  useOnboarding,
  controller,
} = createOnboarding({
  config: {
    presets: {
    // createPreset - wrapper for better type inference
      todoListFirstUsage: createPreset({
        name: '',
        steps: [
          createStep({
            slug: 'createTodoList',
            name: 'create-todo-list',
            description: 'Click button to create todo list',
          }),
          /* other scanario steps */
        ],
      }),
    },
  },
  // onboarding state from backend
  baseState: () => {/* ... */},
  getProgressState: () => {/* ... */},
  // save new onboarding state to backend
  onSave: {
    state: (state) => {/* ... */},
    progress: (progress) => {/* ... */},
  },
});
// App.tsx
import {useOnboardingHint} from '../todo-list-onboarding.ts';

const {anchorRef, hint, open, onClose} = useOnboardingHint();

return (
  <HintPopup
    open={open}
    anchor={anchorRef}
    title={hint?.step.name}
    description={hint?.step.description}
    onClose={onClose}
  />
);
// todo-list.tsx
import {useOnboardingStep} from '../todo-list-onboarding.ts';

const {pass, ref} = useOnboardingStep('createFirstIssue');

return (
  <Button
    onClick={() => {
      pass();
      handleAddTodoList();
    }}
    ref={ref}
    // ...
  >
    "Add new list"
  </Button>
);

Onboarding configuration

You can configure onboarding

const onboardingOptions = {
  config: {
    presets: {/**/},
    baseState: {/**/}, // initial state for current user
    getProgressState: () => {/**/}, // function to load user progress
    onSave: {
      // functions to save user state
      state: (state) => {/**/},
      progress: (progress) => {/**/},
    },
    showHint: (state) => {
      /**/
    }, // optional. function to show hint. Only for vanilla js usage
    logger: {
      // optional. you can specify custom logger
      level: 'error' as const,
      logger: {
        debug: () => {/**/},
        error: () => {/**/},
      },
    },
    debugMode: true, // optional. true will show a lot of debug messages. Recommended for dev environment
    plugins: [/**/], // optional. you can use existing plugins or write your own
    hooks: {
      // optional. you can subscribe to onboarding events
      showHint: ({preset, step}) => {/**/},
      stepPass: ({preset, step}) => {/**/},
      addPreset: ({preset}) => {/**/},
      beforeRunPreset: ({preset}) => {/**/},
      runPreset: ({preset}) => {/**/},
      finishPreset: ({preset}) => {/**/},
      beforeSuggestPreset: ({preset}) => {/**/},
      beforeShowHint: ({stepData}) => {/**/},
      stateChange: ({state}) => {/**/},
      hintDataChanged: ({state}) => {/**/},
      closeHint: ({hint}) => {/**/},
      init: () => {/**/},
      wizardStateChanged: ({wizardState}) => {/**/},
    },
  },
};

For default preset you can specify properties:

const onboardingOptions = {
  config: {
    presets: {
      // you can also use createPreset helper
      createProject: {
        // preset name should be unique
        name: 'Creating project', // text for user
        type: 'default', // optional. 'default'(default value) | 'interlal' | 'combined'
        visibility: 'visible', // optional. 'visible'(defaule value) | 'initialHidden' | 'alwaysHidden';
        description: '', // optional, text for user
        steps: [
          {
            slug: 'openBoard', // step slug must be unique across all presets
            name: '', // text to show in popup
            description: '', // text to show in popup
            placement: 'top', // optional. Hint placement for step
            passMode: 'onAction', // optional. 'onAction'(default value) | 'onShowHint' - trigger step pass on hint show
            hintParams: {}, // optional. any custom properties for hint
            closeOnElementUnmount: false, // optional. default valeue - false. Will close hint when element umnounts. 'True' not reccomended in general^ but may me helpful for some corners
            passRestriction: 'afterPrevious', // optional. afterPrevious will block pass step is previous not passed
            hooks: {
              // optional
              onStepPass: () => {/**/},
              onCloseHint: () => {/**/},
              onCloseHintByUser: () => {/**/},
            },
          },
        ],
        hooks: {
          // optional
          onBeforeStart: () => {/**/},
          onStart: () => {/**/},
          onEnd: () => {/**/},
        },
      },
    },
  },
};

Also, there are combined preset. It is group presets, but it acts like one. When combined preset runs, it resolves to one of internal preset. You can find example in test data

Plugins and event

You can use event system. Available events: showHint, stepPass, addPreset, beforeRunPreset, runPreset, finishPreset, beforeSuggestPreset, beforeShowHint, stateChange, hintDataChanged, closeHint, init, wizardStateChange

controller.events.subscribe('beforeShowHint', callback);

You can use plugins

  • MultiTabSyncPlugin - closes hint in all browser tabs.
new MultiTabSyncPlugin({
    enableStateSync: false, // Experimantal. Default - false(recommended). Sync all onboarding state.
    enableCloseHintSync: true, // closes hont in all browser tabs,
    changeStateLSKey: 'onboarding.plugin-sync.changeState', // localStorage key for state sync
    closeHintLSKey: 'onboarding.plugin-sync.closeHint', // localStorage key for close hint in all tabs
});
  • PromoPresetsPlugin - all 'always hidden presets' becomes 'promo presets'. They can turn on onboarding, can only show hint only if user not interact with common onboarding presets. Perfect for educational hints
new PromoPresetsPlugin({
    turnOnWhenShowHint: true, // Default - true. Force to turn on onboarding, when promo hint should be shown
    turnOnWhenSuggestPromoPreset: true, // Default - true. Force to turn on onboarding, when promo preset suggested
});
  • WizardPlugin - highly recommended, if wizard is used. hide wizard on run preset, show on finish, erase progress for not finished presets on wizard close.

Example:

import {createOnboarding} from '@gravity-ui/onboarding';
import {
  MultiTabSyncPlugin,
  PromoPresetsPlugin,
  WizardPlugin,
} from '@gravity-ui/onboarding/dist/plugins';

const {controller} = createOnboarding({
  /**/
  plugins: [
    new MultiTabSyncPlugin({enableStateSync: false}),
    new PromoPresetsPlugin(),
    new WizardPlugin(),
  ],
});

You can write your own plugin

import {createOnboarding} from '@gravity-ui/onboarding';
import {WizardPlugin} from '@gravity-ui/onboarding/dist/plugins';

const myPlugin = {
  apply: (onboarding) => {
    /**
     * Do something with onboarding controller
     * For exampe subscribe on event
     *  onboarding.events.subscribe('init', () => {});
     */
  },
};

const {controller} = createOnboarding({
  /**/
  plugins: [new WizardPlugin(), myPlugin],
});

Promo manager

How to use promo-manager

1. Init promo manager and setup the progress update

// promo-manager.ts

import { createPromoManager } from '@gravity-ui/onboarding/dist/promo-manager';
import { ShowOnceForPeriod } from '@gravity-ui/onboarding/dist/promo-manager/helpers';

export const { controller, usePromoManager, useActivePromo } = createPromoManager({
    config: {
        promoGroups: [{
            slug: 'poll',
            conditions: [ShowOnceForPeriod({month: 1})],
            promos: [
                {
                    slug: 'issuePoll',
                    conditions: [ShowOncePerMonths(6)],
                    meta: {...}
                },
            ],
        }],
    },
    progressState: () => {/* ... */},
    getProgressState: () => {/* ... */},
    onSave: {
        progress: (state) => () => {/* ... */},
    },
});

2. Trigger promo in your component

// TriggerExample.tsx

import { usePromoManager } from './promo-manager';

const { status, requestStart, skipPromo } = usePromoManager('issuePoll');

useMount(() => {
    requestStart();
});

useUnmount(() => {
    skipPromo();
});

if(status === 'active') {
    // allowed to run. Do something
}

Condition and constraints

You can use conditions for each promo. Or use constraints to set limitations between promos.

Promo could be started only if

  • All promo conditions returns true
  • All promo group condition returns true
  • All constraints passed
import {
    ShowOnceForSession,
    ShowOnceForPeriod,
    MatchUrl,
    LimitFrequency
} from '@gravity-ui/onboarding/dist/promo-manager/helpers';

const groupOfPolls = {
    slug: 'groupOfPolls',
    promos: [
        {slug: 'somePoll', conditions: [ShowOnceForPeriod({month: 1})]},
        {slug: 'pollForPageWithparam', conditions: [
            MatchUrl('param=value'),
            ShowOnceForPeriod({month: 5})
        ]}
    ],
}

const groupOfHints = {
    slug: 'groupOfHints',
    conditions: [ShowOnceForSession()],
    promos: [
        {slug: 'someHint'},
        {slug: 'hintForSpecificPage', conditions: [
            MatchUrl('/folder/\\w{5}/page$'),
        ]},
    ],
}

const {controller} = createPromoManager({
    config: {
        constraints: [
            LimitFrequency({
                slugs: ['somePoll', 'groupOfHints'], // can use promos slugs and group slugs
                interval: {days: 1},
            })
        ],
        promoGroups: [groupOfHints, groupOfPolls]
    },
});

You can write your own conditions.

const user = {/**/} // get user from state
const usersWithLanguage = (language) => user.language = language;

const promo = {slug: 'somePoll', conditions: [usersWithLanguage('english')]};

Promo manager events

Promo manager can run promo on events.

const promo = {
    slug: 'promo1',
    conditions: [],
    trigger: {on: 'someCustomEvent', timeout: 1000}
}

const {controller} = createPromoManager({
    config: {
        promoGroups: [{
            slug: 'group',
            conditions: [],
            promos: [promo],
        }]
    },
});

controller.sendEvent('someCustomEvent')

You can also use UrlEventPlugin to run promos on specific url. Promo will run when user opens page.

const {controller} = createPromoManager({
    config: {
        promoGroups: [
            {
                slug: '1',
                promos: [
                    {
                        slug: 'promo1',
                        conditions: [MatchUrl('/folder/\\w{5}/page$'),],
                        trigger: {on: 'pageOpened', timeout: 2000},
                    },
                ],
            },
        ],
    },
    plugins: [new UrlEventsPlugin({eventName: 'pageOpened'})],
})

JSON config

You can define conditions, constraints as JSON serializable objects. So you can take config from json, parse it and use. It can be useful for editing config without rebuild project and release.

const usersWithLanguage = (language) => user.language = language;

const {controller} = createPromoManager({
    config: { // config section now can be parsed from json
        constraints: [
            {
                helper: 'LimitFrequency',
                args: [{
                    slugs: ['somePoll', 'groupOfHints'], // can use promos slugs and group slugs
                    interval: {days: 1},
                }],
            },
        ],
        promoGroups: [{
            slug: 'groupOfPolls',
            promos: [
                {
                    slug: 'somePoll',
                    conditions: [{
                        helper: 'ShowOnceForPeriod',
                        args: [{month: 1}]
                    }]
                },
                {
                    slug: 'pollForPageWithparam',
                    conditions: [
                        {
                            helper: 'MatchUrl',
                            args: ['param=value']
                        },
                        {
                            helper: 'usersWithLanguage',
                            args: ['English']
                        },
                    ]
                }
            ],
        }]
    },
    conditionHelpers: {
        usersWithLanguage,
    },
});

Onboarding integration

You can use onboarding with PromoPresetsPlugin to show advertising and educational hints. You can use promo manager to limit frequency and set constraint with other promo in service.

import {
    createOnboarding
} from "./index";

const {controller: onboardingController} = createOnboarding({
    config: {
        presets: {
            coolNewFeature: {
                name: 'Cool feature',
                visibility: 'alwaysHidden',
                steps: [/**/]
            },
            coolNewFeature2: {
                name: 'Cool feature2',
                visibility: 'alwaysHidden',
                steps: [/**/],
            }
        }
    },
    plugins: [new PromoPresetsPlugin(),]
   /**/ 
})

const {controller} = createPromoManager({
    ...testOptions,
    config: {
        promoGroups: [
            {
                slug: 'hintPromos',
                conditions: [ShowOnceForSession()], // only 1 promo hint for session
                promos: [
                    {
                        slug: 'coolNewFeature', // slug = onboarding preset name 
                        conditions: [/**/], //  you can add additional conditions
                    },
                ],
            },
        ],
    },
    onboarding: {
        getInstance: () => onboardingController,
        groupSlug: 'hintPromos',
    },
});