Tela is a small framework for building Intercom Canvas Kit applications in node. Write your apps as simple classes, run a server to receive calls to your app with one line, and get your canvasses type-checked with TypeScript.
Simply install with npm or yarn. Tela is available on the NPM registry or GitHub Packages.
npm install @efstajas/tela
or
yarn add @efstajas/tela
In Tela, you define Canvas Kit apps as simple classes that match the webhooks generated by Intercom for an app. Afterwards, you can use Tela to serve your apps with one line of code.
π‘ This guide assumes you use TypeScript β everything also works with plain JS too, but syntax may be different. You really should use TypeScript either way!
Create a new file named your-app.app.ts
. Within that, export a new class YourName that implements App
, and return an array consisting of a single text
component:
//your-app.app.ts
import { App, Component } from '@efstajas/tela'
export default class ExampleApp implements App {
public initialize = (body): Component[] => {
return [
{
type: 'text',
text: 'Hello world',
style: 'paragraph'
}
]
}
}
Congratulations, you just implemented your first app. initialize
is a handler
β and initialize specifically is required, but you can also add configure
and submit
handlers if you need them. For your convenience, these handlers are a bit magic: Instead of you handling response sending and canvas creation manually in each handler, you simply return either an array of Components or a Promise that resolves to an array of Components directly. Tela will automatically take care of wrapping your components to create a valid canvas definition and send it back to Intercom once your handler has resolved. Anyway β let's run our new app!
In a new file index.ts
, import your app, create a new Tela instance, and register your app:
//index.ts
import Tela from '@efstajas/tela'
import YourApp from './your-app.app.ts'
const tela = new Tela()
tela.registerApp('example-app', new YourApp())
The first argument to registerApp
is your app name β please make sure it's a URL-safe string, since it'll be used as a path for the server later. Please make sure you pass a new instance of your app, not the class itself!
Now that our app is registered, we can go ahead and start our server:
tela.listen(8000)
Go ahead and run the script β congratulations, your server is now listening on port 8000. Try calling POST /example-app/initialize
, and you'll see that you receive back a valid canvas defininion with the text
component you defined in your initialize
handler.
To get your app up and running in Intercom, go read the official Intercom Canvas Kit documentation. It's quite simple!
In each app you can define up to three handlers: initialize
, submit
and configure
. To understand what they're for, it's best to read Intercom's documentation. In addition to a synchronous handler like in the Getting Started guide, you can also create async handlers, for example if you need to get some data from an API to create your components:
//your-app.ts
import { App, Component } from '@efstajas/tela'
export default class ExampleApp implements App {
public initialize = async (body): Promise<Component[]> => {
const {
customer
} = body
const userId = customer.id
const userData = await someApiService.getUser(userId)
return [
{
type: 'text',
text: `The user's favorite color is ${userData.favoriteColor}.`,
style: 'paragraph'
}
]
}
}
If you need to read information that Intercom sends with the requests, worry not: The first argument for your handler is Intercom's request body
.
Sometimes, you want to perform some logic after every incoming request and pass down some data to individual handlers. For that, you can use the registerMiddleware
function.
Let's say for example that we want to parse Intercom's locale
context value from the incoming request, initialize an i18next instance for localization, and then pass it into each handler of our app for convenient usage.
tela.registerMiddleware(async (req, middlewareContext) => {
const body = req.body
const {
context: intercomContext
} = body
const browserLanguage = (intercomContext && intercomContext.locale) || 'en'
const t = await i18n(browserLanguage)
return {
...middlewareContext,
t
}
})
As you can see, registerMiddleware
accepts any handler (promise or synchronous function). Your middleware gets the full express req
object, as well as the previous' middleware's middlewareContext
which will include what was returned by the previous middleware in the chain.
Simply perform your logic and return an object that contains all previous middleware context and the new context added by this middleware handler. If you call registerMiddleware
multiple times, all handlers will be executed for each incoming request.
Within your app's handlers, you can now find your middlewareContext
as part of the context
argument.
//your-app.ts
import { App, Component } from '@efstajas/tela'
export default class ExampleApp implements App {
public initialize = async (body, context: HandlerContext): Promise<Component[]> => {
const {
middlewareContext
} = handlerContext
const { t } = middlewareContext
return [
{
type: 'text',
text: t('translation.key')
style: 'paragraph'
}
]
}
}
If you need to send Intercom stored data values and / or a content URL for Live Canvasses in addition to components to construct a view, you can return the more verbose HandlerResult
or a Promise resolving to a HandlerResult
instead:
//your-app.ts
import { App, HandlerResult } from '@efstajas/tela'
export default class ExampleApp implements App {
public initialize = async (body): Promise<HandlerResult> => {
return {
components: [ /* Your view⦠*/ ],
storedData: {
foo: 'bar'
},
contentUrl: '' // Your Live Canvas Content URL
}
}
}
components
is of course required, while storedData
and contentUrl
are optional.
Alongside the request body
passed from Intercom, your handler also receives a context
object as the second argument. The context includes the current app name your handler is running in, the app's base endpoint path along with two objects hooks
and methods
, which you can use to find out the app's other handler's endpoints at runtime.
public initialize = (requestBody, context: HandlerContext) => {
const {
endpoint,
appName,
hooks,
methods
} = context
console.log(`
This handler's path is ${endpoint}.
It's part of app ${appName}.
`)
/*
The hooks and methods objects are helpful for registering a webhook
with one of your handlers at runtime, for example.
Let's say that in response to an action on Intercom you want to make an
API call that establishes a webhook to a handler in this app:
service.createWebhook({
url: `${hostname}${hooks.hookName.endpoint}`
})
*/
return [
{
type: 'text',
text: 'Hello world',
style: 'paragraph'
}
]
}
Often-times, you'll need to listen to external webhooks other than those for Canvas Kit and perform some action in response. You can define external webhook handlers in a public hooks
object:
public hooks = {
hookName: (req, res, next, context) => {
console.log(`Handling incoming webhook at ${context.endpoint}`)
res.send(200)
next()
}
}
These handlers are defined as standard Express middleware. Each defined hook will be initialized at /appname/hookname*
(Note the * β that means that a webhook coming in at /appname/hookname/foobar
will still hit your handler).
Of course, you can run multiple apps at the same time by calling registerApp
multiple times before calling listen
. Each app will be initialized at /appname/handlername
. registerApp
also returns a Promise that resolves to a context
object, including the paths for all handlers that were initialized within your app.
import Tela, { HandlerContext } from '@efstajas/tela'
// Import your apps
import apps from './apps'
const tela = new Tela()
let promises: Promise<void>[] = []
apps.forEach((app) => {
promises.push(
tela.registerApp('test', new App())
.then((context) => {
console.log('App initialized', context)
})
.catch((e) => {
console.error(e)
})
)
})
await Promise.all(promises)
console.log('All apps initialized, starting serverβ¦')
tela.listen(8000).then(() => {
console.log(`Listening at 8000`)
}).catch((e) => {
console.error(e)
})
If you need to add your own endpoints not part of an Intercom app to the internal server, like for example a /health
endpoint, you can access the internal Express server instance directly. Please note that you should do this and registering apps only before calling listen
.
import Tela from '@efstajas/tela'
import App from './app'
const tela = new Tela()
tela.expressInstance.get('/health', (req, res, next) => {
res.send(200)
next()
})
tela.listen(8000).then(() => {
console.log(`Listening at 8000`)
}).catch((e) => {
console.error(e)
})