Skip to content

Host Bridge

The game page runs in your App’s WebView and talks to the App through a small JSBridge. It follows the BAISHUN 1.3.2 JSBridge format that most voice-chat apps already implement, so if you embed other H5 games today you probably have it.

Open the game at its entry URL (see Game catalogue):

https://sandbox.sentosatower.com/games/<gameId>/index.html?op=<operatorId>
https://games.sentosatower.com/games/<gameId>/index.html?op=<operatorId>
Method Called by the page as Your App should
getConfig Android / Flutter: NativeBridge.getConfig(JSON.stringify({ jsCallback }))
iOS: webkit.messageHandlers.getConfig.postMessage({ jsCallback })
Fetch a fresh launch code, then call window[jsCallback]("<config JSON string>")
gameLoaded NativeBridge.gameLoaded() / messageHandlers.gameLoaded.postMessage({}) First frame is ready: hide your loading view
gameRecharge same pattern Open your top-up page (balance too low, or the player tapped the balance)
destroy same pattern Close the game and release the WebView

Optional extensions — ignored silently if you do not implement them. The page passes one JSON string argument:

Method Payload Use
haptic {"type":"light" | "success" | "heavy"} Vibrate
roomEvent {"type":"bigwin","gameId","name","amount","roundId"} Announce a big win in the room
displayMode {"mode":"half" | "full","height"?} The player switched half / full screen (e.g. adjust touch pass-through)

App → page: after a successful top-up call window.walletUpdate({}); the game refreshes the balance from the platform.

Field Required Description
code yes Launch code: opaque, one-time, valid ≤ 5 minutes, issued by your backend. The platform exchanges it via POST /players/authenticate.
userId recommended Your player id (display and troubleshooting; identity comes from authenticate)
roomId optional Current voice-chat room, echoed as room_id on transactions
language recommended ISO language code (en, ar, zh-CN, …) or BAISHUN numeric code
gameMode optional 2 half screen, 3 full screen
safeHeight, safeHeightUnit optional Usable height in half-screen mode; unit css (CSS pixels) or design750 (pixels on a 750-wide design)
appId, appChannel, gameConfig, gsp optional BAISHUN compatibility fields, accepted and ignored
{ "code": "lc_7Qm2…", "userId": "84021734", "roomId": "8880085", "language": "ar", "gameMode": 2, "safeHeight": 420, "safeHeightUnit": "css" }
  • Bridge before page scripts. If your App injects the bridge late (after the page starts, as Flutter and some Android wrappers do), append uid=<userId>&time=<unix ms> to the entry URL. With both parameters present the page waits up to 3 s for the bridge to appear instead of falling back to browser mode.
  • getConfig answer within 8 s. If NativeBridge.getConfig synchronously returns a non-empty JSON string, the page uses it. Otherwise it waits for the jsCallback call up to 8 s, then shows an entry error. Budget about 5 s for fetching the launch code from your backend.
  • No stale scripts. Game scripts are served with Cache-Control: no-cache and versioned URLs; do not force-cache the WebView.
  • Call getConfig fresh for every game open: launch codes are single-use.
class StarGameBridge(private val activity: Activity, private val webView: WebView) {
@JavascriptInterface
fun getConfig(params: String?): String {
val callback = params?.let { JSONObject(it).optString("jsCallback") }.orEmpty()
// Ask your backend for a one-time launch code (off the UI thread).
launchCodes.issue(currentUser.id) { code ->
val config = JSONObject()
.put("code", code)
.put("userId", currentUser.id)
.put("roomId", currentRoom.id)
.put("language", Locale.getDefault().language)
.put("gameMode", 2)
.put("safeHeight", halfScreenHeightCssPx)
.put("safeHeightUnit", "css")
val args = JSONArray().put(callback).put(config.toString())
webView.post { webView.evaluateJavascript("(function(a){window[a[0]](a[1]);})($args)", null) }
}
return "" // empty: the page waits for the callback
}
@JavascriptInterface fun gameLoaded() = activity.runOnUiThread { hideLoading() }
@JavascriptInterface fun gameRecharge() = activity.runOnUiThread { openTopUp() }
@JavascriptInterface fun destroy() = activity.runOnUiThread { closeGame() }
@JavascriptInterface fun haptic(payload: String) { /* optional */ }
@JavascriptInterface fun displayMode(payload: String) { /* optional */ }
}
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.addJavascriptInterface(StarGameBridge(activity, webView), "NativeBridge")
webView.loadUrl("https://sandbox.sentosatower.com/games/lantern/index.html?op=$operatorId")

You can check your launch codes without the App. Open the entry URL with &uid=1&time=1 added, and inject this before the page scripts run (for example with a userscript manager set to run at document start):

window.NativeBridge = {
getConfig(p) {
const cb = JSON.parse(p).jsCallback;
setTimeout(() => window[cb](JSON.stringify({ code: "<launch code>", userId: "<player id>", gameMode: 2, language: "en" })), 20);
return "";
},
gameLoaded() { console.log("host: gameLoaded"); },
gameRecharge() { console.log("host: gameRecharge"); },
destroy() { console.log("host: destroy"); },
};

Or automate it with Playwright:

import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
page.on("console", (m) => console.log(m.text()));
await page.addInitScript((code) => {
window.NativeBridge = {
getConfig(p) {
const cb = JSON.parse(p).jsCallback;
setTimeout(() => window[cb](JSON.stringify({ code, userId: "self-test", gameMode: 2, language: "en" })), 20);
return "";
},
gameLoaded() { console.log("host: gameLoaded"); },
gameRecharge() {},
destroy() {},
};
}, process.env.LAUNCH_CODE);
await page.goto(`https://sandbox.sentosatower.com/games/lantern/index.html?op=${process.env.OPERATOR_ID}&uid=1&time=1`);
await page.waitForTimeout(5000);
await page.screenshot({ path: "lantern.png" });
await browser.close();

Expected: the betting screen appears within about 3 seconds, host: gameLoaded is logged, and your wallet receives one POST /players/authenticate for the launch code.