Compare commits

...
2 Commits
Author SHA1 Message Date
joywayer 1203957924 修复定位的回调问题 2026-08-07 12:18:44 +08:00
joywayerandClaude Opus 5 3f8b382ad3 加桥/定位边界诊断日志,排查子游戏收不到 getlocationinfo
现象:大厅 H5 能正确拿到定位,子游戏拿不到。已静态排除「子游戏未注册
handler」与「回调错发给大厅」两个猜测(BridgeBus 是 per-WebView 实例、
handlers 是实例状态,反向 call 捕获各自的 bridge,物理上不可能串台;
下载真实子游戏 zip 比对后确认 H5 侧大厅/子游戏逻辑完全对称)。

为定位真正的失败层,在各组件边界加日志(纯诊断,不改任何行为):
- BridgeBus 加 label(lobby / subGame)区分来源,记录 ← H5 调用、
  → H5 反向 call、native handler 未注册、evaluateJavaScript 失败
- sendToJS 改为返回 'ok'/'no-bridge',H5 侧 bridge 未就绪导致的静默丢包
  现在会打错误日志(原实现 `if (window.X)` 直接丢弃,完全不可见)
- LocationService 记录 requestOnce 序号 / shared manager id /
  requestLocation 的 BOOL 返回值 / completionBlock 是否回来 / stop() 调用
  —— 高德文档明确 requestLocation 返回 NO 时 completionBlock 永不调用,
  当前代码忽略该返回值会让 continuation 永挂,H5 连 errorCode 12 都收不到
- H5ErrorRelay 补 console.warn 中继,让 WVJB「H5 侧无对应 handler」的
  warn 能出现在 Xcode console

