@slack-wrench/bolt-interactions
TypeScript icon, indicating that this package has built-in type declarations

2.1.0 • Public • Published

Bolt ⚡️ Interactions

Bolt interactions allows you to effortlessly create stateful user action flows with your Bolt app.

Vanilla Bolt gives you some ability to do this already, but its state is scoped to a whole channel. This library keeps the scope to the interaction allowing for faster data fetching and a greater ability to rationalize how your app works.

Install

yarn add @slack-wrench/bolt-interactions
# or
npm install --save @slack-wrench/bolt-interactions

Understanding Flows

This package creates a new concept of a user interaction called a flow. A flow logically represents a set of linked actions typically associated with a user flow. Introducing this new concept makes your application easier to reason about and keeps your focus on the outcome and user interaction.

flows are implemented as a set of bolt event listeners linked by a shared state.

Here's an example:

import { App } from '@slack/bolt';
import {
  interactionFlow,
  InteractionFlow,
} from '@slack-wrench/bolt-interactions';
import FileStore from '@slack-wrench/bolt-storage-file'; // Use whatever ConversationStore you want

// Create and configure your app with a ConversationStore
const convoStore = new FileStore();
InteractionFlow.store = convoStore;
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
  convoStore,
});

// Create a new interaction, usually this will be the default export of its own file
const commandFlow = interactionFlow('yourCommand', (flow, app) => {
  app.command('/bolt-interaction', ({ say }) => {
    // Start a new flow from a command, set an initial state
    const { interactionIds } = flow.start({ clicked: 0 });

    say({
      blocks: [
        {
          type: 'section',
          text: {
            type: 'plain_text',
            text: 'You used a command!',
            emoji: true,
          },
        },
        {
          type: 'actions',
          elements: [
            {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Click Me',
                emoji: true,
              },
              // Use flow interaction ids from context
              action_id: interactionIds.appButton,
            },
            {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Stop',
                emoji: true,
              },
              // Use flow interaction ids from context
              action_id: interactionIds.stopButton,
            },
          ],
        },
      ],
    });
  });

  flow.action('appButton', async ({ context, say }) => {
    // Access the current state, and functions to set new state, end the flow,
    // or get interactionIds for continuing the flow
    const { state, setState, endFlow, interactionIds } = context;

    state.clicked += state.clicked;

    say(`The button has been clicked ${state.clicked}`);

    // Update the state for new buttons
    await setState(state);
  });

  flow.action('stopButton', async ({ context, say }) => {
    const { endFlow } = context;

    say('You ended the flow');

    // Cleans up state in the database
    await endFlow();
  });
});

// Register your flows to the app
commandFlow(app);

(async () => {
  // Start the app
  await app.start(process.env.PORT || 3000);

  console.log('⚡️ Bolt app is running!');
})();

Working with your Flow

Flow Listeners

Flow listeners are a special set of bolt event listeners. They can be created with the flow counter part of bolt app listeners (flow.action and app.action for example). They can do everything that normal bolt listeners can plus they can interact with the flow they're a part of.

Flow Context

Flow listeners have their context extended with some extra functions and data. You can also get these through flow.start.

When you create a new flow with flow.start, you've created a "flow instance" that is unique to that interaction.

  • state (FlowState) - The current state of that flow instance.
  • setState ((state: any, expiresAt?: number) => Promise) - Function to update the state
  • endFlow (() => Promise) - Function to end the flow, and clean up its state
  • interactionIds (Record<string, string)) - ids to pass to block kit for various actions, usually action_id.

interactionFlow

interactionFlow<FlowState>((flow, app) => {
  // Flow code
});
// => : (App) => InteractionFlow

Helper function to create a new InteractionFlow. It allows you to separate your interaction flow from the Bolt app to help organize your code base.

flow.start

At any time, you can start a new flow. You can do it when a non-stateful slack action happens (like a command, or a message), or when something outside of slack happens, like a webhook from github.

interactionFlow<FlowState>((flow, app) => {
  flow.start(initialState); // Can be called any time
  // => : Promise<Interaction.FlowContext<FlowState>>
});

Starts a new flow and sets the initial state. You can also pass an optional instanceId.

Arguments:

  • initialState (any): The starting state of the flow. Flow middleware will get this value whenever they're called.
  • instanceUd (string) Optional: If you have an instance id that you want to use, otherwise it will be generated with InteractionFlow.interactionIdGenerator

Returns: The flow context

flow.action

interactionFlow<FlowState>((flow, app) => {
  flow.action(...listeners, { context, ... });
});

Create a flow listener that listens for actions

flow.view

In handling views, you'll need to pass an interactionId as the callback and reference the same string in a flow.view action handler. An example of handling view submissions with a view that is opened when an overflow action is clicked:

interactionFlow<FlowState>((flow, app) => {
  const callback_id = 'edit';

  flow.action<BlockOverflowAction>(
    'openEdit',
    async ({
      action: { action_id },
      body: { trigger_id },
      context: { token, interactionIds },
    }) => {
      await flow.client.views.open({
        trigger_id,
        token,
        view: {
          type: 'modal',
          // setup callback_id with unique interactionId based on string
          callback_id: interactionIds[callback_id],
          title: PlainText('Edit Story'),
          submit: PlainText('Update Story'),
          close: PlainText('Cancel'),
          blocks: [
            /* ... */
          ],
        },
      });
    },
  );

  // handle all submissions of above; callback_id is used to recapture context
  flow.view<ViewSubmitAction>(callback_id, async ({ view, context, ... }) => {
    /* do things when a SlackViewAction is triggered */
  });
});

Working with Ids

If you are working with your own ids for flow instances, it can be helpful to parse them. There some utilities to help with that.

InteractionFlow.parseFlowId

const parsedFlowId = interactionFlow.parseFlowId('flow_12345');

expect(parsedFlowId).toEqual({
  flowId: 'flow_12345',
  name: 'flow',
  instanceId: '12345',
});

InteractionFlow.parseInteractionId

const parsedFlowId = interactionFlow.parseFlowId('flow_12345:::interaction');

expect(parsedFlowId).toEqual({
  flowId: 'flow_12345',
  name: 'flow',
  instanceId: '12345',
  interaction: 'interaction',
  interactionId: 'flow_12345:::interaction',
});

Testing your Flow

When testing your flows, it's helpful to have some predictability and not need whole databases. bolt-interactions exports a few hooks that you can use to make this happen.

import { MemoryStore } from '@slack/bolt';
import { InteractionFlow } from '@slack-wrench/bolt-interactions`;

// Update the store that Interaction flows use
InteractionFlow.store = new MemoryStore();

// Change the function that randomly generates ids to something a
// bit more predictable
InteractionFlow.interactionIdGenerator = () => 'a-random-string';

Readme

Keywords

none

Package Sidebar

Install

npm i @slack-wrench/bolt-interactions

Weekly Downloads

48

Version

2.1.0

License

Apache-2.0

Unpacked Size

405 kB

Total Files

25

Last publish

Collaborators

  • directctrl
  • barlock