PWA Scanner (offline)
Use the Bodygram PWA Scanner for offline-capable scanning across modern browsers, iOS, Android, and hybrid WebView apps.
The Bodygram PWA Scanner is a Progressive Web App build of the scanner that continues to work when the device is offline. Once loaded, it can capture front and side images directly on the user's device without needing a live internet connection — handy for retail, events, and any environment with unreliable connectivity.
The Bodygram PWA Scanner is an experimental feature. It has been tested in common scenarios, but you may hit edge cases on specific devices or browsers. Use it in controlled environments and share feedback so we can improve stability.
Supported environments
The PWA Scanner targets any platform with Progressive Web App support:
- Modern web browsers (Chrome, Firefox, Safari, Edge)
- iOS (Safari on iPhone / iPad)
- Android (Chrome or in-app
WebView) - Hybrid apps (React Native, Cordova, Flutter — with proper WebView configuration)
Scanner URL
The PWA Scanner is hosted at a single URL — no token required:
https://platform.bodygram.com/pwa/scannerQuery parameters
| Parameter | Description |
|---|---|
cameraDirection | back to use the rear camera. Omit for the default (front) camera. |
screens | Comma-separated list of screens to show. See Screens. Defaults to all. |
Example with the form skipped, gyro alignment enabled, and the back camera selected:
https://platform.bodygram.com/pwa/scanner?screens=gyro-permission,gyro,scan&cameraDirection=backScreens
name | Description | Mandatory | Note |
|---|---|---|---|
form | Input form for age, gender, height, weight. | No | Skipping this screen makes formData in the completion payload null. |
how-to-scan | Instructions on how to scan. | No | — |
can-hear-voice | Instructions on voice guidance. | No | If this is the first screen, add &tap=true — iOS needs a tap gesture before playing audio. |
gyro-permission | Instructions to place/hold the device. | No | Required before the gyro screen. |
gyro | Gyro alignment screen. | No | Must be preceded by gyro-permission. |
scan | Camera + scan UI. | Yes | — |
result | Scan result screen. | No | — |
all | All screens. | No | Default value. |
Form data behavior
When you skip the form screen:
- Only the front and side scan images are returned.
payload.formDatain the completion event isnull.- You need to collect
height,age,gender, andweightyourself.
Embed in a web page
Load the PWA Scanner in an <iframe> with the required camera and sensor permissions:
<iframe
src="https://platform.bodygram.com/pwa/scanner"
allow="camera; microphone; accelerometer; magnetometer; gyroscope"
style="width: 100%; height: 100%; border: none;"
></iframe>Using the Bodygram Platform SDK?
If you want parent ↔ iframe communication via the SDK (rather than raw postMessage), load the Bodygram Platform SDK and initialize it with shouldSetupIframe: true. See Embed in an <iframe> in the standard scanner guide.
Receiving events
The PWA Scanner communicates with the parent window using postMessage. Each event has the shape { type, payload }:
type | When it fires | payload |
|---|---|---|
0 | User requested to close the scanner. | {} |
5 | Model loading status changed. | { status, model } |
6 | Offline scan completed. | { images: { front, side }, formData } — formData is null when the form screen is skipped. |
window.addEventListener('message', (event) => {
const { type, payload } = event.data;
switch (type) {
case 0:
console.log('Close requested');
break;
case 5:
console.log('Model status changed', payload);
break;
case 6:
console.log('Offline scan completed', payload);
console.log('Front image:', payload.images.front);
console.log('Side image:', payload.images.side);
console.log('Form data:', payload.formData); // may be null
break;
}
});Complete example
A minimal page that opens the PWA Scanner on button click, closes it when requested, and logs the scan result:
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PWA Scanner</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
</style>
</head>
<body>
<button id="open-scanflow">Open Scanflow</button>
<div id="anchor" style="width: 100dvw; height: 100dvh;"></div>
<script>
function openScanflow() {
const anchor = document.getElementById('anchor');
const scanflow = document.createElement('iframe');
scanflow.src = 'https://platform.bodygram.com/pwa/scanner';
scanflow.style.width = '100%';
scanflow.style.height = '100%';
scanflow.setAttribute(
'allow',
'camera; microphone; gyroscope; accelerometer; magnetometer;'
);
anchor.appendChild(scanflow);
document.getElementById('open-scanflow').style.display = 'none';
}
function closeScanflow() {
const anchor = document.getElementById('anchor');
anchor.removeChild(anchor.firstChild);
document.getElementById('open-scanflow').style.display = 'block';
}
window.addEventListener('message', (event) => {
const { type, payload } = event.data;
switch (type) {
case 0:
closeScanflow();
break;
case 5:
console.log('Model status changed', payload);
break;
case 6:
console.log('Offline scan completed');
console.log('Front image:', payload.images.front);
console.log('Side image:', payload.images.side);
console.log('Form data:', payload.formData);
break;
}
});
document
.getElementById('open-scanflow')
.addEventListener('click', openScanflow);
</script>
</body>
</html>Embed in Android WebView
The general WebView setup is identical to the standard scanner — see the Android WebView guide for manifest permissions, camera permission handling, and WebChromeClient wiring.
The PWA Scanner uses a different JavaScript interface name and exposes only two methods:
| Interface name | ScanflowJSWebviewInterface |
|---|---|
onRequestClose(args) | Fires when the user taps the close (×) button. args is "{}". |
onOfflineScanComplete(args) | Fires when the offline scan completes. args is a JSON string with formData and images. |
onOfflineScanComplete payload:
{
"formData": {
"age": 25,
"gender": "male",
"weight": 70,
"height": 170,
"inches": 0,
"measurementSystem": "metric"
},
"images": {
"front": "<base64 JPEG>",
"side": "<base64 JPEG>"
}
}A full MainActivity for the PWA Scanner:
package com.example.bsp_integration // ← your package name
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface
import android.webkit.PermissionRequest
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class OfflineScanFormData(
val age: Int,
val gender: String,
val weight: Int,
val height: Int,
val inches: Int,
val measurementSystem: String,
)
@Serializable
data class OfflineScanImages(
val front: String,
val side: String,
)
@Serializable
data class OfflineScanData(
val formData: OfflineScanFormData,
val images: OfflineScanImages,
)
class MainActivity : ComponentActivity() {
private lateinit var webView: WebView
private lateinit var cameraPermissionRequest: PermissionRequest
private val registerCameraPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) {
cameraPermissionRequest.grant(cameraPermissionRequest.resources)
}
}
fun onRequestPermission(request: PermissionRequest) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED
) {
request.grant(request.resources)
} else {
cameraPermissionRequest = request
registerCameraPermission.launch(Manifest.permission.CAMERA)
}
}
@SuppressLint("MissingInflatedId")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webview)
webView.addJavascriptInterface(
MyJavaScriptInterface(),
"ScanflowJSWebviewInterface"
)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.cacheMode = WebSettings.LOAD_NO_CACHE
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webChromeClient = object : WebChromeClient() {
override fun onPermissionRequest(request: PermissionRequest) {
onRequestPermission(request)
}
}
webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
) = false
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
webView.evaluateJavascript(
"""
window.BGScanflowJSWebviewInterface = function(fnName, args) {
ScanflowJSWebviewInterface[fnName] && ScanflowJSWebviewInterface[fnName](args);
}
""".trimIndent(),
null
)
}
}
webView.loadUrl("https://platform.bodygram.com/pwa/scanner")
}
inner class MyJavaScriptInterface {
@JavascriptInterface
fun onRequestClose(message: String?) {
Log.d("PWA/close", message.orEmpty())
}
@JavascriptInterface
fun onOfflineScanComplete(message: String?) {
val data = Json.decodeFromString<OfflineScanData>(message.orEmpty())
Log.d("PWA/formData", data.formData.toString())
Log.d("PWA/frontImage", data.images.front)
Log.d("PWA/sideImage", data.images.side)
}
}
}Related
- Use API + Scanner UI — the standard online scanner
- Android WebView integration — base WebView setup and camera permission flow
- API Reference
