Installation

Chargez un module ES depuis le CDN, montez-le dans un conteneur qui vous appartient, et le widget prend la suite

Saphere Scan est livré sous la forme d’un unique module ES. Il n’y a rien à installer, rien à empaqueter et aucune dépendance de pair à concilier — le paquet, le modèle de détection du visage et les ressources sont tous servis par le CDN.

Quel CDN

Il y en a deux, un par environnement, et celui depuis lequel vous importez décide de l’environnement contre lequel le widget travaille — il en déduit l’hôte de l’api de mesure à partir de cette même origine. Tous les exemples de cette page visent la Production ; remplacez l’hôte par celui du Test, et rien d’autre ne change.

EnvironnementURL du module
Productionhttps://cdn.saphere.ai/saphere-scan/v2/main.js
Testhttps://cdn.test.saphere.ai/saphere-scan/v2/main.js

Les deux servent la même livraison : ce que vous validez sur le Test est ce qui tournera en Production. Voir Environnements pour ce qui diffère par ailleurs — au premier chef, que les clés d’API ne sont pas interchangeables.

La page fonctionnelle la plus courte

<!doctype html>
<html lang="fr">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        html, body { margin: 0; height: 100%; }
        #scan { height: 100dvh; }
    </style>
</head>
<body>
    <div id="scan"></div>

    <script type="module">
        import SaphereScan from "https://cdn.saphere.ai/saphere-scan/v2/main.js"

        const instance = SaphereScan.create("#scan", {
            lang: "fr",
            proxy: {
                retrieveAccessToken: { strategy: "delegate", url: "/api/saphere-token" }
            }
        })

        await instance.bootstrap()
    </script>
</body>
</html>

C’est une intégration complète. Le widget mène l’utilisateur à travers le consentement, les consignes et la capture, puis rend compte par les événements.

Le cycle de vie

Trois méthodes, et elles constituent toute la surface publique.

SaphereScan.create(root, options)

Valide les options et retourne une instance. Elle ne monte rien — aucune application Angular n’est construite, aucune caméra n’est touchée, aucune requête réseau n’est émise.

root est un sélecteur CSS ou un HTMLDivElement. Toute autre valeur lève Root element must be an HTMLDivElement.

Les options sont validées ici, de façon synchrone, contre l’arbre d’options complet. Une clé mal orthographiée est ignorée silencieusement ; une valeur de forme incorrecte lève. C’est délibéré : vous l’apprenez au moment du create(), sur votre propre machine, plutôt que trois écrans après le début du parcours d’un utilisateur.

await instance.bootstrap()

Construit l’application et la monte. Retourne l’instance, ce qui permet de chaîner.

L’appeler deux fois lève SaphereScan is already bootstrapped. Si le montage échoue, la promesse est rejetée avec l’erreur sous-jacente, l’application partiellement construite est démontée, et un événement widget:error porte le message — de sorte qu’un gestionnaire de supervision l’apprend même si personne n’attend la promesse.

instance.destroy()

Démonte le widget et libère la caméra, le websocket et les workers.

Elle est idempotente. L’appeler sur une instance qui n’a jamais été montée, ou deux fois de suite, ne fait rien et ne lève rien — vous n’avez pas à suivre l’état du widget pour avoir le droit de le démonter.

const instance = SaphereScan.create("#scan", options)
await instance.bootstrap()

// plus tard — en quittant la page, en fermant une fenêtre modale, en démontant un composant
instance.destroy()

Toujours détruire au démontage

Le widget tient un flux caméra, un websocket et deux web workers. Une application monopage qui retire le conteneur du DOM sans appeler destroy() laisse le voyant de la caméra allumé.

Le contrat du conteneur

Le widget possède son conteneur. Trois conséquences à connaître avant de le mettre en forme.

Il remplace les enfants du conteneur. Tout ce que vous avez laissé dans le div — un contenu d’attente, votre propre indicateur de chargement — est retiré au moment où bootstrap() s’exécute. Placez votre contenu d’attente à l’intérieur du conteneur et il disparaît exactement au bon moment, sans aucune coordination de votre côté.

