Get started with the JS UI kit + .NET
This getting started guide is a fully functional .NET web application with several Weavy apps integrated. In addition to the apps, this guide will also showcase how to handle authentication, sync user data, register webhooks to listen for notifications, and other useful things. The main goal when we created this guide was to have a near real life example of how to integrate Weavy into a web application.
See also: JS UI kit reference
Getting the code
Start by cloning the repo from https://github.com/weavy-labs/weavy-js-dotnet.
Sign up for a Weavy account and create a new Weavy environment.
Sign in to your account and create a new API key for your Weavy environment. You'll need it later.
Create an
appsettings.json
file in the root folder and add the following:{ "Weavy": { "Server": "url to your weavy environment", "ApiKey": "your api key" } }
Populate the
Server
property with the url to your Weavy environment and theApiKey
property with the API key you just created.Run
npm install && npm run build
in the root folder to build the css and js.Run
dotnet run
to start the site on https://localhost:7059
The application
So, what is this Acme website? You should think of it as the application where you want to add the functionality that Weavy offers. This could be your product, intranet or whatever web application you are building. We created the Acme website to be able to add Weavy to a "real" application. Of course, it's still a very basic example, but the important stuff here is what actually happens when a user signs in to the website and how the Weavy app components are initialized and displayed.
Building blocks
These are the building blocks of the website and where Weavy is integrated in some way. Either as one of the ready-to-use apps from the JS UI kit, or as an API request to the Weavy API. We want to show both.
Building block | Acme website | Weavy |
---|---|---|
Login page | Authenticates a user against the Acme local db (sqlite). | After login to the Acme website, the user data is synced to Weavy |
Top navigation | Display notifications and switch light/dark mode | Notifications from Weavy. A global Weavy Messenger component |
Users list | Display all the users in the Acme website db. Edit the user. |
After updating an Acme user, the user data is synced to Weavy |
Menu/Pages | Each of the pages under Weavy contains a Weavy JS UI kit app which displays each of the Weavy apps available; Chat, Posts and Files. |
|
Examples | Example to make a request to the Web API | |
Realtime | A SignalR connection from the .NET server to the frontend. Used to send a notification to the frontend when an incoming webhook notification is delivered from the Weavy environment |
Users in the Acme website and Weavy
One concept that is important to understand is how the users in the host application, in this case the Acme website, and Weavy correlate. The users that you have in your application are managed by your application. But Weavy also need an user account for each host application user in order to be able to identify which user is performing any interaction with a Weavy app or the api. This is accomplished by supplying a Bearer token for each request.
When you are using the JS UI kit, all of this is taken care of by the Weavy
instance that you create. What you need to supply is a function returning a valid Bearer token for the host application user.
You'll learn more about this in the Authentication section below.
Authentication
Let's begin with some code to illustrate the authentication process. This code snippets are taken from the \scripts\weavy.js
file.
import { Weavy } from '@weavy/dropin-js';
// configure weavy
Weavy.url = weavy_url
Weavy.tokenFactory = async (refresh) => {
var response = await fetch('/token?refresh=' + (refresh || false));
return await response.text();
}
Weavy.tz = user_timezone || '',
\scripts\weavy.js
So what's going on here. First, we have defined a tokenFactory
function that's making a request to the .NET server side api to get a valid Bearer token. Remember that the request for a new Weavy token should always be made server-to-server. Never from your frontend UI!
The api endpoint on the .NET server looks like this:
[HttpGet("~/token")]
public async Task<IActionResult> GetToken(bool refresh) {
var accessToken = await _weavy.GetToken(User, refresh);
return accessToken != null ? Content(accessToken) : BadRequest();
}
\Controllers\UsersController.cs
The endpoint gets a token from the GetToken
function:
public async Task<string> GetToken(ClaimsPrincipal user, bool refresh) {
var id = user.Id();
ArgumentNullException.ThrowIfNull(nameof(id));
// check local token store for access_token
var accessToken = _tokenStore.GetToken(id.Value);
if (accessToken == null || refresh) {
// no token in storage (or invalid token)
var uid = user.Guid();
_logger.LogDebug("Requesting access_token for {uid} ", uid);
// request a new access_token from the Weavy environment (passing in token creation options is optional, but can be used to set lifetime of the created access_token)
var response = await _httpClient.PostAsJsonAsync($"/api/users/{HttpUtility.UrlEncode(uid)}/tokens", new { ExpiresIn = 7200 }, options: _jsonSerializerOptions);
if (response.IsSuccessStatusCode) {
var resp = await response.Content.ReadFromJsonAsync<TokenResponse>(options: _jsonSerializerOptions);
accessToken = resp.AccessToken;
// save token in our local storage
_tokenStore.SaveToken(id.Value, resp.AccessToken);
}
}
// return access_token
return accessToken;
}
The code may seem a lot, so let's break it down.
First of all, we get the current signed in user id:
var id = user.Id();
Check if we already have a token for the user:
var accessToken = _tokenStore.GetToken(id.Value);
All of the apps in the JS UI kit will send true/false to the tokenFactory
. This indicates that a request to the Weavy api from a component failed and a new token needs to be created. This will happen when a Bearer token is expired or revoked. Checking if the token needs a refresh
is also convenient for you as you don't need to make a request to the Weavy environment each time asking for a new token. In the example above, we have a _tokenStore
containing all the users tokens (in memory only). If the refresh
parameter is false, we just get it from the token store, otherwise, ask the Weavy environment for a new one.
var uid = user.Guid();
var response = await _httpClient.PostAsJsonAsync($"/api/users/{HttpUtility.UrlEncode(uid)}/tokens", new { ExpiresIn = 7200 }, options: _jsonSerializerOptions);
If we need to get a new token, a request is made to the Weavy environment api endpoint /api/users/[unique id]/tokens
. Note that the Weavy API key is supplied as the Bearer token. This is set on the _httpClient
. The unique id
is the important thing here. This is unique id for the signed in user that Weavy uses when creating the user. This could be the user's username, id or whatever. In the Acme website, the users uid
is a Guid.
If the user does not exist in Weavy, the user is created. You can supply more info about the user, such as name, email etc. In the Acme website application, we don't need to do this when getting a token. Instead, we make sure the user is always synced with the correct data when signing in and updating a user. More on this in the Sync Acme user data to Weavy section below.
Sync Acme user data to Weavy
Now that you have learned how you can authenticate a user and get a token, let's check out how we can keep the Acme user's data in sync with the Weavy data. After all, when the user interacts with a Weavy app, for example the Posts app, we want to make sure the current user profile info is displayed correctly.
In the Acme website application, we decided to do this sync in three different places. When the user signs in the the website, when the user updates the profile and when a user is edited/updated from the users list.
After login
The \Controllers\UsersController.cs
handles the authentication process. When a user signs in, the Login
Action is called and validates the user against the Acme database. If successful, we can use the current user object to sync the user data to Weavy.
// sync user to Weavy in the background
_weavy.SyncUser(user).FireAndForget(_logger);
public async Task<UserResponse> SyncUser(User user) {
var uid = user.Guid.ToString();
_logger.LogDebug("Syncing user {uid} to Weavy", uid);
var profile = new UserModel {
Name = user.Name,
Email = user.Email,
PhoneNumber = user.Phone,
//Picture = "",
Directory = "Acme"
};
var response = await _httpClient.PutAsJsonAsync("/api/users/" + HttpUtility.UrlEncode(uid), profile, options: _jsonSerializerOptions);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<UserResponse>();
}
To sync the user's data, we can make a request to an endpoint in the Weavy api, /api/users/[unique user id]
. We supply the user data we want to update in the body. In this case we want to update the name, email, phone and directory. The directory
is optional, but in this case we want to add all the Acme users into a director called ACME
. You can group users in Weavy by adding them to a specific directory.
If the user does not exist, the user is created in Weavy. So the first time an Acme user logs in to the Acme web application, the user will also be created in Weavy.
We use the same SyncUser(User user)
function when a user updates its profile and when a user is updated from the Users list. So we make sure the correct user info always is in sync with Weavy.
Adding apps
Now it's time to add som Weavy apps to the Acme website. You can check them out under the Weavy
section in the left hand side menu. All of the Chat
, Feed
and Files
pages contains a Weavy app added from the JS UI kit.
These apps are so called Contextual apps
. These apps is meant to belong to a specific context in your application and requires you to specify a unique id when you add them. This could for example be for a specific product page, a user, a project and so on. The unique id is something you decide what it's going to be. Let's say you you are on a project page for the project My project
. This page has a unique identifier in you application called project-1
. The unique id for a Chat could them for example be chat-project-1
.
Before you can display a contextual app, the app must already be created in Weavy. In addition to that, the user in the host application, in this case the Acme website, must also me a member
of the Weavy app. The Weavy api exposes an endpoint to handle all of this. Let's take a look at the Chat page for example:
[HttpGet("chat")]
public async Task<IActionResult> Chat() {
// init Weavy chat app and ensure authenticated user is member
var app = new AppModel {
Type = "chat",
Uid = "acme_chat",
Name = "Chat",
};
var model = await _weavy.InitApp(app, User);
return View("App", model);
}
The InitApp
function is called with the specific app data.
public async Task<AppResponse> InitApp(AppModel app, ClaimsPrincipal user) {
_logger.LogDebug("Initializing app {uid} ", app.Uid);
// the init endpoint accepts an optional user to add as member
var member = new { Uid = user.Guid() };
var response = await _httpClient.PostAsJsonAsync("/api/apps/init", new { App = app, User = member }, options: _jsonSerializerOptions);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<AppResponse>(options: _jsonSerializerOptions);
}
In the body, we specify the app we want to initialize, and the user we want to add to the app as a member.
Data | Type | Description |
---|---|---|
app | ||
uid | string | The unique identifier for the app, for example chat-project-1 |
name | string | A title for the app |
type | string | The type of Weavy app to initialize. One of chat , posts or files |
user | ||
uid | string | The unique identifier of the user to add as a member |
The /api/apps/init
endpoint will create the app if it doesn't exist, otherwise it will return the existing one. If the user isn't already a member of the app, the user will be added.
On the frontend of the Acme website, the app may be loaded in two ways. Either you can load it via javascript or via HTML.
In this example, we are loading the contextual apps via HTML in the App view at Views/Home/App.cshtml
.
<div class="contextual-app">
<weavy-chat uid="@Model.Uid" />
</div>
At this time, the app has been initialized in the Controller action when loading the page.
If you set an
uid
to the Files, Chat or Posts app components that doesn't exist in Weavy, you will get a 404 error when trying to display the app.
The Weavy Messenger
The Messenger
app is a little bit different than the Chat
, Posts
and Files
components. The Messenger is not a contextual app. You can think of it more like a global messenger where users in a directory can create private or room conversations. The Messenger component is available in the Acme website by clicking on the Messenger icon in the top right corner in the top navigation.
The Messenger component is always available in Weavy and is not needed to be created before hand. If you take a look at the \scripts\weavy.js
file, you can see that no special initialization is done on the server side.
The Messenger may also be created via HTML, but in this example we demonstrate how to create it via javascript.
// get DOM element where we want to render the Weavy messenger app
const messengerContainer = document.getElementById("messenger");
// create and append messenger
const messenger = new Messenger({
load: false, // app is initially unloaded
});
// Set dark theme className for the messenger
if (document.documentElement.dataset.bsTheme === 'dark') {
messenger.classList.add("wy-dark")
}
// Add the messenger to the DOM
messengerContainer.append(messenger);
// load messenger when container DOM element (bootstrap off-canvas) is shown
messengerContainer.addEventListener('show.bs.offcanvas', event => {
messenger.load();
});
Webhooks and notifications
The webhooks in Weavy allow you to build integrations that subscribe to certain events happening in the Weavy environment. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. You can for example use webhooks to synchronize user and profile information between systems, create reports, send notifications and more.
In the Acme website, we are going to use the webhooks to show a list of notifications to the user when something relevant happens in the Weavy component apps. For example when someone post something in the feed or uploads a new file in the Files app.
Setup
First of all we need to create the desired webhook in Weavy. You can easily do this by making a request to the Weavy api.
curl -H "Authorization: Bearer {WEAVY_APIKEY}" -H "Content-Type: application/json" {WEAVY_SERVER}/api/webhooks -d "{'payload_url': 'http://localhost:7059/api/webhooks', 'triggers': ['notifications']}"
This will create a webhook that will post to the endpoint http://localhost:7059/api/webhooks
(this is an endpoint in the host application, in this case the Acme website that should receive the webhook) whenever a notification in Weavy is created, updated or marked as read/unread.
The \Controllers\WebhooksController.cs
handles the incoming webhook:
public async Task<IActionResult> HandlePayload(){
...
}
On a notification_created
action, the messages is sent to the frontend using a SignalR Hub.
switch (action.GetString()) {
case "notification_created":
if (json.RootElement.TryGetProperty("notification", out var notification) && notification.TryGetProperty("user", out var user)) {
// push notification id to user (on a background thread to avoid blocking since Weavy expects a reply within 10 seconds)
var notificationId = notification.GetProperty("id").GetInt32();
var userId = user.GetProperty("uid").GetString();
_hub.Clients.User(userId).SendAsync("notification", notificationId).FireAndForget();
}
break;
}
On the frontend, we receive the SignalR message and display a toast:
// connect to signalR hub for realtime events
var connection = new signalR.HubConnectionBuilder().withUrl('/hub').build();
// listen to notification event
connection.on('notification', function (id) {
console.log('received notification:', id);
// display notification to user
Weavy.fetch(`/api/notifications/${id}`).then(data => {
console.log('notification', data.text);
const toastContainer = document.getElementById('toasts');
const toastTemplate = document.getElementById('toast');
if (toastContainer && toastTemplate) {
const clone = toastTemplate.content.firstElementChild.cloneNode(true);
const toastBody = clone.querySelector('.toast-body');
toastBody.innerText = data.plain;
toastContainer.appendChild(clone);
const toast = new bootstrap.Toast(clone);
toast.show();
}
});
});
scripts\weavy.js