Intégration WebView
Faire tourner le module v1 dans une coquille React Native ou Ionic : la page, le pont, et les permissions.
Une nouvelle intégration devrait utiliser Saphere Scan v2. Ce qui suit documente le module v1 à l’intérieur d’une coquille native.
Le module v1 est une application web. Dans une application mobile, il tourne dans une WebView, sur une petite page HTML que vous livrez avec le paquet, et parle au natif par le pont que fournit le cadriciel.
Trois choses décident s’il fonctionne, tout simplement :
- La permission caméra doit être accordée nativement, avant que la WebView ne charge. La couche web ne peut pas la demander à votre place.
- La WebView doit autoriser la lecture de média en ligne sans geste de l’utilisateur. Sans quoi le flux de la caméra ne démarre jamais.
- La page doit pouvoir charger le module depuis le CDN, ce qui suppose un accès réseau et, sur Android, les bons drapeaux d’accès aux fichiers.
React Native
La page que vous livrez
Placez un integration.html dans les répertoires de ressources de chaque plateforme — android/app/src/main/assets/ et le paquet iOS.
Les deux coquilles de cette page importent depuis la Production. Un build de Test de votre application vise l’autre CDN, et un point d’accès à jetons qui appelle l’api correspondante :
| Environnement | Base du CDN |
|---|---|
| Production | https://cdn.saphere.ai |
| Test | https://cdn.test.saphere.ai |
Voir Environnements — ce sont deux comptes distincts, donc les clés ne se transposent pas.
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=0">
<style>
html, body { padding: 0; margin: 0; overflow: hidden; }
#container { height: 100vh; }
</style>
</head>
<body>
<div id="container"></div>
<script type="module">
import { Handler } from "https://cdn.saphere.ai/saphere-scan/v1/mjs/main.min.js"
const { ReactNativeWebView } = window
const options = {
createMeasure: {
// Pointe vers VOTRE serveur, qui détient la clé d'API.
strategy: "delegate",
url: "https://your-backend.example/api/saphere-measure"
},
// Tout ce que le widget rapporte est transmis au natif.
onEvent: event => ReactNativeWebView.postMessage(JSON.stringify(event))
}
Handler.load("#container", options)
</script>
</body>
</html>
N'incorporez pas la clé d'API ici
Ce fichier est livré dans le paquet de votre application. Tout ce qu’il contient peut être lu en décompressant l’APK ou l’IPA — y compris un en-tête Authorization écrit dans createMeasure.headers.
Pointez url vers votre propre serveur et laissez-le détenir la clé.
L’écran natif
import { WebView } from "react-native-webview"
import { Platform } from "react-native"
const uri = Platform.OS === "android"
? "file:///android_asset/integration.html"
: "./assets/integration.html"
export const ScanScreen = () => (
<WebView
source={{ uri }}
// Sans ces deux-là, le flux de la caméra ne démarre jamais sur iOS.
allowsInlineMediaPlayback
mediaPlaybackRequiresUserAction={false}
javaScriptEnabled
// Nécessaires pour charger la page depuis le répertoire de ressources sur Android.
allowFileAccess
allowUniversalAccessFromFileURLs
originWhitelist={["*"]}
textZoom={100}
startInLoadingState
onMessage={({ nativeEvent: { data } }) => {
const event = JSON.parse(data)
if (event.type === "result")
console.log("variables:", event.variables)
}}
/>
)
| Propriété | Pourquoi elle compte |
|---|---|
allowsInlineMediaPlayback | iOS lit la vidéo en plein écran par défaut, ce qui casse la mise en page |
mediaPlaybackRequiresUserAction={false} | Sans elle, le flux attend une tape qui ne viendra jamais |
allowFileAccess, allowUniversalAccessFromFileURLs | Android a besoin des deux pour charger depuis file:///android_asset et atteindre le réseau |
textZoom={100} | Empêche le réglage système de taille de police de casser la mise en page |
onMessage | Reçoit ce que postMessage envoie |
Le pont est unidirectionnel
ReactNativeWebView.postMessage fait sortir les événements de la WebView. Il n’existe aucun mécanisme v1 permettant au natif de piloter le widget — aucun moyen d’annuler une mesure ou de changer les options depuis React Native une fois qu’il est monté.
S’il vous faut cela, démontez la WebView.
Permission caméra
Demandez-la avant de rendre la WebView. Un appel à getUserMedia dans une WebView dont l’application hôte n’a pas la permission système échoue immédiatement, et l’utilisateur voit l’écran « caméra refusée » du widget sans aucune invite système pour l’expliquer.
import { PERMISSIONS, RESULTS, check, request } from "react-native-permissions"
const CAMERA = Platform.OS === "android"
? PERMISSIONS.ANDROID.CAMERA
: PERMISSIONS.IOS.CAMERA
export async function ensureCamera(): Promise<boolean> {
const status = await check(CAMERA)
if (status === RESULTS.GRANTED)
return true
return await request(CAMERA) === RESULTS.GRANTED
}
Manifestes
iOS — Info.plist :
<key>NSCameraUsageDescription</key>
<string>Used to measure your vital signs from video.</string>
Écrivez une vraie phrase. La revue de l’App Store rejette un texte de remplissage, et l’utilisateur la lit au moment où il décide.
Android — AndroidManifest.xml :
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
Ionic / Capacitor
La coquille Capacitor est elle-même une WebView, de sorte que le widget se monte directement dans l’application — sans page interne.
Chargez le build classique depuis le gabarit de l’application :
<!-- index.html -->
<script src="https://cdn.saphere.ai/saphere-scan/v1/js/main.min.js"></script>
Puis montez-le depuis un composant :
import { Component } from "react"
declare global {
interface Window { Handler?: any }
}
export class ScanView extends Component {
private readonly options = {
createMeasure: {
strategy: "delegate",
url: "https://your-backend.example/api/saphere-measure"
},
onEvent: (event: { type: string }) => console.log(event.type, event)
}
componentDidMount() {
// La balise de script s'est déjà exécutée ; aucun pont n'est nécessaire,
// le widget est déjà dans la WebView de l'application.
window.Handler?.load("#container", this.options)
}
async componentWillUnmount() {
await window.Handler?.destroy()
}
render() {
return <div id="container" style={{ height: "100vh" }} />
}
}
AndroidManifest.xml a besoin des deux mêmes permissions que plus haut, ainsi que de la configuration d’activité qui empêche la WebView d’être recréée à la rotation :
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" />
Une activité recréée relance la mesure
Sans cesconfigChanges, tourner l’appareil détruit et recrée l’activité — et la WebView avec elle. Une mesure en cours est perdue, et l’utilisateur recommence.Aide-mémoire
- Permission caméra demandée nativement, avant que la WebView n’apparaisse
-
NSCameraUsageDescriptionrédigée comme une vraie phrase -
CAMERAetINTERNETdéclarées sur Android - Lecture de média en ligne autorisée, geste utilisateur non requis
- Aucune clé d’API où que ce soit dans la page livrée
-
configChangesréglés pour que la rotation ne recrée pas la WebView - La page servie ou chargée depuis une origine sécurisée