Hooks
Là où votre code peut refuser ce que le widget s’apprête à faire, borné de sorte qu’il ne puisse jamais figer le parcours
Les événements vous laissent observer. Les hooks vous laissent agir.
Un hook est attendu, et un false littéral refuse ce qu’il annonce. Il y en a deux, et la raison pour laquelle il n’y en a que deux est un argument de sûreté qui mérite d’être lu avant d’utiliser l’un ou l’autre.
const instance = SaphereScan.create("#scan", {
hooks: {
beforeScreenChange: ({ from, to }) => to !== "/measurement" || quotaRemaining()
},
proxy: { retrieveAccessToken: { strategy: "delegate", url: "/api/saphere-token" } }
})
Le contrat
Quatre règles, et elles valent pour chaque hook.
Seul un false littéral refuse. Tout le reste laisse passer — undefined, null, true, 0, une chaîne vide, un objet. Une fonction qui effectue une vérification et oublie de faire un return n’arrête donc rien, ce qui est la direction sûre pour cette erreur.
Un délai de garde de deux secondes, et il n’est pas configurable. Au-delà, le widget poursuit comme si le hook n’avait pas répondu. Le délai est une constante et non une option à dessein : le hook retient une transition pendant toute sa durée sans indicateur d’attente à l’écran, de sorte qu’une valeur de soixante secondes serait un widget figé soixante secondes. Un hook qui dépasse le délai est rapporté en console — c’est le seul cas qu’aucun événement ne porte, parce qu’il se résout en autorisation et qu’il n’y a rien à observer.
Une exception ou un rejet laisse passer, et est rapporté en console. L’échec de votre code n’est jamais une raison d’arrêter la mesure d’un utilisateur, et il n’est jamais rapporté comme une erreur du widget.
Aucun hook ne s’exécute pendant l’acquisition. Les deux hooks se situent à des moments où l’utilisateur est inactif. Rien de ce qui se produit pendant les trente secondes de capture ne peut être retenu par du code d’intégrateur — par construction, non par convention.
Pourquoi ne pas simplement lire la valeur de retour d'`onEvent` ?
Le widget v1 faisait exactement cela, et cela signifiait qu’un gestionnaire lent pouvait bloquer une mesure. Garder l’observation sans retour ni attente et placer la faculté de refuser dans une surface distincte et bornée dans le temps est ce qui supprime ce risque tout en conservant la capacité.beforeScreenChange
Appelé avant chaque transition d’écran. Reçoit les deux écrans tels que l’intégrateur les voit.
hooks: {
beforeScreenChange: async ({ from, to }) => {
if (to !== "/measurement")
return true
// Demandez à votre propre serveur s'il reste une mesure à cet utilisateur
const { allowed } = await fetch("/api/quota").then(response => response.json())
return allowed
}
}
| Contexte | { from, to } — les mêmes chemins que rapporte screen:enter |
Effet d’un false | La navigation n’a pas lieu. L’utilisateur reste où il est. |
| Rapporté par | screen:blocked avec from et to |
Un refus laisse l’utilisateur sur un écran dont les commandes fonctionnent toujours. C’est toute la raison pour laquelle ce hook est défendable : il peut appuyer à nouveau sur le bouton, ou revenir en arrière.
Les écrans sur lesquels il n’est jamais consulté
Trois, plus le premier, et ces exclusions ne relèvent pas du conservatisme.
Le tout premier écran. Il est atteint par une redirection, sans écran précédent. Le refuser laisserait le widget vide, sans rien sur quoi l’utilisateur puisse agir.
/result et /error. Refuser l’écran de résultat laisserait l’utilisateur bloqué à jamais sur l’écran d’attente, après que measure:result vous a déjà été délivré. Refuser l’écran d’erreur le priverait du message qui explique ce qui s’est passé.
/load-face-detectors. Le refuser rend toute mesure impossible, sans erreur et sans bouton — une impasse silencieuse.
Ces trois écrans sont exclus des deux côtés d’une transition, ce qui a un second effet à connaître : l’aller-retour interne par l’écran de chargement du détecteur puis le retour à la mesure ne compte plus pour deux questions posées pour un seul geste de l’utilisateur.
beforeMeasureStart
Appelé au moment où le visage est accepté, juste avant que le cadrage ne soit figé et que l’acquisition ne commence.
hooks: {
beforeMeasureStart: async () => {
const { allowed } = await fetch("/api/quota", { method: "POST" })
.then(response => response.json())
return allowed
}
}
| Contexte | {} |
Effet d’un false | La tentative prend fin. |
| Rapporté par | measure:aborted avec le code REFUSED_BY_INTEGRATOR |
C’est le dernier moment où une mesure peut être déclinée sans gaspiller les trente secondes de capture, ce qui en fait le bon endroit pour une vérification de quota ou une dernière porte de consentement.
Un refus ici est terminal
Il met fin à la tentative plutôt que de la suspendre. L’écran de placement ne peut pas revenir en arrière une fois le départ décidé, et en mode automatique — le mode par défaut — un refus réversible redemanderait l’autorisation à chaque fois que l’utilisateur tient la pose. La tentative emprunte donc le chemin d’échec existant :measure:aborted, puis l’écran calme de fin de mesure.REFUSED_BY_INTEGRATOR est classé parmi les arrêts, non parmi les pannes. Rien n’est cassé ; quelqu’un a dit non. Cette classification est aussi ce qui sélectionne l’écran le plus calme pour l’utilisateur, plutôt qu’un écran intitulé comme une erreur.
Le contexte est vide plutôt que de porter un numéro de tentative, parce que la numérotation vit un niveau au-dessus. Corrélez avec l’événement measure:attempt qui l’a immédiatement précédé.
Ce à quoi les hooks ne servent pas
Un hook est une porte, non une source de données. Deux choses qu’il ne peut délibérément pas faire :
Il ne peut pas fournir le jeton d’accès. C’est le rôle de proxy.retrieveAccessToken, qui a ses trois stratégies et son propre délai. Un hook autour de la récupération du jeton retiendrait une mesure dont les captures s’accumulent déjà.
Il ne peut pas retenir le résultat. Au moment où un résultat existe, la mesure est calculée et payée. measure:result le rapporte ; rien ne le filtre.
Les deux ensemble
Une intégration de production typique utilise un hook et lit plusieurs événements :
const instance = SaphereScan.create("#scan", {
lang: "fr",
proxy: {
retrieveAccessToken: { strategy: "delegate", url: "/api/saphere-token" }
},
hooks: {
// Une décision, au dernier moment libre avant trente secondes de capture.
beforeMeasureStart: () => quota.consume()
},
onEvent: event => {
switch (event.type) {
case "screen:blocked":
analytics.track("scan_blocked", { to: event.to })
break
case "camera:error":
support.log("camera refused", event.code, event.name)
break
case "measure:result":
save(event.result)
break
case "measure:aborted":
analytics.track("scan_abandoned", { code: event.code })
break
case "measure:failed":
support.log("scan failed", event.code)
break
}
}
})
await instance.bootstrap()