契约影响:无。仅新增日志与可选 label 参数,桥接接口名 / 字段名 / 数据结构
均未变动(docs/H5-Native-Contract.md §3.1[20]/ §3.2[6]不变)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMPCLfsuxvwgMotzsb67QH
2026-08-07 01:54:18 +08:00
13 changed files with 696 additions and 161 deletions
+51 -2
View File
@@ -616,13 +616,14 @@ H5 端无需任何改动:仍然按照"收到 `gameui_stop_voice(user)` 就停
**表 B — `getlocationinfo`** (成功):
```jsonc
{
"errorCode": 0, // ⚠️ 新外壳补齐项,msext 遗漏,见下方说明
"address": "<完整地址>",
"city": "<市>",
"cityCode": "<城市编码>",
"country": "<国>",
"district": "<区>",
"latitude": "30.567890", // ⚠️ string,原生 stringWithFormat:@"%f",不是 double
"longitude": "104.123456", // ⚠️ string
"latitude": 30.567890, // ⚠️ 数字(非字符串),6 位小数。新外壳修正项,见下方说明
"longitude": 104.123456, // ⚠️ 数字(非字符串),6 位小数
"province": "<省>", // ⚠️ 小写 p,与 sharelogin 的 "Province" 大写形成不一致,沿用历史
"street": "<街道>"
}
@@ -632,6 +633,54 @@ H5 端无需任何改动:仍然按照"收到 `gameui_stop_voice(user)` 就停
{ "errorCode": 12, "errorMsg": "缺少定位权限" } // errorCode 是 NSNumberJSON 看是数字 12
```
**`errorCode: 0`(成功状态码)—— 唯一一处有意偏离 msext 的定位字段**
msext 全工程(`grep -rn errorCode msext/Class/`)只在**失败**分支发 `errorCode: 12`
`gameController.m:2507` / `NewRootVC.m:2113` / 老 UIWebView 路径
`RootVC.m:1976` / `fourviewVC.m:1518`),成功分支的 9 个字段里**没有** errorCode。
**这是原工程的遗漏**,不是刻意设计。
H5 侧以 `errorCode == 0` 作为"这份定位数据有效"的判据。证据(2026-08-07 从真机沙盒
取回的现网 H5 逐字核对):
| H5 包 | `05_Func.js` 日期 | 全文 `errorCode` |
|---|---|---|
| 子游戏 `jinxianmahjong` | 2026-07-19 | 有 2 处 —— `Func.startlocation` / `Func.getlocation``catch` 兜底桩自造定位对象时带 `"errorCode":0` |
| 大厅 `gamehall` | 2026-02-04 | **0 处** |
即:较新的子游戏 H5 依赖成功包里的 `errorCode`,较老的大厅 H5 不依赖。这正是
「大厅定位正常、子游戏拿不到定位」这一现象的成因 —— 原生成功包缺 `errorCode`
时子游戏 H5 不认这份数据。
因此新外壳在成功包里补 `errorCode: 0`JSON 数字,与失败包的 `12` 同类型)。
对不检查该字段的大厅 H5 是多一个无害字段,不改变其行为。
**`latitude` / `longitude` 是数字,不是字符串 —— 第二处修正**
msext 的两条路径自己就不一致:
| msext 路径 | 代码 | JSON 形态 |
|---|---|---|
| WVJB / WKWebView`gameController.m:2528``NewRootVC.m:2132`| `[NSString stringWithFormat:@"%f", …]` 放进 `@{}` | **字符串** `"28.636486"` |
| 老 UIWebView / JSContext`RootVC.m:1998``fourviewVC.m:1541`| 手拼 `\"latitude\":%f` | **数字** `28.636486` |
现网子游戏 H5`jinxianmahjong` `05_Func.js:2029-2031`)兜底桩自造的定位对象写的是
`"latitude":28.623546` —— **数字**形态。与 `errorCode` 是同一类问题:WVJB 路径当年
把它字符串化了。新外壳按数字发。
实现细节:用 `BridgeData.decimal(Decimal)`(不是 `.number(Double)`)。原因是
`JSONSerialization``Double` 会输出 17 位有效数字,`28.636486` 变成
`28.636486000000001`;虽然 JS `JSON.parse` 出来是同一个 IEEE754 double、H5 不可分辨,
但 JSON 文本与原工程不一致。`Decimal``String(format: "%f", …)` 构造,序列化成
`NSDecimalNumber` → 文本 `28.636486`,与老路径 `%f` 拼串逐字节一致。
**这两处(`errorCode: 0`、经纬度数字)是定位链路上唯一有意偏离 msext WVJB 路径的地方**
其余(9 个字段名、`cityCode` 大写 C、`province` 小写 p、`street` 不带 `路` 后缀、
失败包 `{errorCode:12, errorMsg:"缺少定位权限"}`、error 与 success 两段式发送顺序、
任一逆地理字段为 nil 时整条不发)全部严格对齐。
实现:`Source/Bridge/Handlers/StartLocationHandler.swift``Source/Location/LocationService.swift`
**表 C — `sharelogin`**
```jsonc
{
+3 -3
View File
@@ -5,9 +5,9 @@
<key>gameid</key>
<string>G2hw0ubng0zcoI0r4mx3H2yr4GejidwO</string>
<key>channel</key>
<string>FtJf073aa0d6rI1xD8J1Y42fINTm0ziK</string>
<string>frdt0C1GG0t91P0McFo0rbA1he5yurbS</string>
<key>gamedir</key>
<string>FtJf073aa0d6rI1xD8J1Y42fINTm0ziK</string>
<string>frdt0C1GG0t91P0McFo0rbA1he5yurbS</string>
<key>gamestart</key>
<string>gamehall</string>
<key>gameconfig</key>
@@ -15,7 +15,7 @@
<key>market</key>
<string>2</string>
<key>agent</key>
<string>veRa0qrBf0df2K1G4de2tgfmVxB2jxpv</string>
<string>00bA05haB0d9ZC0fwGD09Q2OA30insbQ</string>
<key>appversion</key>
<string>44</string>
<key>other</key>
@@ -4,8 +4,26 @@
* H5 端桥协议代码,与 Native BridgeBus@MainActor BridgeProtocol 实现)
* 一一对应。详见 docs/H5-Native-Implementation-Design.md §3.2 / §3.3。
*
* 在 BridgedWebView 构造时通过 WKUserScript.atDocumentStart)注入到所有
* 加载的 H5 页面。H5 业务代码运行时,window.WebViewJavascriptBridge 已就绪
* ⚠️ 本文件的**语义**必须与原工程 msext 的 `WebViewJavascriptBridge_JS.m`
* marcuswestin 原版,daoqi/msext/Class/WebViewJavascriptBridge/)保持一致
* 传输层可以现代化(msext 用 iframe + _fetchQueue 拉取,我们用
* WKScriptMessageHandler 直推),但**派发时机与消息字段必须逐项对齐**,
* 否则 H5 侧同一份代码在两个壳里的执行顺序不同 → 行为不一致。
*
* 已对齐的 msext 语义(每一条都曾经不一致,是排查「子游戏定位拿不到」时发现的):
*
* 1. **异步派发**msext `dispatchMessagesWithTimeoutSafety = true`,且
* `disableJavscriptAlertBoxSafetyTimeout` 在整个 msext 工程里**从未被调用**,
* 所以 Native → H5 的每一条消息都走 `setTimeout(_doDispatch)`,在**新的
* macrotask** 上执行。早前本文件是同步派发(在 evaluateJavaScript 内直接
* 调 handler),H5 handler 相对自身 pending 脚本/微任务的顺序与 msext 不同。
* 2. **WVJBCallbacks 用 `setTimeout(..., 0)` flush**msext `_callWVJBCallbacks`),
* 不是同步 flush —— 决定 H5 的 `registerHandler` 何时生效。
* 3. **H5 → Native 的 response 消息带 `handlerName`**msext `_doSend({handlerName,
* responseId, responseData})`)。
* 4. `_disableJavascriptAlertBoxSafetyTimeout` 作为 H5 侧 handler 注册(msext 有)。
* 5. 缺省安装 `window.onerror`msext 在 H5 未装时兜底装一个)。
* 6. 「收到 Native 消息但没有对应 handler」用 `console.log`msext 原文),不是 warn。
*
* 协议(Native ↔ H5 双向 JSON 消息):
* - H5 → Nativewindow.webkit.messageHandlers.WVJBHandler.postMessage({...})
@@ -18,18 +36,26 @@
;(function () {
if (window.WebViewJavascriptBridge) { return; }
// msext 同款:H5 没装 onerror 时兜底装一个
if (!window.onerror) {
window.onerror = function (msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
};
}
// ── 内部状态 ─────────────────────────────────────────────
var messageHandlers = {}; // H5 注册的 handlername → fn(data, responseCallback)
var responseCallbacks = {}; // 等待 Native 响应的 JS 回调:callbackId → fn(responseData)
var nextCallbackId = 1;
// msext `dispatchMessagesWithTimeoutSafety`:默认 true,仅在 Native 下发
// `_disableJavascriptAlertBoxSafetyTimeout` 时置 falsemsext 从不下发)。
var dispatchMessagesWithTimeoutSafety = true;
// ── 对外 API ─────────────────────────────────────────────
var bridge = window.WebViewJavascriptBridge = {
/**
* H5 注册 handler,等待 Native 主动 callHandler。重名覆盖。
* @param {string} handlerName
* @param {function(data, responseCallback)} handler
*/
registerHandler: function (handlerName, handler) {
messageHandlers[handlerName] = handler;
@@ -37,39 +63,31 @@
/**
* H5 主动调 Native handler。
* @param {string} handlerName
* @param {*} data 任意 JSON 可序列化值
* @param {function(responseData)} [responseCallback]
* msext `callHandler` 支持 2 参形式(第二个参数是函数时视为 responseCallback)。
*/
callHandler: function (handlerName, data, responseCallback) {
var callbackId = null;
if (typeof responseCallback === 'function') {
callbackId = 'cb_' + (nextCallbackId++) + '_' + Date.now();
responseCallbacks[callbackId] = responseCallback;
if (arguments.length === 2 && typeof data === 'function') {
responseCallback = data;
data = null;
}
var message = { handlerName: handlerName };
if (data !== undefined && data !== null) {
message.data = data;
}
if (callbackId) {
message.callbackId = callbackId;
}
_postMessageToNative(message);
_doSend({ handlerName: handlerName, data: data }, responseCallback);
},
disableJavscriptAlertBoxSafetyTimeout: function () {
dispatchMessagesWithTimeoutSafety = false;
},
/**
* Native → H5 入口。BridgeBus.sendToJS 会注入:
* window.WebViewJavascriptBridge._handleMessageFromObjC('<base64of JSON>')
* base64 包装是为了避免 JSON 内容里的单引号 / 反斜杠扰乱 evaluateJavaScript 的字符串字面量。
* base64 包装是为了避免 JSON 内容里的单引号 / 反斜杠扰乱 evaluateJavaScript
* 的字符串字面量(msext 走的是逐个 escape,等价)。
*
* ⚠️ 无返回值:msext 的派发是 setTimeout 异步的,拿不到「H5 有没有跑到
* handler」的同步结果。原生侧不要依赖它的返回值做诊断。
*/
_handleMessageFromObjC: function (base64String) {
try {
var json = _decodeBase64UTF8(base64String);
var message = JSON.parse(json);
_dispatchFromNative(message);
} catch (e) {
console.error('[WVJB] handle message from native failed:', e);
}
_dispatchMessageFromObjC(base64String);
}
};
@@ -79,42 +97,77 @@
try {
window.webkit.messageHandlers.WVJBHandler.postMessage(message);
} catch (e) {
console.error('[WVJB] post to native failed (WVJBHandler 通道未注册?):', e);
console.log('[WVJB] post to native failed (WVJBHandler 通道未注册?):', e);
}
}
function _dispatchFromNative(message) {
// Native 响应 H5 早前 callHandler
if (message.responseId) {
var cb = responseCallbacks[message.responseId];
if (cb) {
delete responseCallbacks[message.responseId];
try { cb(message.responseData); }
catch (e) { console.error('[WVJB] response callback threw:', e); }
function _doSend(message, responseCallback) {
if (typeof responseCallback === 'function') {
var callbackId = 'cb_' + (nextCallbackId++) + '_' + new Date().getTime();
responseCallbacks[callbackId] = responseCallback;
message['callbackId'] = callbackId;
}
_postMessageToNative(message);
}
/// msext `_dispatchMessageFromObjC` 逐句等价:默认走 setTimeout(新 macrotask)。
function _dispatchMessageFromObjC(base64String) {
if (dispatchMessagesWithTimeoutSafety) {
setTimeout(_doDispatchMessageFromObjC);
} else {
_doDispatchMessageFromObjC();
}
function _doDispatchMessageFromObjC() {
var message;
try {
message = JSON.parse(_decodeBase64UTF8(base64String));
} catch (e) {
console.log('[WVJB] handle message from native failed:', e);
return;
}
return;
}
// Native 主动调用 H5 handler
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.warn('[WVJB] no H5 handler registered for:', message.handlerName);
return;
}
var responseCallback;
var responseCallback;
if (message.callbackId) {
var responseId = message.callbackId;
responseCallback = function (responseData) {
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId];
if (!responseCallback) { return; }
responseCallback(message.responseData);
delete responseCallbacks[message.responseId];
return;
}
if (message.callbackId) {
var callbackResponseId = message.callbackId;
responseCallback = function (responseData) {
// msext 同款:response 消息里**带 handlerName**
_doSend({
handlerName: message.handlerName,
responseId: callbackResponseId,
responseData: responseData
});
};
}
var handler = messageHandlers[message.handlerName];
if (!handler) {
console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
// 诊断上报(不改 msext 语义,只是额外往自家通道发一条原生侧忽略的消息)
_postMessageToNative({ diagDispatched: message.handlerName, hadHandler: false });
} else {
var threw = null;
try { handler(message.data, responseCallback); }
catch (e) { threw = String(e); }
_postMessageToNative({
responseId: responseId,
responseData: responseData
diagDispatched: message.handlerName,
hadHandler: true,
threw: threw
});
};
// msext 不吞异常(handler 抛错会冒到 setTimeout 的全局 onerror);
// 这里把它原样重抛,保持行为一致,只是先记录了一笔。
if (threw !== null) { throw new Error(threw); }
}
}
try { handler(message.data, responseCallback); }
catch (e) { console.error('[WVJB] H5 handler "' + message.handlerName + '" threw:', e); }
}
/**
@@ -127,25 +180,26 @@
}).join(''));
}
// msext 同款:把 Native 可下发的 safety-timeout 开关注册成 H5 侧 handler
bridge.registerHandler("_disableJavascriptAlertBoxSafetyTimeout",
bridge.disableJavscriptAlertBoxSafetyTimeout);
// ── 告知原生「桥已就绪」──────────────────────────────────
// 等价 msext 的 `__bridge_loaded__` iframe 回调 → `injectJavascriptFile` →
// flush `startupMessageQueue`。原生在收到本消息前发出的 callHandler 必须排队,
// 不能丢(msext 有 startupMessageQueue,早前我们是直接丢弃)。
_postMessageToNative({ bridgeReady: true });
// ── marcuswestin 旧式握手兼容 ─────────────────────────────
// 部分 H5 代码这样初始化 bridge
// function setupWebViewJavascriptBridge(callback) {
// if (window.WebViewJavascriptBridge) return callback(WebViewJavascriptBridge);
// if (window.WVJBCallbacks) return window.WVJBCallbacks.push(callback);
// window.WVJBCallbacks = [callback];
// /* iframe trick - WKWebView 模式不需要 */
// }
// setupWebViewJavascriptBridge(function (bridge) { bridge.registerHandler(...) });
//
// 我们在 documentStart 就注入 bridge,正常路径下 H5 拿到的 window.WebViewJavascriptBridge
// 已存在,会立即同步 callback(bridge)。但若 H5 代码本身用了 WVJBCallbacks 队列模式
// (Array.push 在 bridge 之前),需要在此处 flush 一次。
if (Array.isArray(window.WVJBCallbacks)) {
var pending = window.WVJBCallbacks;
// msext `_callWVJBCallbacks` 用 setTimeout(..., 0)**不是同步 flush**
// 这决定 H5 的 registerHandler 相对页面其它顶层脚本的生效时机。
setTimeout(function () {
var callbacks = window.WVJBCallbacks;
if (!callbacks) { return; }
delete window.WVJBCallbacks;
for (var i = 0; i < pending.length; i++) {
try { pending[i](bridge); }
catch (e) { console.error('[WVJB] pending WVJBCallback threw:', e); }
for (var i = 0; i < callbacks.length; i++) {
try { callbacks[i](bridge); }
catch (e) { console.log('[WVJB] pending WVJBCallback threw:', e); }
}
}
}, 0);
})();
+156 -7
View File
@@ -8,6 +8,75 @@
import Foundation
import WebKit
import os.log
/// `let`Logger Sendable便 evaluateJavaScript
/// MainActor
private let bridgeLog = Logger(subsystem: "ylgamehall", category: "Bridge")
/// getlocationinfo
///
/// `Logger.debug` devicectl console / Xcode
/// console `.error` `print` stderr
/// 便 CoreLocation os_log
/// `Documents/diag.log` Xcode print Xcode console
///
/// xcrun devicectl device copy from --domain-type appDataContainer \
/// --domain-identifier com.skyapp.ylgamehall --source Documents/diag.log ...
///
nonisolated func diagLog(_ message: String) {
let line = "[DIAG \(diagTimestamp())] \(message)"
print(line)
diagFileSink.append(line)
}
private nonisolated let diagFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "HH:mm:ss.SSS"
return f
}()
private nonisolated func diagTimestamp() -> String {
diagFormatter.string(from: Date())
}
/// `Documents/diag.log` session
/// 便 diagLog
// SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor nonisolated
// @Sendable / evaluateJavaScript
private nonisolated final class DiagFileSink: @unchecked Sendable {
private let queue = DispatchQueue(label: "ylgamehall.diag.log")
private let url: URL
private var wroteHeader = false
init() {
let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
url = dir.appendingPathComponent("diag.log")
}
func append(_ line: String) {
queue.async { [self] in
var text = line + "\n"
if !wroteHeader {
wroteHeader = true
let stamp = ISO8601DateFormatter().string(from: Date())
text = "\n===== session \(stamp) pid=\(ProcessInfo.processInfo.processIdentifier) =====\n" + text
}
guard let data = text.data(using: .utf8) else { return }
let fm = FileManager.default
if !fm.fileExists(atPath: url.path) {
fm.createFile(atPath: url.path, contents: nil)
}
guard let handle = try? FileHandle(forWritingTo: url) else { return }
handle.seekToEndOfFile()
handle.write(data)
try? handle.close()
}
}
}
private nonisolated let diagFileSink = DiagFileSink()
/// H5 Native 线
///
@@ -24,14 +93,33 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
/// JS `window.WebViewJavascriptBridge`
public static let jsBridgeName = "WebViewJavascriptBridge"
/// `lobby` / `subGame` / WebView
///
public let label: String
private weak var webView: WKWebView?
private var handlers: [String: BridgeHandler] = [:]
private var pendingCallbacks: [String: BridgeCallback] = [:]
private var nativeCallbackCounter: UInt64 = 0
/// msext `WebViewJavascriptBridgeBase.startupMessageQueue`
/// `_queueMessage` bridge loaded `injectJavascriptFile` flush
///
/// `window.WebViewJavascriptBridge` ****
/// `sendToJS 'getnetwork' H5no-bridge`
/// msext H5 WebView H5
/// `startlocation` `getlocationinfo`
private var startupMessageQueue: [[String: Any]] = []
/// H5 WVJB.js postMessage `{bridgeReady:true}` true flush
private var bridgeReady = false
/// WebView + `WVJBHandler`
public init(webView: WKWebView, controller: WKUserContentController) {
public init(webView: WKWebView,
controller: WKUserContentController,
label: String = "webview") {
self.webView = webView
self.label = label
super.init()
controller.add(self, name: Self.scriptMessageHandlerName)
}
@@ -40,6 +128,7 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
public func register(_ name: String, handler: @escaping BridgeHandler) {
handlers[name] = handler
diagLog("[\(self.label)] register handler '\(name)'")
}
public func call(_ name: String, data: BridgeData?, callback: BridgeCallback?) {
@@ -53,6 +142,10 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
pendingCallbacks[cbId] = callback
payload["callbackId"] = cbId
}
// / / msext
let wire = (try? JSONSerialization.data(withJSONObject: payload, options: [.sortedKeys]))
.flatMap { String(data: $0, encoding: .utf8) } ?? "<encode-failed>"
diagLog("[\(self.label)] → H5 callHandler '\(name)' wire=\(wire)")
sendToJS(payload: payload)
}
@@ -73,6 +166,27 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
// MARK: -
private func handleIncoming(_ msg: [String: Any]) {
// H5 WVJB.js msext `__bridge_loaded__`
// user script flush
if msg["bridgeReady"] as? Bool == true {
bridgeReady = true
let queued = startupMessageQueue
startupMessageQueue.removeAll()
if !queued.isEmpty {
diagLog("[\(self.label)] 桥就绪,flush \(queued.count) 条暂存消息")
}
for payload in queued { dispatchToJS(payload: payload) }
return
}
// H5 handlerhandler
if let dispatched = msg["diagDispatched"] as? String {
let had = msg["hadHandler"] as? Bool ?? false
let threw = msg["threw"] as? String
diagLog("[\(self.label)] H5 派发 '\(dispatched)'hadHandler=\(had) threw=\(threw ?? "nil")")
return
}
// responseIdJS Native callHandler
if let responseId = msg["responseId"] as? String {
let respData = msg["responseData"].flatMap { BridgeData(jsonObject: $0) }
@@ -95,9 +209,19 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
// handler responseData JS
let responseCallback: BridgeCallback? = makeResponseCallback(for: callbackId)
diagLog("[\(self.label)] ← H5 call '\(name)' data=\(String(describing: msg["data"]))")
guard let handler = handlers[name] else {
print("[BridgeBus] no handler registered for '\(name)'")
responseCallback?(nil)
// msext `WebViewJavascriptBridgeBase.flushMessageQueue`
// if (!handler) { NSLog(@"WVJBNoHandlerException, ..."); continue; }
// `continue` **responseCallback **
//
// `responseCallback?(nil)` H5 {responseId:...}
// H5 `Func.getlocation()` callback `getlocationinfo`
// `bridge.callHandler('getlocationinfo',"",function(resp){})`
// `getlocationinfo` handler
// JS
diagLog("[\(self.label)] no NATIVE handler registered for '\(name)'msext 同款:不回 responseCallback")
return
}
@@ -124,17 +248,42 @@ public final class BridgeBus: NSObject, BridgeProtocol, WKScriptMessageHandler {
sendToJS(payload: payload)
}
/// msext `WebViewJavascriptBridgeBase._queueMessage:`
/// `startupMessageQueue`
private func sendToJS(payload: [String: Any]) {
guard let webView else { return }
guard bridgeReady else {
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
diagLog("[\(self.label)] '\(what)' 桥未就绪 → 入队(msext startupMessageQueue 等价,队列长 \(startupMessageQueue.count + 1)")
startupMessageQueue.append(payload)
return
}
dispatchToJS(payload: payload)
}
/// msext `_dispatchMessage:` `_handleMessageFromObjC`
/// H5 `setTimeout` msext dispatchMessagesWithTimeoutSafety=true
/// evaluateJavaScript **** H5 handler
private func dispatchToJS(payload: [String: Any]) {
guard let webView else {
diagLog("[\(self.label)] dispatchToJS 丢弃:webView 已释放")
return
}
guard let data = try? JSONSerialization.data(withJSONObject: payload),
let json = String(data: data, encoding: .utf8)
else {
print("[BridgeBus] JSON encode failed for payload: \(payload)")
diagLog("[\(self.label)] JSON encode failed for payload: \(String(describing: payload))")
return
}
// base64 JSON / JS
// msext escape ///U+2028/U+2029
let base64 = Data(json.utf8).base64EncodedString()
let js = "if (window.\(Self.jsBridgeName)) window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');"
webView.evaluateJavaScript(js, completionHandler: nil)
let js = "window.\(Self.jsBridgeName)._handleMessageFromObjC('\(base64)');"
let tag = label
let what = (payload["handlerName"] as? String) ?? (payload["responseId"] as? String) ?? "?"
webView.evaluateJavaScript(js) { _, error in
if let error {
diagLog("[\(tag)] dispatchToJS '\(what)' evaluateJavaScript 失败: \(error.localizedDescription)")
}
}
}
}
+15 -2
View File
@@ -43,6 +43,14 @@ public typealias BridgeCallback = @Sendable (BridgeData?) -> Void
public enum BridgeData: Sendable {
case string(String)
case number(Double)
/// JSON ****
///
/// `.number(Double)``JSONSerialization` Double 17
/// `28.636486` `28.636486000000001` JS `JSON.parse`
/// IEEE754 doubleH5 JSON ****
/// `getlocationinfo` msext UIWebView
/// `RootVC.m:1998` `\"latitude\":%f`
case decimal(Decimal)
case bool(Bool)
case null
case array([BridgeData])
@@ -89,6 +97,7 @@ extension BridgeData {
switch self {
case .string(let s): return s
case .number(let d): return d
case .decimal(let d): return d as NSDecimalNumber
case .bool(let b): return b
case .null: return NSNull()
case .array(let arr): return arr.map { $0.jsonObject }
@@ -124,6 +133,7 @@ extension BridgeData {
return String(Int(d))
}
return String(d)
case .decimal(let d): return "\(d)"
case .bool(let b): return b ? "true" : "false"
case .null: return nil
case .array, .object: return nil
@@ -131,8 +141,11 @@ extension BridgeData {
}
nonisolated public var asDouble: Double? {
if case .number(let d) = self { return d }
return nil
switch self {
case .number(let d): return d
case .decimal(let d): return (d as NSDecimalNumber).doubleValue
default: return nil
}
}
nonisolated public var asInt: Int? {
+18 -4
View File
@@ -47,20 +47,25 @@ public final class H5ErrorRelay: NSObject {
switch kind {
case "console.error":
let args = (dict["args"] as? [String]) ?? []
print("[H5 console.error]", args.joined(separator: " "))
diagLog("[H5 console.error] " + args.joined(separator: " "))
case "console.warn":
// WebViewJavascriptBridge.js Native handlerName
// H5 console.warn
let args = (dict["args"] as? [String]) ?? []
diagLog("[H5 console.warn] " + args.joined(separator: " "))
case "onerror":
let msg = (dict["msg"] as? String) ?? ""
let src = (dict["src"] as? String) ?? ""
let line = (dict["line"] as? Int) ?? 0
let col = (dict["col"] as? Int) ?? 0
let stack = dict["stack"] as? String
print("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
diagLog("[H5 onerror] \(msg) at \(src):\(line):\(col)" + (stack.map { "\n\($0)" } ?? ""))
case "unhandledrejection":
let reason = (dict["reason"] as? String) ?? ""
let stack = dict["stack"] as? String
print("[H5 unhandledrejection]", reason + (stack.map { "\n\($0)" } ?? ""))
diagLog("[H5 unhandledrejection] " + reason + (stack.map { "\n\($0)" } ?? ""))
default:
print("[H5 unknown error]", dict)
diagLog("[H5 unknown error] \(dict)")
}
}
@@ -80,6 +85,15 @@ public final class H5ErrorRelay: NSObject {
} catch(e){}
if (origErr) { origErr.apply(console, arguments); }
};
var origWarn = console.warn;
console.warn = function(){
try {
var args = [];
for (var i=0; i<arguments.length; i++) { args.push(String(arguments[i])); }
send({ kind:'console.warn', args: args });
} catch(e){}
if (origWarn) { origWarn.apply(console, arguments); }
};
window.addEventListener('error', function(ev){
send({
kind:'onerror',
@@ -21,7 +21,7 @@
//
// Phase 4.E SDK2.x static delegate API handleOpenURL/sendAuthReq
// SDK WeChatManager singleton pop
// Phase 5 `LocationService.shared.stop()`canImport
// Phase 5 `location.stop()`canImport
// Xcode Embed AMapLocationKit
// Phase 3.B/C/D / `AudioRecorder.shared.cancel()` opencore-amr
// `QiniuUploader.cancelInFlight()` Task.cancel
@@ -34,7 +34,9 @@ import Foundation
public enum BackGameDataHandler {
public static func register(on bridge: any BridgeProtocol) {
/// - Parameter location: **** LocationServicemsext cleanUpAction
/// gameController manager
public static func register(on bridge: any BridgeProtocol, location: LocationService) {
// 18backgameData
// data H5 getWebdata
// cb: "backgameData"
@@ -57,7 +59,7 @@ public enum BackGameDataHandler {
#if canImport(AMapLocationKit)
// msext gameController.m:2473-2478 cleanUpAction
LocationService.shared.stop()
location.stop(caller: "backgameData")
#endif
// Phase 3.C / Phase 3.B QiniuUploader.cancelInFlight
@@ -17,43 +17,82 @@
//
import Foundation
import os.log
// SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor let MainActor
// handler @Sendable nonisolatedLogger Sendable
private nonisolated let startLocLog = Logger(subsystem: "ylgamehall", category: "Location")
public enum StartLocationHandler {
public static func register(on bridge: any BridgeProtocol) {
bridge.register("startlocation") { _, callback in
/// - Parameters:
/// - label: `lobby` / `subGame`
/// - location: **** LocationServicemsext /
/// `AMapLocationManager` LocationService.init
public static func register(on bridge: any BridgeProtocol,
label: String = "?",
location: LocationService) {
bridge.register("startlocation") { data, callback in
// msext cb
callback?(.string("startlocation"))
diagLog("[\(label)] ← startlocation 入参 type=\(data?.asLooseString ?? "nil")")
#if canImport(AMapLocationKit)
// SDK callback 9
// data == 1 Phase 5.4
// msext NewRootVC.m:411-421 / gameController.m:533-543
// int tempinfo = [data intValue];
// if (tempinfo == 1) [self.locationManager startUpdatingLocation]; //
// else [self reGeocodeAction]; //
// H5 gamehall / jinxianmahjong 2
// msext msext
let isContinuous = Int(data?.asLooseString ?? "") == 1
Task { @MainActor in
do {
let payload = try await LocationService.shared.requestOnce()
bridge.call("getlocationinfo", data: .object([
"address": .string(payload.address),
"city": .string(payload.city),
"cityCode": .string(payload.cityCode),
"country": .string(payload.country),
"district": .string(payload.district),
"latitude": .string(payload.latitude), // string
"longitude": .string(payload.longitude), // string
"province": .string(payload.province), // p
"street": .string(payload.street)
]), callback: nil)
} catch LocationError.authorizationDenied {
// msext gameController.m:2507
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
} catch {
// 12msext
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
// outcomemsext completionBlock
// .success
let dispatch: @MainActor (LocationOutcome) -> Void = { outcome in
switch outcome {
case .failure:
// msext gameController.m:2507
diagLog("[\(label)] → getlocationinfo errorCode=12")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(12),
"errorMsg": .string("缺少定位权限")
]), callback: nil)
case .success(let payload):
// msext gameController.m:2528 9 + `errorCode: 0`
//
// docs/H5-Native-Contract.md §3.2 B
// msext **** `errorCode: 12`
// 9 **** errorCode
// H5 `errorCode == 0` ""
// jinxianmahjong 05_Func.js2026-07-19
// `Func.startlocation` / `Func.getlocation`
// `"errorCode":0` gamehall
// 05_Func.js2026-02-04 errorCode
// ""
// errorCode H5
// **** msext
//
diagLog("[\(label)] → getlocationinfo 成功 city=\(payload.city) province=\(payload.province)")
bridge.call("getlocationinfo", data: .object([
"errorCode": .number(0),
"address": .string(payload.address),
"city": .string(payload.city),
"cityCode": .string(payload.cityCode),
"country": .string(payload.country),
"district": .string(payload.district),
"latitude": .decimal(payload.latitude), // 6
"longitude": .decimal(payload.longitude), // 6
"province": .string(payload.province), // p
"street": .string(payload.street)
]), callback: nil)
}
}
if isContinuous {
location.startContinuous(caller: label, emit: dispatch)
} else {
location.requestOnce(caller: label, emit: dispatch)
}
}
#endif
@@ -90,6 +90,7 @@ public final class AppCoordinator {
AudioPlayer.shared.stopAllBackground()
let subGame = SubGameViewController(request: request)
diagLog("[coordinator] push 子游戏(栈深 \(nav.viewControllers.count)\(nav.viewControllers.count + 1)")
nav.pushViewController(subGame, animated: true)
return true
}
@@ -97,6 +98,7 @@ public final class AppCoordinator {
/// pop data H5 backgameData
/// WebContainer `getWebdata` callback
public func popSubGame(returningData data: String) {
diagLog("[coordinator] pop 子游戏回大厅")
navigationController?.popViewController(animated: true)
NotificationCenter.default.post(
name: .subGameDidReturn,
+234 -32
View File
@@ -11,6 +11,10 @@
import Foundation
import CoreLocation
import os.log
/// letLogger Sendable便 AMap
private let locLog = Logger(subsystem: "ylgamehall", category: "Location")
#if canImport(AMapLocationKit)
import AMapLocationKit
@@ -28,78 +32,276 @@ public struct LocationPayload: Sendable {
public let cityCode: String
public let country: String
public let district: String
public let latitude: String
public let longitude: String
/// **JSON ** `%f` 6
/// `Decimal` `Double` init §3.2 B
public let latitude: Decimal
public let longitude: Decimal
public let province: String
public let street: String
}
public enum LocationError: Error, Sendable {
case sdkNotLinked
case authorizationDenied
case timeout
case underlying(any Error)
#if canImport(AMapLocationKit)
extension LocationPayload {
/// msext `@{...}`
///
/// msext
/// ```objc
/// @try{ [_bridge callHandler:@"getlocationinfo" data:@{
/// @"address":regeocode.formattedAddress, @"city":regeocode.city, ... }]; }
/// @catch (NSException * e) { NSLog(...); }
/// ```
/// `@{}` ** value nil NSException**
/// `@try/@catch` 9 nil
/// ** getlocationinfo H5**
///
/// `?? ""`
/// H5 msext
/// nil nil
nonisolated init?(location: CLLocation, reGeocode: AMapLocationReGeocode) {
guard let address = reGeocode.formattedAddress,
let city = reGeocode.city,
let cityCode = reGeocode.citycode,
let country = reGeocode.country,
let district = reGeocode.district,
let province = reGeocode.province,
let street = reGeocode.street
else { return nil }
self.address = address
self.city = city
self.cityCode = cityCode
self.country = country
self.district = district
// **** `%f` 6
//
// docs/H5-Native-Contract.md §3.2 Bmsext WVJB
// `gameController.m:2528` / `NewRootVC.m:2132`
// `[NSString stringWithFormat:@"%f", ...]` ****
// UIWebView / JSContext `RootVC.m:1998` / `fourviewVC.m:1541`
// `\"latitude\":%f` ** JSON ** H5
// jinxianmahjong 05_Func.js:2029-2031
// `"latitude":28.623546` `errorCode` WVJB
//
//
// `%f` `Decimal` 6
// JSONSerialization `28.636486` Double 17
// `28.636486000000001` JS parse IEEE754 doubleH5
// JSON
let latText = String(format: "%f", location.coordinate.latitude)
let lonText = String(format: "%f", location.coordinate.longitude)
self.latitude = Decimal(string: latText) ?? Decimal(location.coordinate.latitude)
self.longitude = Decimal(string: lonText) ?? Decimal(location.coordinate.longitude)
self.province = province
self.street = street
}
}
#endif
/// **** failure success
/// msext `completionBlock` requestOnce
public enum LocationOutcome: Sendable {
/// H5 `getlocationinfo({errorCode:12, errorMsg:""})`
case failure
/// H5 `getlocationinfo(<9 >)`
case success(LocationPayload)
}
@MainActor
public final class LocationService {
public static let shared = LocationService()
/// `lobby` / `subGame`
private let owner: String
public init() {}
/// ****
/// msext `NewRootVC.m:1568` / `gameController.m:1230`
/// `[[AMapLocationManager alloc] init]` manager + completionBlock +
/// delegate manager
/// 1. `requestLocationWithReGeocode:completionBlock:`
///
/// completionBlock continuation H5
/// 2. `stopUpdatingLocation` cancel
/// `backgameData`
/// 3. `delegate = nil` manager ****
public init(owner: String = "?") {
self.owner = owner
#if canImport(AMapLocationKit)
// msext `configLocationManager` `setDelegate:self` ****
// `amapLocationManager:doRequireLocationAuth:` delegate
// init shim lazy
_ = delegateShim
#endif
}
#if canImport(AMapLocationKit)
/// msext `configLocationManager`
private let manager: AMapLocationManager = {
let m = AMapLocationManager()
m.desiredAccuracy = kCLLocationAccuracyHundredMeters
m.pausesLocationUpdatesAutomatically = false
m.locationTimeout = 6
m.reGeocodeTimeout = 3
m.locatingWithReGeocode = true
return m
}()
/// `startlocation` type == 1 delegate
/// msext `configLocationManager` `setDelegate:self` VC
/// LocationService NSObject
private lazy var delegateShim: LocationDelegateShim = {
let shim = LocationDelegateShim(owner: owner)
manager.delegate = shim
return shim
}()
#endif
/// + msext locAction + completionBlock
public func requestOnce() async throws -> LocationPayload {
/// requestOnce
private var requestSeq: UInt64 = 0
/// + ** msext `initCompleteBlock` completionBlock**
/// `gameController.m:2493-2545` / `NewRootVC.m:2099-2150`
///
/// ```objc
/// if (error) {
/// callHandler getlocationinfo {errorCode:12,...} //
/// if (error.code == AMapLocationErrorLocateFailed) return;
/// }
/// if (location) { if (regeocode) {
/// if (regeocode.formattedAddress != nil)
/// callHandler getlocationinfo <9 > //
/// }}
/// ```
///
/// ** return** locateFailed CLLocation
/// / error **** location msext
/// errorCode 12H5
///
/// `async throws -> LocationPayload`
/// error return H5 errorCode 12
/// `C_Player.SetLocationInfo({errorCode:12})` addr
/// /CPU
/// msext outcome
///
/// - Parameter emit: MainActor 0 / 1 / 2
public func requestOnce(caller: String = "?",
emit: @escaping @MainActor (LocationOutcome) -> Void) {
#if canImport(AMapLocationKit)
return try await withCheckedThrowingContinuation { (cont: CheckedContinuation<LocationPayload, Error>) in
manager.requestLocation(withReGeocode: true) { loc, regeo, err in
requestSeq += 1
let seq = requestSeq
diagLog("[\(caller)] requestOnce #\(seq) 发起(owner=\(owner) manager=\(UInt(bitPattern: ObjectIdentifier(self.manager).hashValue))")
let t0 = Date()
// completionBlock 线 msext
let accepted = manager.requestLocation(withReGeocode: true) { loc, regeo, err in
let ms = Int(Date().timeIntervalSince(t0) * 1000)
diagLog("[\(caller)] requestOnce #\(seq) completionBlock 回来(耗时 \(ms)ms):loc=\(loc != nil) regeo=\(regeo != nil) err=\(err?.localizedDescription ?? "nil")")
MainActor.assumeIsolated {
if let err = err as NSError? {
// msext 12 errorCodehandler
emit(.failure) //
if err.code == AMapLocationErrorCode.locateFailed.rawValue {
cont.resume(throwing: LocationError.authorizationDenied)
} else {
cont.resume(throwing: LocationError.underlying(err))
diagLog("[\(caller)] requestOnce #\(seq) locateFailed,按 msext 就此结束")
return
}
diagLog("[\(caller)] requestOnce #\(seq) 有 error 但非 locateFailed,按 msext 继续尝试补发真实定位")
}
// msext location / regeocode / formattedAddress
guard let loc, let regeo, regeo.formattedAddress != nil else {
diagLog("[\(caller)] requestOnce #\(seq) 无可用逆地理(loc=\(loc != nil) regeo=\(regeo != nil)),按 msext 不发 9 字段")
return
}
guard let loc, let regeo else {
cont.resume(throwing: LocationError.timeout)
guard let payload = LocationPayload(location: loc, reGeocode: regeo) else {
diagLog("[\(caller)] requestOnce #\(seq) 逆地理有 nil 字段,按 msext@{} 抛 NSException)整条不发")
return
}
cont.resume(returning: LocationPayload(
address: regeo.formattedAddress ?? "",
city: regeo.city ?? "",
cityCode: regeo.citycode ?? "",
country: regeo.country ?? "",
district: regeo.district ?? "",
latitude: String(format: "%f", loc.coordinate.latitude),
longitude: String(format: "%f", loc.coordinate.longitude),
province: regeo.province ?? "",
street: regeo.street ?? ""
))
emit(.success(payload)) //
}
}
// AMapLocationManager.hRequestmsext
// H5 continuation
// 便
if !accepted {
diagLog("[\(caller)] requestOnce #\(seq) requestLocation 返回 NO(未挂上请求),按 msext 静默")
}
#else
throw LocationError.sdkNotLinked
diagLog("[\(caller)] requestOnce: AMapLocationKit 未链接,按 msext 无 SDK 场景不回 H5")
#endif
}
/// `startlocation` type == 1
/// msext `NewRootVC.m:414` / `gameController.m:537`
/// `if (tempinfo == 1) [self.locationManager startUpdatingLocation];`
/// delegate `amapLocationManager:didUpdateLocation:reGeocode:`
/// `getlocationinfo`msext `gameController.m:2551-2560` `reGeocode`
/// `formattedAddress` `didFailWithError` msext NSLog H5
///
/// - Parameter emit:
public func startContinuous(caller: String = "?",
emit: @escaping @MainActor (LocationOutcome) -> Void) {
#if canImport(AMapLocationKit)
diagLog("[\(caller)] startContinuous 发起(owner=\(owner)")
delegateShim.onUpdate = emit
delegateShim.caller = caller
manager.startUpdatingLocation()
#else
diagLog("[\(caller)] startContinuous: AMapLocationKit 未链接,按 msext 无 SDK 场景不回 H5")
#endif
}
/// msext gameController.m:2473-2478 cleanUpAction
public func stop() {
public func stop(caller: String = "?") {
#if canImport(AMapLocationKit)
// `stopUpdatingLocation` cancel manager
// manager msext
// cleanUpAction manager
diagLog("[\(caller)] stop()stopUpdatingLocation + delegate=nilowner=\(owner)")
manager.stopUpdatingLocation()
manager.delegate = nil
#endif
}
}
#if canImport(AMapLocationKit)
/// `AMapLocationManagerDelegate`
/// msext NewRootVC / gameController
/// LocationService 宿
@MainActor
private final class LocationDelegateShim: NSObject, AMapLocationManagerDelegate {
var onUpdate: (@MainActor (LocationOutcome) -> Void)?
var caller: String = "?"
private let owner: String
init(owner: String) {
self.owner = owner
super.init()
}
/// msext `gameController.m:2551-2560` reGeocode formattedAddress
/// 9
nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
didUpdate location: CLLocation!,
reGeocode: AMapLocationReGeocode!) {
// AMap Sendable Sendable
// LocationPayload MainActor Swift 6 reGeocode
guard let location, let reGeocode, reGeocode.formattedAddress != nil else {
diagLog("[持续定位] 更新但无可用逆地理,按 msext 不发")
return
}
guard let payload = LocationPayload(location: location, reGeocode: reGeocode) else {
diagLog("[持续定位] 逆地理有 nil 字段,按 msext@{} 抛 NSException)整条不发")
return
}
MainActor.assumeIsolated {
diagLog("[\(caller)] 持续定位 → getlocationinfo city=\(payload.city)")
onUpdate?(.success(payload))
}
}
/// msext `gameController.m:2547` ** H5**
nonisolated func amapLocationManager(_ manager: AMapLocationManager!,
didFailWithError error: (any Error)!) {
diagLog("[持续定位 owner=\(owner)] didFailWithError: \(error?.localizedDescription ?? "nil")msext 同款:不通知 H5")
}
}
#endif
@@ -17,7 +17,9 @@ public final class BridgedWebView: UIView {
public let webView: WKWebView
public let bridge: BridgeBus
public init() {
/// - Parameter label: `lobby` / `subGame`
///
public init(label: String = "webview") {
// WKWebViewConfiguration §4.1
let configuration = WKWebViewConfiguration()
configuration.defaultWebpagePreferences.allowsContentJavaScript = true
@@ -71,7 +73,8 @@ public final class BridgedWebView: UIView {
}
#endif
let bridge = BridgeBus(webView: webView,
controller: configuration.userContentController)
controller: configuration.userContentController,
label: label)
self.webView = webView
self.bridge = bridge
@@ -33,7 +33,11 @@ public final class SubGameViewController: UIViewController {
// MARK: - UI
private let bridgedWebView = BridgedWebView()
private let bridgedWebView = BridgedWebView(label: "subGame")
/// msext gameController.m:1230 `configLocationManager`
/// alloc AMapLocationManager VC
private let locationService = LocationService(owner: "subGame")
private let splash = SplashOverlay()
// MARK: - Handlers
@@ -88,7 +92,7 @@ public final class SubGameViewController: UIViewController {
DeviceInfoHandler.register(on: bridge)
BrowserHandler.register(on: bridge)
OpenSaomaHandler.register(on: bridge)
StartLocationHandler.register(on: bridge)
StartLocationHandler.register(on: bridge, label: "subGame", location: locationService)
// assetsRoot = H5 msext gameController.m:364
// closure effectiveGameDir boot pipeline
@@ -118,7 +122,7 @@ public final class SubGameViewController: UIViewController {
OpenurlTitleDataHandler.register(on: bridge)
// backgameData退 + callback getWebdata
BackGameDataHandler.register(on: bridge)
BackGameDataHandler.register(on: bridge, location: locationService)
// 3 stub H5 "no handler"
// Agora VideoRoomHandlers 3 stub
@@ -15,7 +15,11 @@ public final class WebContainerViewController: UIViewController {
// MARK: - UI
private let bridgedWebView = BridgedWebView()
private let bridgedWebView = BridgedWebView(label: "lobby")
/// msext NewRootVC.m:1568 `configLocationManager`
/// msext
private let locationService = LocationService(owner: "lobby")
private let splash = SplashOverlay()
// MARK: - Handlers ( handler enum register)
@@ -77,7 +81,7 @@ public final class WebContainerViewController: UIViewController {
DeviceInfoHandler.register(on: bridge) // §3.1 21 + §3.2 1
BrowserHandler.register(on: bridge) // §3.1 16
OpenSaomaHandler.register(on: bridge) // §3.1 22 stub
StartLocationHandler.register(on: bridge) // §3.1 20Phase 5 Phase 2 stub
StartLocationHandler.register(on: bridge, label: "lobby", location: locationService) // §3.1 20Phase 5 Phase 2 stub
// Phase 3.A + 3.C/3.D stub
// assetsRoot = H5 lobbyIndex lobby