Il se monte dans un shadow root. Le CSS de votre page ne peut pas atteindre l’intérieur du widget, et le CSS du widget ne peut pas déborder dans votre page. C’est ce qui rend le widget sûr à poser dans un système de design existant, et c’est aussi pourquoi vous ne pouvez pas le remettre en forme par une feuille de style — la personnalisation passe par les options.

Il a besoin d’un conteneur qui a une hauteur. Le widget remplit 100 % de son conteneur dans les deux dimensions. Un div sans hauteur s’écrase à rien et le widget ne sera pas visible.

/* Plein écran — le choix habituel sur mobile et en WebView */
#scan { height: 100dvh; }

/* Ou un cadre fixe dans une page plus large */
#scan { width: min(420px, 100%); aspect-ratio: 9 / 16; margin-inline: auto; }

Le parcours est conçu en mode portrait. Un cadre haut et étroit est ce pour quoi il est fait.

Sans modules ES

Certaines intégrations ne peuvent pas utiliser <script type="module"> : une coquille WebView ancienne, une configuration d’empaqueteur que vous ne maîtrisez pas, un CMS qui n’accepte qu’une balise de script simple. Le même paquet s’expose aussi globalement.

<script src="https://cdn.saphere.ai/saphere-scan/v2/main.js"></script>
<script>
    window.addEventListener("saphere-scan:ready", async ({ detail: { SaphereScan } }) => {
        const instance = SaphereScan.create("#scan", { lang: "fr", proxy: { /* … */ } })
        await instance.bootstrap()
    })
</script>

Le CustomEvent saphere-scan:ready est émis sur window une fois le paquet évalué, et porte la classe dans detail.SaphereScan. La même classe est également posée sur window.IVirtual.SaphereScan.

Écoutez l’événement plutôt que de lire la variable globale directement. Une balise <script> simple n’a pas la garantie d’avoir fini de s’exécuter quand votre propre script en ligne démarre, et l’événement supprime cette course.

Le paquet refuse d’être chargé deux fois : une seconde évaluation lève IVirtual.SaphereScan is already defined. Pour faire tourner plusieurs widgets, créez plusieurs instances à partir de l’unique classe — n’incluez pas le script deux fois.

TypeScript

Le module exporte ses types, de sorte qu’une intégration écrite en TypeScript obtient l’autocomplétion sur l’arbre d’options et un switch exhaustif sur les événements.

import SaphereScan from "https://cdn.saphere.ai/saphere-scan/v2/main.js"
import type { SaphereScanOptions, SaphereScanEvent, SaphereScanEventType } from "…"

const options: SaphereScanOptions = {
    lang: "fr",
    proxy: { retrieveAccessToken: { strategy: "delegate", url: "/api/saphere-token" } },
    onEvent: (event: SaphereScanEvent) => {
        switch (event.type) {
            case "measure:result":
                // `event.result` est typé ici, et seulement ici
                save(event.result)
                break
            case "measure:failed":
                report(event.code)
                break
        }
    }
}
ExportCe que c’est
SaphereScanOptionsL’arbre d’options entier, chaque branche facultative. Ce que create() accepte.
SaphereScanRootstring | HTMLDivElement — ce que create() accepte comme premier argument.
SaphereScanEventL’union discriminée des quarante événements, portée par type.
SaphereScanEventTypeSeulement les noms, quand le discriminant seul suffit.
SAPHERE_SCAN_EVENTSLes noms sous forme de constantes gelées, pour les intégrations en JavaScript.

SAPHERE_SCAN_EVENTS existe parce qu’en JavaScript un switch sur des littéraux de chaîne nus n’a aucun filet : une faute de frappe devient une branche qui ne s’exécute jamais et ne se plaint jamais.

import SaphereScan, { SAPHERE_SCAN_EVENTS } from "https://cdn.saphere.ai/saphere-scan/v2/main.js"

const onEvent = event => {
    if (event.type === SAPHERE_SCAN_EVENTS.MEASURE_RESULT)
        save(event.result)
}

La validation à l'exécution demeure

Les types sont un confort, pas la garantie. L’arbre d’options est validé à l’exécution à chaque appel de create(), parce que le mode d’intégration principal est le JavaScript et que les types n’y protègent de rien.

La suite

Mettez en place la récupération du jeton — le widget ne démarrera pas une mesure sans elle — puis branchez les événements.