Building an onboarding flow

In this post, we go over building an onboarding flow for our users where they will link their accounts to our Telegram bot.

Building an onboarding flow

In this post, we go over building an onboarding flow for our users where they will link their accounts to our Telegram bot.

This is the fourth post in a series of posts documenting my journey building Botler - your personal AI butler. In this post, I will build the the onboarding flow for our users where they connect their account to the Telegram bot.

A quick overview of what we've done so far.  We've set up a NextJS app with authentication and connected it to a database. We also have a barebones Telegram bot running on an ExpressJS server. There's no point if these two can't talk to each other so that's what we're going to take care of now.

In this post, we'll build the first thing the users see when they log in to their dashboard. An onboarding flow where they connect their Telegram account to the web app so that we have enough information for the bot to be able to dynamically send messages (notifications, alerts) to our users. So yeah, let's get started!

Setting up the database model

For our bot to be able to message our users, we need to know their Telegram username and ID. And in order to be able to store these, we need to add these fields to our database.

We'll start by creating a new model to store Telegram related details for our users.

Telegram details model
Telegram details model

This model is pretty self-explanatory but the main fields to look at here are the username, telegram_id, and the user_id. We will be using the user_id field to link the telegram_details model to the users model so we know which user these telegram_details belong to. Linking these tables will also allow us to ensure that one user can only edit/update their own telegram_details.

Building the onboarding modal

Our model is ready but in order to be able to populate data in it, we need to show our users some sort of UI where they can give us this information. We're going to do this in an onboarding modal. The idea is, if a user has completed their onboarding, they get access to the dashboard and if they haven't, they will be shown this modal.

const OnboardingModal = ({ isOpen, setIsOpen }: Props) => {
  const [telegramUsername, setTelegramUsername] = useState('')

  return (
    <Modal isOpen={isOpen} setIsOpen={setIsOpen} shouldCloseOnEsc={false}>
      <div className="flex flex-col">
        <div className="flex flex-col">
          <p className="text-gray-900 text-xl font-semibold">
            Welcome to Botler
          </p>
          <p className="text-gray-700 text-sm">
            Before you can start using the dashboard, we need a few more details
            about you.
          </p>
        </div>

        <Wizard>
          <Steps>
            <Step
              id="username"
              render={({ next }) => (
                <UsernameStep
                  next={next}
                  setTelegramUsername={setTelegramUsername}
                />
              )}
            />
            <Step
              id="connect"
              render={({ previous, next }) => (
                <ConnectionStep
                  previous={previous}
                  next={next}
                  telegramUsername={telegramUsername}
                />
              )}
            />
            <Step id="success" render={() => <SuccessStep />} />
          </Steps>
        </Wizard>
      </div>
    </Modal>
  )
}
Onboarding modal

In the snippet above, we open a modal in which we run our onboarding flow. To build our onboarding flow, we've created a wizard UI using react-albus. Our wizard flow consists of 3 steps:

  • Username step: In this step, we show our users a form and ask them for their Telegram username.
  • Connection step: In this step, we ask our users to initiate a chat with our Telegram bot. Once they do that, we receive the message on our backend server and attach their user ID to their username.
  • Success step: If everything goes according to plan, we show our users a message mentioning that they've successfully connected their account to the bot.

Username step

const UsernameStep = ({ next, setTelegramUsername }: Props) => {
  const [insertTelegramUsername] = useMutation(INSERT_TELEGRAM_USERNAME, {
    onError: () => {
      showErrorAlert({
        text:
          'Looks like something went wrong. Could you please reach out to support for help with the issue?',
      })
    },
    onCompleted: (data) => {
      const {
        insert_telegram_details_one: { username },
      } = data
      setTelegramUsername(username)
      next()
    },
  })
  return (
    <Formik
      initialValues={{ telegramUsername: '' }}
      validationSchema={UsernameSchema}
      onSubmit={async (values, { setSubmitting }) => {
        const { telegramUsername } = values

        try {
          await insertTelegramUsername({
            variables: { username: telegramUsername },
          })
          setSubmitting(false)
        } catch (error) {
          showErrorAlert({
            text: 'Something went wrong. Please try again later',
          })
        }
      }}
    >
      {({ isSubmitting }) => (
        <Form>
          <label className="mt-5 block">
            <span className="block mb-1 text-sm font-medium text-gray-700">
              Telegram username
            </span>
            <Field
              name="telegramUsername"
              type="text"
              placeholder="farazpatankar"
              className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
            />
          </label>
          <ErrorMessage
            name="telegramUsername"
            component="span"
            className="text-red-500 text-sm"
          />
          <button
            type="submit"
            disabled={isSubmitting}
            className="mt-5 flex py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-800 hover:bg-indigo-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          >
            {isSubmitting ? (
              <Spinner classes="text-white h-5 w-5" />
            ) : (
              <>Next</>
            )}
          </button>
        </Form>
      )}
    </Formik>
  )
}
Username step

