Library for integrating Queue-it's virtual waiting room into a HarmonyOS Next application built with ArkTS and ArkUI.
This SDK is for the HarmonyOS Next ecosystem (API version 12 and higher).
Two sample integrations are available within the entry module of the demo application:
DemoPage.ets: A demo page with a button for each SDK method. BesidestryPass()andshowQueue()it still has buttons for the legacyrun()method; follow this README for the recommended integration.DemoWithProtectedAPIPage.ets: Shows how to handle API endpoints that are protected by a Queue-it KnownUser (connector) implementation. Its queue handling still uses the legacyrun()method; for the waiting room part, usetryPass()andshowQueue()as described in Integration with Connector Protected APIs.
Before starting, please download the "Mobile App Integration" whitepaper from the GO Queue-it Platform. This document contains essential information for a successful integration.
To use the SDK, you can run following command to install dependency:
ohpm install queueit-libraryOr you can download .har (Harmony Archive) file from releases section and as a local dependency to your application.
-
Add Dependency: Copy the
.harfile into your project (e.g., into alibsfolder) and reference it in your application'soh-package.json5.// In your application's oh-package.json5 "dependencies": { // ... other dependencies "queueit-library": "file:libs/queueit-library.har" }
Add the following permissions to your application's src/main/module.json5 file.
// src/main/module.json5
{
"module": {
// ... other module properties
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_permission_reason",
"usedScene": {
"abilities": [ ".EntryAbility" ],
"when": "inuse"
}
},
{
"name": "ohos.permission.GET_NETWORK_INFO",
"reason": "$string:network_permission_reason",
"usedScene": {
"abilities": [ ".EntryAbility" ],
"when": "inuse"
}
}
]
}
}Remember to define internet_permission_reason and network_permission_reason in your src/main/resources/base/element/string.json file.
As the app developer, you must manage the state (i.e., whether the user was previously queued up). After receiving the onQueuePassed callback, your app should store this state, potentially with a date/time expiration. When the user navigates to a protected part of the app, your code should check this state and only trigger the Queue-it logic if the user has not recently passed the queue.
The integration uses two methods of QueueItEngine:
tryPass()checks the status of the waiting room and returns aQueueTryPassResult.showQueue(result)shows the waiting room to the user, when the result says the user must queue.
Between the two calls your app decides when and where the waiting room is shown, and can change the queue URL if needed (see Modify the queue URL before showing the waiting room).
flowchart TD
start([User opens a protected screen]) --> hasPass{Valid Queue-it pass<br/>stored by the app?}
hasPass -- Yes --> show[Show the protected screen]
hasPass -- No --> tryPass["engine.tryPass()"]
tryPass -- Request failed --> failed[Decide: let the user in<br/>or try again later]
tryPass -- QueueTryPassResult --> result{redirectType}
result -- "Not queue: safetynet, disabled,<br/>idle, after event (queueittoken)" --> store[Store the pass<br/>with an expiry]
result -- queue --> modify["Optional: modify the queue URL<br/>(e.g. add an invite code)"]
modify --> showQueue["engine.showQueue(result)"]
showQueue --> wr[[Waiting room in WebView]]
wr -- "onQueuePassed (queueittoken)" --> store
wr -- Session restart --> tryPass
wr -- Closed or error --> back[Go back]
store --> show
The SDK uses HarmonyOS's Navigation component to present the queue page. Your page must be set up to host a Navigation container and provide the SDK with its NavPathStack.
The integration steps are:
- Wrap your page content in a
Navigationcomponent, providing it with an@State NavPathStack. - When initializing
QueueItEngine, pass thisNavPathStackinstance to its constructor. - Define a destination builder for the
Navigationcomponent that maps the route name'QueueView'to the SDK's<QueueView>component.
Here is a complete example of the UI setup:
// In your page
import { QueueItEngine, QueueITViewManager, QueueView } from 'queueit-library';
@Component
struct MyPage {
// 1. Navigation state for your page
@State navPathStack: NavPathStack = new NavPathStack();
// 2. The SDK components. The view manager must be @State: QueueView links to it.
private engine: QueueItEngine | null = null;
@State queueViewManager: QueueITViewManager | null = null;
async initializeSdk() {
// ... (code to get context, define listener, etc.)
// 3. Pass your NavPathStack to the engine
this.engine = new QueueItEngine(
context,
customerId,
eventId,
listener,
this.navPathStack // Pass the stack here
);
this.queueViewManager = this.engine.getWebViewManager();
}
// 4. Define the destination builder
@Builder queueItMap(name: string) {
if (name === 'QueueView') {
// The view manager must be linked with '$'
QueueView({ viewManager: $queueViewManager })
}
}
build() {
NavDestination() {
// 5. Use the Navigation component as the container
Navigation(this.navPathStack) {
Column() {
// Your page content (buttons, text, etc.) goes here
Button('Enter Event').onClick(() => this.enterEvent()) // See section 2
}
}
.navDestination(this.queueItMap) // Attach the builder
.mode(NavigationMode.Auto)
}
}
}When you call showQueue(), the SDK pushes the QueueView onto your navPathStack.
First check the user's status with tryPass(), then show the queue page with showQueue().
// In your page
import {
QueueItEngine,
QueueListener,
QueuePassedInfo,
QueueDisabledInfo,
QueueError,
QueueTryPassResult,
QueueITApiClient
} from 'queueit-library';
import common from '@ohos.app.ability.common';
import promptAction from '@ohos.promptAction';
// ... inside your @Component struct (navPathStack, engine and queueViewManager as in section 1;
// customerIdInput, eventIdInput and isTestEnvironment are your own fields)
@State isLoadingSdk: boolean = false;
// 1. Define the listener to handle callbacks from the SDK
private createQueueListener(): QueueListener {
return {
onQueuePassed: (info: QueuePassedInfo) => {
this.isLoadingSdk = false;
this.queueViewManager?.hideQueue();
// The user may continue: either the user's turn came in the waiting room, or tryPass()
// returned a result that is not RedirectType.Queue (see below).
// Store the pass (e.g. with an expiry) and navigate to your protected content.
promptAction.showToast({ message: `Queue Passed! Token: ${info.queueItToken}` });
},
onQueueViewWillOpen: () => {
this.isLoadingSdk = false;
},
onQueueDisabled: (info: QueueDisabledInfo) => {
// Required by the interface, but not called in the tryPass() + showQueue() flow (see below)
this.isLoadingSdk = false;
},
onQueueItUnavailable: () => {
this.isLoadingSdk = false;
this.queueViewManager?.hideQueue();
promptAction.showToast({ message: 'Queue-it is unavailable. Please try again later.' });
},
onError: (error: QueueError, errorMessage: string) => {
this.isLoadingSdk = false;
this.queueViewManager?.hideQueue();
promptAction.showToast({ message: `Critical error: ${errorMessage}` });
},
onWebViewClosed: () => {
// The user closed the waiting room
this.isLoadingSdk = false;
},
onSessionRestart: () => {
// The user restarted the session from the waiting room: check the status again
this.enterEvent();
}
};
}
// 2. Initialize the engine
private initializeSdkEngine(): void {
const context = this.getUIContext()?.getHostContext() as common.Context;
// Remember to pass the NavPathStack
this.engine = new QueueItEngine(
context,
this.customerIdInput,
this.eventIdInput,
this.createQueueListener(),
this.navPathStack
);
this.queueViewManager = this.engine.getWebViewManager();
}
// 3. Check the waiting room status, then show the queue page if needed
async enterEvent(): Promise<void> {
if (!this.engine) {
this.initializeSdkEngine();
}
QueueITApiClient.isTest = this.isTestEnvironment;
this.isLoadingSdk = true;
// Or tryPass(enqueueToken) / tryPass(undefined, enqueueKey)
const result: QueueTryPassResult | null = await this.engine!.tryPass();
if (!result) {
// The request failed. The SDK has called onError or onQueueItUnavailable,
// except when another request is already in progress (then no callback is made).
this.isLoadingSdk = false;
return;
}
// RedirectType.Queue: the user must queue. showQueue() pushes the QueueView onto the
// navigation stack. You can modify the queue URL first, see section 3.
// Any other result (safety net, disabled, idle, after event, ...) carries a queueittoken:
// showQueue() does not show the queue page and calls onQueuePassed instead.
this.engine!.showQueue(result);
}isPassedThrough only tells you that the result has a queueittoken. A disabled, idle or after-event waiting room also returns a token, so it reaches onQueuePassed and onQueueDisabled is not called in this flow. If you need to tell these cases apart, check result.redirectType yourself instead of passing a non-queue result to showQueue().
hideQueue() is used to close the queue page webview programmatically.
In the demo, we call it within the callbacks of createQueueListener() to ensure the queue view is dismissed before navigating users to the target page.
After tryPass() returns a result with redirectType RedirectType.Queue, and before you call showQueue(), you can change the queue URL. For example, to send the visitor to an invite-only waiting room, add the invite code as the code query parameter. The SDK has no separate invite-code setting, so this is how you pass an invite code on HarmonyOS.
QueueTryPassResult is read-only. Create a new QueueTryPassResult with the same values and the updated queueUrl, and pass it to showQueue(). showQueue() loads the URL as given.
import { QueueTryPassResult, RedirectType } from 'queueit-library';
function withInviteCode(result: QueueTryPassResult, inviteCode: string): QueueTryPassResult {
const queueUrl: string | null = result.queueUrl;
if (!queueUrl || queueUrl.includes('?code=') || queueUrl.includes('&code=')) {
// No queue URL, or the URL already has an invite code: keep the result as it is
return result;
}
let separator: string = '&';
if (!queueUrl.includes('?')) {
separator = '?';
} else if (queueUrl.endsWith('?') || queueUrl.endsWith('&')) {
separator = '';
}
return new QueueTryPassResult(
result.queueItToken,
`${queueUrl}${separator}code=${encodeURIComponent(inviteCode)}`,
result.targetUrl,
result.isPassedThrough,
result.redirectType,
result.urlTTLInMinutes
);
}
// In enterEvent() (section 2), instead of this.engine!.showQueue(result):
this.engine!.showQueue(
result.redirectType === RedirectType.Queue ? withInviteCode(result, inviteCode) : result
);Only modify a result with redirectType RedirectType.Queue, and still pass every other result to showQueue(), so that onQueuePassed is called for it.
Only add or change query parameters. Keep the scheme, host and path of the queue URL: the SDK uses the queue host to decide which pages stay inside the waiting room WebView.
The SDK handles back button presses while the QueueView shows the waiting room, based on the disableBackButtonFromWR option in QueueItEngineOptions:
true(default): the back press is ignored, so the user stays in the waiting room.false: the back press is not consumed, andNavigationpops the queue page from your navigation stack.
No additional onBackPressed code is required from the consuming page.
You can configure the engine by passing an options object to the constructor.
import { QueueItEngine, QueueItEngineOptions } from 'queueit-library';
const options = new QueueItEngineOptions();
// Disables the hardware/software back button while the queue view is shown
options.disableBackButtonFromWR = true;
// Set a delay in milliseconds before the queue view is displayed
options.viewDelayMs = 500;
// layoutName and language are optional
const engine = new QueueItEngine(context, customerId, eventId, listener, navPathStack, undefined, undefined, options);QueueItEngineOptions also has a webViewUserAgent field, but the SDK does not apply it yet: the WebView always uses its default user agent with the SDK version added.
If your application calls an API protected by a Queue-it connector (KnownUser), you need to handle potential redirects from the API layer. DemoWithProtectedAPIPage.ets shows the API client and error handling; its queue handling still uses the legacy run() method, so follow the steps below for the waiting room part.
The flow is as follows:
- Make API Request: Your app makes a standard API request.
- Catch
MustBeQueuedError: If the user needs to be queued, your API client should throw a specific error containing the redirect URL from thex-queueit-redirectheader. - Handle Redirect: In your UI logic, catch this error.
- Parse IDs: Extract the Customer ID (
c) and Event ID (e) from the redirect URL. - Show the Waiting Room: Instantiate a new
QueueItEnginewith the parsed IDs and your page'sNavPathStack. CalltryPass(), then pass the result toshowQueue(). ForRedirectType.Queueit shows the queue view; any other result already carries a queueittoken, andshowQueue()callsonQueuePassedwith it. - Retry API Call: In the
onQueuePassedcallback, store the newqueueItTokenand retry the original API request. This time, your API client should include the token with the request.
sequenceDiagram
participant App
participant API as Your API (Queue-it connector)
participant SDK as Queue-it SDK
participant WR as Waiting room (WebView)
App->>API: Request with x-queueit-ajaxpageurl header<br/>(+ Queue-it cookie, if stored)
alt No x-queueit-redirect header
API-->>App: API response
App->>App: Store the Queue-it cookies
else x-queueit-redirect header
API-->>App: 200 OK with x-queueit-redirect
App->>App: Extract c (customer ID) and e (waiting room ID)
App->>SDK: new QueueItEngine(context, c, e, listener, navPathStack)
App->>SDK: engine.tryPass()
SDK-->>App: QueueTryPassResult
alt redirectType is not queue (safetynet, disabled, idle, after event)
Note over App,SDK: queueittoken is in the result
else redirectType is queue
opt Modify the queue URL (e.g. add an invite code)
App->>App: Build a result with the updated queue URL
end
App->>SDK: engine.showQueue(result)
SDK->>WR: Open the waiting room
WR-->>SDK: User's turn
SDK-->>App: onQueuePassed(queueittoken)
end
App->>API: Repeat the request with ?queueittoken=...
API-->>App: API response + Queue-it cookies
App->>App: Store the Queue-it cookies
end
Your API client throws an error type of your own when the response has the x-queueit-redirect header, for example:
// MustBeQueuedError.ets
export class MustBeQueuedError extends Error {
public redirectUrl: string;
constructor(redirectUrl: string) {
super(`Must be queued. Redirect to: ${redirectUrl}`);
this.redirectUrl = redirectUrl;
this.name = 'MustBeQueuedError';
}
}Then, in your page:
import {
QueueItEngine,
QueueListener,
QueuePassedInfo,
QueueDisabledInfo,
QueueError,
QueueTryPassResult
} from 'queueit-library';
import { MustBeQueuedError } from './MustBeQueuedError';
import common from '@ohos.app.ability.common';
import url from '@ohos.url';
// ... inside your @Component struct (navPathStack and queueViewManager as in section 1).
// productRepository is your API client: it adds the queueittoken to the next request.
async fetchProduct(): Promise<void> {
try {
const product = await this.productRepository.getProduct();
// Handle successful fetch
} catch (e) {
if (e instanceof MustBeQueuedError) {
// Trigger the queueing logic
await this.handleQueueRedirect(e.redirectUrl);
} else {
// Handle other errors
}
}
}
private async handleQueueRedirect(redirectUrl: string): Promise<void> {
try {
const parsedUrl = url.URL.parseURL(decodeURIComponent(redirectUrl));
const customerId: string | null = parsedUrl.params.get('c');
const eventId: string | null = parsedUrl.params.get('e');
if (!customerId || !eventId) {
// Not a valid Queue-it redirect
return;
}
const context = this.getUIContext()?.getHostContext() as common.Context;
// Create a temporary engine to handle this specific redirect
const redirectEngine = new QueueItEngine(context, customerId, eventId,
this.createQueueRedirectListener(), this.navPathStack);
// Assign its manager to the UI state to make the QueueView's NavDestination work
this.queueViewManager = redirectEngine.getWebViewManager();
const result: QueueTryPassResult | null = await redirectEngine.tryPass();
if (result) {
// RedirectType.Queue: shows the queue view (you can modify the queue URL first, see section 3).
// Any other result carries a queueittoken: showQueue() calls onQueuePassed instead.
redirectEngine.showQueue(result);
}
} catch (e) {
// Handle parsing errors
}
}
// The listener for this flow
private createQueueRedirectListener(): QueueListener {
return {
onQueuePassed: (info: QueuePassedInfo) => {
// Add the new token to the repository for the next API call, then retry the original request
this.productRepository.addQueueToken(info.queueItToken);
this.fetchProduct();
},
onQueueViewWillOpen: () => {},
onQueueDisabled: (info: QueueDisabledInfo) => {
// Not called in the tryPass() + showQueue() flow
},
onQueueItUnavailable: () => {
this.queueViewManager?.hideQueue();
// Decide whether to let the request through or show an error
},
onError: (error: QueueError, errorMessage: string) => {
this.queueViewManager?.hideQueue();
// Show an error
}
};
}This SDK is licensed under the MIT License. See the LICENSE file for details.