Mobile apps
Embed the hosted Viewer in React Native, Flutter, iOS and Android apps.
Start with a published Viewer
A Website embeds the hosted Viewer in an iframe. A Mobile app loads the same Viewer directly in its WebView. ArchWalk maintains panorama rendering, navigation and controls; your host manages presentation, navigation policy and lifecycle. There is no native ArchWalk SDK or separate panorama renderer.
Opening the link in Safari or Chrome is useful, but does not validate mobile app embedding. Test inside the actual application's WebView on a physical device.
- Publish an Experience in Creator and copy its HTTPS
/aw360/v/{publicId}link. - Choose a host below. Keep the URL in local runtime configuration, not committed examples or reports.
- Load the Viewer with JavaScript enabled. Use the Viewer fullscreen icon to exercise the host bridge.
- Complete the device validation checklist.
Already using a coding agent? Choose Integrate with AI → Mobile app → your platform. Website prompts and Partner API provisioning remain available separately. A mobile Viewer needs no Partner API key or Creator session token.
React Native
Use react-native-webview. The working Expo reference includes validated messages, native immersive presentation, diagnostics, recovery and a manual exit control. Keep the same WebView ref/key throughout fullscreen changes.
cd examples/aw360-mobile-webview-spike
npm ci
cp .env.example .env.local
# Edit EXPO_PUBLIC_ARCHWALK_VIEWER_URL in .env.local with your published URL.
npm start
# Physical iPhone/Android: open compatible Expo Go and scan Metro's QR code.
npm run ios # booted iOS Simulator + full Xcode
npm run android # booted Android emulator + Android SDK
The transport path is onMessage ← window.ReactNativeWebView.postMessage(json). Inject the generic adapter into the main Viewer document; acknowledge after native layout commits using injectJavaScript. Reinstall/replay state on load and AppState resume. Use onError, onHttpError, onContentProcessDidTerminate (iOS) and onRenderProcessGone (Android). Process loss needs explicit recovery and may reset camera state; ordinary resume/fullscreen must not reload.
<WebView
source={{ uri: publishedViewerUrl }}
javaScriptEnabled
onMessage={handleValidatedFullscreenRequest}
onShouldStartLoadWithRequest={request => request.url === publishedViewerUrl}
onOpenWindow={blockNewWindow}
/>
The names above are your host handlers, not ArchWalk SDK functions. Copy the complete reference rather than treating this abbreviated snippet as a security-complete app. RN's originWhitelist alone is not an exact-location restriction; the reference uses its callback to block external links rather than letting the OS open arbitrary URLs implicitly. Main-frame-only injection is requested where supported, and the adapter also guards window.parent === window.
Flutter
Use the official webview_flutter package with WebViewController / WebViewWidget. It uses WKWebView on iOS and Android WebView on Android. The Flutter reference and setup fits a fresh app; dependencies stay inside that app.
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(NavigationDelegate(
onNavigationRequest: (request) => request.url == publishedViewerUrl
? NavigationDecision.navigate : NavigationDecision.prevent,
))
..addJavaScriptChannel('ArchWalkNative', onMessageReceived: handleRequest)
..loadRequest(Uri.parse(publishedViewerUrl));
// Retain controller and WebViewWidget during immersive changes.
The channel adapts ArchWalk360Host.postMessage; runJavaScript sends confirmation. SystemChrome handles native presentation, never a different panorama renderer. Android target SDK/system-bar rules may restrict requested immersive mode: verify on the target device. Portable Flutter channels do not expose trustworthy sender-origin/main-frame metadata. Restrict the loaded document and navigation, validate the exact payload/current URL and expose only this non-privileged presentation operation. Apps needing stronger frame attestation must use the platform-specific WebView adapter; do not treat this bridge as authentication.
Use WidgetsBindingObserver for resume, layout/SafeArea for orientation and notches, and NavigationDelegate for HTTP/main-document errors. Review installed platform packages for process-termination hooks; this portable example does not claim native process recovery coverage. Do not silently reload on every resume.
Native iOS
Use WKWebView, WKUserContentController, WKScriptMessageHandler and WKNavigationDelegate. The Swift reference includes a weak message handler, main-frame/exact-URL validation, user scripts, native immersive constraints, native exit and resume/termination handling.
config.userContentController.add(weakHandler, name: "archwalkFullscreen")
config.userContentController.addUserScript(WKUserScript(
source: adapterScript, injectionTime: .atDocumentStart, forMainFrameOnly: true))
// In didReceive: validate message.frameInfo.isMainFrame and exact Viewer URL.
// After native layout commits:
webView.evaluateJavaScript("window.confirmArchWalkFullscreen(true);")
Load the shared teaching adapter from the app bundle. Do not loosen App Transport Security or allow arbitrary popups. WKWebView may lack HTML fullscreen; native presentation changes hide the host chrome and retain the same WKWebView. Home indicator auto-hide remains controlled by iOS. On content process termination, show explicit Reload; recovery may restart the camera.
Native Android
Use Android WebView with AndroidX WebKit. The Kotlin reference uses origin-scoped messaging, exact current URL and main-frame checks, controlled navigation, WindowInsets immersive presentation and explicit render-process recovery.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, "ArchWalkNative", setOf(expectedOrigin)) {
view, message, sourceOrigin, isMainFrame, _ ->
// Validate origin, main frame, exact Viewer URL and exact V1 payload.
// Change host presentation, then confirm actual state via evaluateJavascript.
}
}
Use an exact HTTPS origin rule, not *. Feature-check installed Android System WebView; if scoped messaging is unavailable, report the limitation rather than adding an unrestricted JavaScript interface. The sample blocks mixed content/file/content access and unsolicited windows. It also handles browser custom fullscreen views where supported. Android Back exits presentation and synchronizes state; process loss destroys the unusable WebView only after failure and recreates it only on explicit Reload.
Host fullscreen bridge
Keep the existing V1 contract authoritative. This is separate from the Website iframe bridge; it does not relax parent/origin/CSP rules.
Viewer → host (JSON string):
{"source":"archwalk360","version":1,"type":"fullscreenRequest","fullscreen":true}
Host → Viewer, only after presentation commits:
{"source":"archwalk360","version":1,"type":"fullscreenChanged","fullscreen":true}
Use false to exit. Accept only the exact four fields, the expected source/version/type and a real Boolean, not a string/number. Native hosts validate sender location and main frame where the platform exposes them. The adapter is host-owned and installed only in the trusted top-level Viewer:
window.ArchWalk360Host = {
version: 1, capabilities: { fullscreen: true }, fullscreen: false,
postMessage(json) { nativeTransport.send(json); }
};
window.dispatchEvent(new Event('archwalk360:host-ready'));
// After host layout commits (including native exit):
window.ArchWalk360Host.fullscreen = true;
window.dispatchEvent(new CustomEvent('archwalk360:host-message', {
detail: { source: 'archwalk360', version: 1, type: 'fullscreenChanged', fullscreen: true }
}));
nativeTransport.send is your platform transport, not a new API. Prefer supported browser fullscreen even when an adapter exists. On unsupported browser fullscreen, the Viewer asks the host and waits for confirmation before changing its icon. Confirm actual state on refusal, load, resume and native exit. Keep the WebView mounted; never change its key, reload, re-create controls or reset the camera just to enter/exit immersive mode. The shared bridge specification covers late installation and no-host fallback.
Navigation and credentials
Configure one expected published HTTPS Viewer URL, with no username/password, query or fragment. Reject unexpected top-level navigation and unsolicited new windows; open intentional external links only through a separate explicit host policy. Preserve existing HTTPS, CSP, media CORS and iframe restrictions. No wildcard messaging origin.
A published link is public access to a publication, not an API credential. Treat disposable identifiers as sensitive test inputs and keep them out of committed screenshots/logs/reports. Never send Partner API keys, Creator session credentials or host authentication secrets into the Viewer. A trusted document script can call the injected adapter: presentation control is not authentication. Do not attach filesystem, token or other privileged operations to it.
Lifecycle, orientation and safe areas
Keep the same WebView on ordinary foreground/resume. Replay installation and confirmed host state; inspect WebGL rendering and touch after resuming. Let the host resize the WebView on orientation; the Viewer responds to viewport changes. Do not lock orientation solely for ArchWalk. Keep native exit controls clear of the notch/system navigation and at least 44 logical pixels where practical.
WebGL requires a working GPU/system WebView and reachable media. Old devices, memory pressure or background eviction can lose WebGL/content processes. Expose truthful failure/Reload instead of a blank spinner or automatic reload loop. Reload after process loss may reset scene/camera; preserving it across a terminated process is not part of this contract.
Troubleshooting
- Blank document: confirm the exact published URL, HTTPS, network access and native load/HTTP errors. Do not log the full capability URL.
- Document loaded but no panorama: inspect media reachability/CORS, WebGL/GPU and memory; document load alone is not render success.
- Fullscreen icon does nothing: verify transport installation, strict request validation and host confirmation after layout. Do not fake
document.fullscreenEnabled. - Icon stays Enter: the host has not confirmed
fullscreenChanged; sending a request alone is insufficient. - Camera resets: check for changing keys/controller instances or a reload on resume/fullscreen.
- Portrait works, landscape fails: inspect native constraints, viewport resize and safe-area overlays; preserve drag/pinch gestures.
- Android system bars persist: verify target SDK and OS policies; this is host presentation, not a protocol change.
Physical device validation
Report Pass / Fail, device/model, OS, framework/package versions, Android System WebView version where relevant, and redacted failure notes. Test both iOS and Android in the actual app, not just a mobile browser.
| Check | Pass / Fail / Notes |
|---|---|
| Viewer loads and panorama renders (not just document load) | |
| Touch drag and pinch zoom | |
| Previous/Next at first/middle/last scene; no wrap | |
| Filmstrip direct selection | |
| Doorway and Step navigation; floor plan if published | |
| Fullscreen icon appears | |
| Browser fullscreen or host immersive entry, as supported | |
| Icon changes only after host confirmation | |
| Exit through Viewer and native host exit/back synchronizes state | |
| Portrait and landscape; notch/system controls clear | |
| Background/foreground; rendering and input recover | |
| Same WebView stays mounted through fullscreen changes; no camera reset | |
| Unexpected navigation/new windows blocked or explicitly handled | |
| No obvious WebGL/context failure | |
| Explicit recovery after process loss; reset caveat understood |
Run commands and fresh-app setup for Flutter, Swift and Kotlin are in the reference README. Successful unit tests or Metro bundles do not prove physical device compatibility. The original iPhone React Native validation proved load/render/drag/pinch/mobile layout; the expanded hosts still require their own device results.

