Scope: Web3 Browser PWA ↔ native shell (iOS / Android / Desktop WebView). Alignment: Reuse and extend the existing CashTrees / POS client bridge family — do not invent a parallel injection namespace.
| Injected global | Product | Notes |
|---|---|---|
window.CashTreesIOS |
Consumer PWA (SilentPassUI) + shared CashTrees shell |
WKWebView messageHandlers.CashTreesIOS |
window.CashTreesAndroid |
Same | @JavascriptInterface |
window.BeamioPOS |
POS PWA (posPwa) |
Optional; may coexist with CashTrees* |
window.Web3BrowserDesktop |
Web3 Browser desktop shell (new) | Same method names where applicable |
Detection (prefer injected globals over UA):
CashTreesIOS → ios
CashTreesAndroid → android
Web3BrowserDesktop → desktop
else → browser // no raw sockets until a host injectsEvent bus (Native → PWA):
| Platform | CustomEvent name |
|---|---|
| iOS | cashtreesios |
| Android | cashtreesandroid |
| Desktop (recommended) | web3browser |
| POS legacy | beamiopos |
Payload shape: { action: string, …fields }. Async results carry requestId when the PWA started the call.
iOS vs Android argument style (must preserve):
| Style | iOS | Android |
|---|---|---|
| Structured args | Object payload { requestId, url, … } |
Often plain string / JSON string |
openURL |
openURL({ url }) |
openURL(url: string) |
publishAppState |
object | JSON string |
scanQr |
scanQr({ requestId }) |
scanQr(requestId: string) |
PWA helpers must normalize both (see SilentPassUI openExternalUrl, scanQrViaCashTreesNative).
Source of truth in sibling products (copy semantics only — no ../.. imports):
| Area | SilentPassUI | POS PWA |
|---|---|---|
| NFC / QR / openURL | utils/cashTreesNativeNfc.ts, cashTreesIOSBridge.ts |
bridge/cashTreesScanBridge.ts |
| Embedded OTA | utils/cashTreesEmbeddedPwaUpdate.ts |
utils/cashTreesEmbeddedPwaUpdate.ts |
| App state / chat notify | utils/cashTreesNativeAppStateBridge.ts |
bridge/posNativeAppStateBridge.ts |
| Push bind | utils/cashTreesPushBind.ts |
bridge/posCashTreesPushBind.ts |
| Lifecycle | utils/cashTreesAppLifecycle.ts |
(visibility + same events) |
| — | bridge/cashTreesPrintBridge.ts |
|
| POS navigate / wallet | — | bridge/nativeBridge.ts (BeamioPOS) |
| Method | Direction | Semantics |
|---|---|---|
getNfcStatus() |
PWA → Native | Probe shell / NFC: ready | no_hardware | nfc_disabled | nfc_permission_denied | … |
openURL |
PWA → Native | Open http(s) / mailto / tel in system browser (allowlist). Web fallback: window.open. Required for Explorer / docs / onramp. |
| Method | Direction | Semantics |
|---|---|---|
scanQr |
PWA → Native | Camera QR; result via event action: 'scanQr' + requestId / ok / text |
scanRecoveryQr |
PWA → Native | Recovery QR variant; same event pattern |
saveRecoveryQrToPhotos |
PWA → Native | Optional: save recovery image to Photos |
| Method | Direction | Semantics |
|---|---|---|
startPhysicalCardBind |
PWA → Native | Arm NFC read / bind |
cancelPhysicalCardBind |
PWA → Native | Cancel armed session |
| (events) | Native → PWA | NFC detail: ok, tagUidHex, ndefUri, sun params, error |
Web3 Browser may omit NFC in Phase 0–1; keep method names reserved if the same CashTrees shell binary is shared.
| Method / event | Direction | Semantics |
|---|---|---|
getEmbeddedPwaVersion() |
PWA → Native | Current embedded bundle version |
getEmbeddedPwaPendingVersion() |
PWA → Native | Downloaded pending version |
applyEmbeddedPwaUpdate() |
PWA → Native | Apply zip / restart WebView content |
embeddedPwaUpdateAvailable |
Native → PWA | Banner: new version ready |
applyEmbeddedPwaUpdate (event) |
Native → PWA | Ack / progress if shell posts it |
Manifest: update.json + zip — not Service Worker as shell upgrade path.
| Method / event | Direction | Semantics |
|---|---|---|
publishAppState |
PWA → Native | Footer badge, app icon badge, etc. |
notifyBackgroundChat |
PWA → Native | Local alert while shell backgrounded (optional; SI push remains primary for chat) |
bindPushIdentity |
PWA → Native | Bind { eoa, pgpKeyId? } for FCM / APNs |
pushDeviceToken |
Native → PWA | Device token for Cluster bind |
appLifecycle |
Native → PWA | phase: active | inactive | background |
debugLog(level, message) |
PWA → Native | Dev logging into native console |
webContentReady |
PWA → Native | First-paint handoff (splash) — shell documentStart injection |
| Method | Semantics |
|---|---|
platform |
'ios' | 'android' | 'web' |
getWalletAddress / getWalletPrivateKeyHex / hasStoredWallet |
Legacy display / migrate hints — signing truth is PWA IndexedDB |
createWallet / restoreWallet |
Prefer PWA IndexedDB path |
navigateNative(action) |
Charge / History etc. → native UI (POS abandoned native UI; keep for shell routing) |
resendParentPermissionRequest |
Staff permission resend |
printReceipt |
AirPrint / receipt (CashTreesIOS.printReceipt) |
Web3 Browser wallet secrets stay in this app’s IndexedDB (Consumer wallet rules). Do not depend on POS Keychain-only wallet APIs.
These are additions on the same globals (CashTreesIOS / CashTreesAndroid / Web3BrowserDesktop). Prefer the same CustomEvent bus.
| Method | Direction | Contract |
|---|---|---|
socketOpen({ requestId, host, port, tls? }) |
PWA → Native | Open outbound TCP (optional TLS) |
socketWrite({ socketId, dataBase64 }) |
PWA → Native | Write bytes |
socketClose(socketId) |
PWA → Native | Close |
event socket |
Native → PWA | See below |
Events (action: 'socket' or type: 'socket'):
event |
Fields |
|---|---|
open |
socketId, requestId |
data |
socketId, dataBase64 |
close |
socketId, reason? |
error |
socketId?, message, requestId? |
MVP policy: allowlist destinations (CoNET SI / known entry domains / loopback). No arbitrary FS. Binary always base64 on the bridge.
UDP (Phase 2+): udpOpen / udpSend / udpClose — same base64 pattern; follow conet-depin-udp-forward-protocol (never put AES keys in route-B-decryptable commands).
Product path: PWA packs site files from IndexedDB into a ZIP, passes ZIP + local TCP port to the shell; native unzips into a sandbox and starts a static HTTP server on that port (prefer 127.0.0.1).
Full flow: local-website-serve.md.
| Method | Direction | Contract |
|---|---|---|
hostServeWebsite({ requestId, port, siteId, zipBase64?, loopbackOnly?, rootPath? }) |
PWA → Native | Single-shot: unzip + listen; return { ok, origin, port } |
hostServeWebsiteStart({ requestId, port, siteId, byteLength, … }) |
PWA → Native | Begin chunked ZIP transfer |
hostServeWebsiteChunk({ requestId, offset, dataBase64 }) |
PWA → Native | Raw ZIP byte slice at offset (not base64-of-whole-file) |
hostServeWebsiteFinish({ requestId }) |
PWA → Native | Assemble ZIP → unzip → listen; same result as single-shot |
hostStopWebsite(port) |
PWA → Native | Stop HTTP on port; release sandbox |
event hostServe |
Native → PWA | ready | error | stopped (see types) |
Limits: soft single-shot cap HOST_SERVE_ZIP_SINGLE_SHOT_MAX_BASE64_CHARS (~6M chars). Larger sites must use Start/Chunk/Finish.
Truth: IndexedDB remains the durable site store; native sandbox is a serve cache rebuilt on each successful publish.
| Method | Direction | Contract |
|---|---|---|
hostBind({ logicalPort, loopbackOnly? }) |
PWA → Native | Listen only; each GET → PWA |
hostUnbind(logicalPort) |
PWA → Native | Stop listen |
event hostRequest |
Native → PWA | { requestId, method, path, headers? } |
hostRespond({ requestId, status, headers?, bodyBase64? }) |
PWA → Native | Body from IndexedDB resolvePath |
Prefer hostServeWebsite for product publish. Do not treat native disk alone as long-term truth.
Minimum for a shippable Web3 Browser shell (in addition to §2.1–2.2):
| Capability | Why |
|---|---|
openURL |
Explorer, GitBook, onramp |
| Embedded OTA trio + events | Shell version bumps |
appLifecycle |
Pause SI / reconnect policy |
webContentReady |
Splash handoff |
debugLog |
Field diagnostics |
Optional in early phases: NFC, QR, push, print, POS navigate.
| Capability | Consumer CashTrees | POS | Web3 Browser |
|---|---|---|---|
getNfcStatus / NFC bind |
✅ | ✅ | Optional |
scanQr / recovery QR |
✅ | ✅ | Optional |
openURL |
✅ | ✅ | Required |
| Embedded OTA | ✅ | ✅ | Required |
publishAppState / push / lifecycle |
✅ | ✅ | Recommended |
printReceipt |
— | ✅ (iOS) | Out of scope |
BeamioPOS.navigateNative |
— | ✅ | Out of scope |
socketOpen/Write/Close |
❌ | ❌ | Required (new) |
hostServeWebsite (+ chunked) / hostStopWebsite |
❌ | ❌ | Required (new) |
hostBind / hostRespond |
❌ | ❌ | Optional secondary |
openURL: onlyhttp/https/mailto/tel.- Sockets: destination allowlist; no plaintext Layer Minus bypass — HTTP
/postbody remains{ "data": "<OpenPGP armor>" }only. - Never log private keys, full OpenPGP armor, or UDP plaintext /
Securitykey. - Local website: loopback-first; validate ZIP paths (reject
..); public bind only with explicit product approval. - Do not use the bridge as a second wallet secret store (IndexedDB remains Consumer/Web3 Browser truth).
| Platform | Direction |
|---|---|
| iOS | Extend CashTreesIOS WK handler + cashtreesios events; object payloads |
| Android | Extend CashTreesAndroid interface; string/JSON where existing APIs use strings |
| Desktop (macOS) | macos/ SwiftPM AppKit + WKWebView — inject Web3BrowserDesktop (see macos/README.md). Windows/Linux TBD |
TypeScript contracts: src/bridge/nativeBridge.ts.
- New shell methods reuse CashTrees naming / event bus where possible?
- iOS object vs Android string variants both handled in the PWA helper?
- Socket APIs present for SI;
hostServeWebsite(+ stop) forweb3://publish? -
openURL+ Embedded OTA still present? - No
../..import of SilentPassUI / posPwa bridge modules? - No plaintext
/postbypass fields via bridge?