Unity
A game has no script tag either, so this is a package: the same conversation and the same inbox, drawn over your game with UI Toolkit and talking to the same public API the website widget does.
1. Install
Unity 2022.3 or newer, on iOS, Android, desktop and WebGL. No dependencies: the UI is built from code, with no prefabs and no TextMeshPro import. In Window → Package Manager, choose + → Add package from git URL… and paste:
https://github.com/helpwing/helpwing-unity-sdk.gitOr pin a version in Packages/manifest.json:
{
"dependencies": {
"ru.fanyagin.helpwing": "https://github.com/helpwing/helpwing-unity-sdk.git#v0.1.0"
}
}2. Configure the project
Two values, both public. The key is the same pk_… the website snippet carries, and shipping it inside a build is expected. Copy yours from the Chat widget screen of your workspace. The API URL is whichever host serves /widget.js.
Nothing needs adding to the allowed origins list: a native build sends no origin at all. The one exception is WebGL, which is a web page — add the origin it is served from, as you would for the website snippet.
3. Set it up once
In your first scene, add an empty GameObject with Helpwing → Helpwing Client — fill in the API URL and project key — and Helpwing → Support UI. A launcher appears in the corner once the project's config has loaded, and opens the chat over the game. Or from code:
using Helpwing;
using Helpwing.UI;
// Once, in your first scene. The client survives scene loads by default.
var client = HelpwingClient.Create("https://api.helpwing.app", "pk_your_project_key");
client.gameObject.AddComponent<SupportUI>();4. Or draw it yourself
The launcher is a convenience. A game with a Support button in its pause menu turns it off and opens the panel itself, or puts SupportChatView inside a screen of its own.
// "Show Launcher" off on Support UI, and your own button in the pause menu.
supportButton.onClick.AddListener(HelpwingClient.Instance.Open);
HelpwingClient.Instance.StateChanged += state =>
badge.text = state.UnreadCount > 0 ? state.UnreadCount.ToString() : "";
// Or the chat inside a UI Toolkit screen of your own.
// While the view is on a panel, the player counts as reading.
myScreen.Add(new SupportChatView(HelpwingClient.Instance, labels));Or skip the components entirely: everything they draw comes from HelpwingClient.Chat, which has no idea how it is drawn. RichText.Render turns a message into the tags TextMeshPro reads, with every < anybody typed escaped.
var client = HelpwingClient.Instance;
client.StateChanged += state =>
{
// Messages, UnreadCount, Typing, Offline, Config — draw them with uGUI or TextMeshPro.
foreach (var message in state.Messages)
{
if (message.Delivery == Delivery.Failed)
_ = client.Retry(message.ClientMessageId); // free: stored once however often it arrives
}
};
// While your own transcript is on screen, and false when it is not.
client.Chat.SetPresent(true);
await client.Send(inputField.text);5. Say who the player is
await HelpwingClient.Instance.Identify(new Identity
{
Id = account.Id,
Email = account.Email,
Name = account.DisplayName,
// Computed by YOUR server. See below.
UserHash = account.SupportHash,
});Compared by value, so calling it on every sign-in only talks to the server when something changed. It applies to a conversation that started before anyone signed in too — the conversation moves onto the player's customer record when they do.
The identity secret never goes in the build
UserHash is an HMAC-SHA256 of the user id, keyed with your project's identity secret, and your server computes it. A game build is a zip file with your code in it, and IL is easy to read. Fetch the hash from your own backend alongside the rest of the signed-in account, the way you would a session token. Identifying users has the full recipe.
// Signing out on a personal device: the next conversation is nobody's yet.
await HelpwingClient.Instance.Identify(null);
// Signing out on a shared device: forget this conversation entirely.
await HelpwingClient.Instance.ResetConversation();Signing straight in as somebody else needs neither: a conversation belongs to whoever opened it, so when the client sees A and then B, A's conversation is dropped from the device and B opens their own.
What it does about a device
- It asks every five seconds while the chat is open. Every thirty while it is closed, so the unread badge stays honest, and not at all while the app is paused. Polling runs off Update on the main thread, so it works on WebGL too.
- A message sent twice is stored once. Every send carries an id, so the SDK retries through a tunnel without duplicates. Opening a conversation is the one exception — a start whose answer was lost is shown as a failed send, and retrying it is the player's decision.
- Unsent messages survive the app being killed. They are kept in PlayerPrefs and go out on the next launch, in the order they were written.
- A reply is not emailed to somebody who is reading it. While the chat is on screen the SDK says so, and the reply is delivered to the game alone.
What it does not do yet
- Push notifications. Nothing arrives while the game is closed. Ask for an email address — require_email on the widget settings screen — and a reply written while the player is away reaches them there.
- Attachments from the player. No client can send one yet. Files an agent attaches are listed by name.
- One conversation across two devices. The token belongs to one installation, so a reinstall starts a new conversation.
Checklist
- One client, in the first scene. It survives scene loads; a second one in a later scene would poll alongside the first.
- The hash comes from your server. Everything in the build is public.
- WebGL needs its origin allowed. Native builds need nothing.
- Ask for an email address. Until push exists, it is the only way to reach somebody who has closed the game.