In the snippet above, we show our users a basic form with just one field asking for their Telegram username. Once they submit the form, we call our insertTelegramUsername mutation to create a new entry in our telegram_details table. When running the mutation, if something goes wrong, we show our users an alert asking them to reach out to us and if everything works as expected, we save the username to a state variable and take them to the next step.

We handle conflicts within our mutation as well. If for whatever reason (partially completed wizard flow), we already have an entry in the telegram_details table for this user, we just replace that entry with the username the user submits here.

const INSERT_TELEGRAM_USERNAME = gql`
  mutation($username: String!) {
    insert_telegram_details_one(
      object: { username: $username }
      on_conflict: {
        constraint: telegram_details_user_id_key
        update_columns: username
      }
    ) {
      username
    }
  }
`
Insert telegram username mutation

If you notice, we don't pass the user_id field in our mutation. That's because Hasura has a neat option called column presets where we can set values from session variables while adding an entry into a column.

Column preset
Column preset

Connection step

Connection step
Connection step

Very self-explanatory stuff here. We tell our users that we've saved their Telegram username and that we need them to add the bot on Telegram. Once they add the bot on Telegram, it will trigger the /start command for our bot where we will (on the backend) listen for messages from their Telegram username and once the message is received, we will save their Telegram ID.

bot.start(async (ctx) => {
  let reply = 'Welcome to Botler'
  const username = ctx.message?.from.username
  const telegramId = ctx.message?.from.id

  if (!username) {
    ctx.reply(reply)
    return
  }

  try {
    const data = await client.request(GET_USER_BY_TELEGRAM_USERNAME, {
      username,
    })

    const { telegram_details_by_pk } = data

    if (!telegram_details_by_pk) {
      ctx.reply(reply)
      return
    }

    const {
      telegram_details_by_pk: {
        user: { display_name: name },
        telegram_id,
      },
    } = data

    reply += `, ${name}`

    /*
      If there is noo telegram_id in the database,
      update it.
    */
    if (!telegram_id) {
      await client.request(UPDATE_TELEGRAM_ID, {
        username,
        telegramId,
      })

      reply +=
        '. We have successfully updated your Telegram ID. Please return to the dashboard to continue your onboarding'
    }
  } catch (error) {
    console.error(error)
  }

  ctx.reply(reply + '!')
})
/start command

The code above is again, pretty self-explanatory. Whenever the bot receives a /start command, we query the database and check if a user with that username exists. If they do, we check whether we have their telegram_id and if we don't, we save it. After this, we show them a message asking them to return to the dashboard and continue their onboarding.

Within the dashboard, we want to double-check that we have their telegram_id after they click the next button. We do that in the handleNext function shared below.

const { loading, refetch } = useQuery(GET_USER_BY_TELEGRAM_USERNAME, {
  variables: {
    username: telegramUsername,
  },
})

const handleNext = async () => {
  try {
    const response = await refetch({ username: telegramUsername })

    const {
      data: {
        telegram_details_by_pk: { telegram_id: telegramId },
      },
    } = response

    if (!telegramId) {
      showErrorAlert({
        text:
          "Looks like we don't have your Telegram ID yet. Are you sure you followed the process mentioned? If this keeps happening, please reach out to me for support.",
      })
    } else {
      next()
    }
  } catch (error) {
    console.error(error)
    showErrorAlert({ text: 'Something went wrong. Please try again later.' })
  }
}
handleNext function

In this function, we query the telegram_details object for this user in our database to see if we have their telegram_id. If we don't have it, we show them an error and ask them to reach out if they need some support. If we have their Telegram ID, we take them to the next step and show them the success message.

Success step
Success step

And with that, our onboarding flow is complete! You can see the full flow in action in the GIF below.

Onboarding flow
Onboarding flow

Closing

We're finally done with all the putting things together stuff and can now start building features. In subsequent posts, we will focus on incrementally adding services that the bot can send notifications and alerts for, and also work on adding some basic automation related features.

Let me know what you thought about this post here. And as always, feel free to reach out to me with any questions, suggestions, and feedback.

Another reminder that we're only 3 weeks away from launch and have only 23 (at the time of writing this) pre-order items remaining. You can get a lifetime subscription to Botler for only $10 if you place your order ASAP. You won't even be billed till the product goes live!

Faraz Patankar

Probably obsessing over fantasy football, figuring out what to eat for my next meal or working on my next great (soon to be abandoned) side project but hello to you too! 🤗

More posts from this author