DOCS/v2/nfc-triggers-plan.md
Thomas Fransolet 488cd501f4 Tags NFC : le plan arrêté après lecture du code
Le tag porte l'URL du QR, rien d'autre : aucune entité, aucune colonne sur
Section, aucun code NFC pour le cas nominal — l'OS lit le tag et c'est le
lien universel qui aiguille. Ce qui reste à écrire, c'est la colle
(assetlinks.json / AASA, routage du deep link), et elle répare au passage le
QR code, qui n'ouvre jamais l'app depuis l'appareil photo.

Prérequis dur : app.myinfomate.be doit être déployé pour servir les
.well-known.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:36:34 +02:00

129 lines
7.5 KiB
Markdown

# Tags NFC — déclencher le contenu depuis un tag
> **Analysé dans le code le 2026-09-16.** Carte kanban : `cards/5-planifie/265-…`
> ⚠️ **Document retourné trois fois** par les précisions de Thomas. Version arrêtée ci-dessous :
> le tag ouvre **l'app mobile sur le bon contenu**, et la page de téléchargement web reste
> le fallback quand l'app n'est pas installée.
---
## Le comportement retenu
```
tag NFC (NDEF : URL du QR)
├─ app installée → App Link / Universal Link → mymuseum-visitapp s'ouvre sur la section
└─ app absente → app.myinfomate.be/download/{instanceId}/{configId}/{sectionId}
→ page « installez l'application » ← comportement actuel, conservé
```
**Le tag porte exactement l'URL du QR.** Pas d'entité `NfcTag`, pas de code opaque, pas
d'endpoint de résolution, pas de colonne sur `Section`. Un tag NFC est un QR code sans caméra —
c'est ce qui rend ce chantier petit.
**Et il n'y a aucun code NFC à écrire pour le cas nominal.** Sur iOS (iPhone XS+) comme sur
Android, l'OS lit le tag NDEF, affiche une bannière et **ouvre l'URL** ; c'est le lien universel
qui aiguille vers l'app. `nfc_manager`, la permission NFC et l'entitlement Core NFC ne servent
qu'à la lecture **in-app** — utile seulement pour le bouton « Scanner un tag » du lot 4.
---
## Ce qui manque vraiment : la colle
### 1. Les liens universels n'existent pas — c'est le cœur du chantier
Aucun `assetlinks.json` (Android) ni `apple-app-site-association` (iOS) n'est servi sur nos
domaines. Le seul du workspace est dans `OpenGlasses`, qui n'est pas à nous.
⚠️ **Ils doivent être servis par le domaine de l'URL du tag**, donc par `app.myinfomate.be`
qui **n'est pas déployé** (absent de `docker-compose-myinfomate.yml`, qui route `myinfomate.be`,
`api.`, `manager.` et `monitor.`). La carte « Déployer le visiteur web », déjà `critical`, reste
un **prérequis dur**.
À réunir avant d'écrire les fichiers : le **SHA-256 de la clé de signature de release** Android
(l'APK se construit par `tool/build_apk.sh`) et le **Team ID + bundle id** iOS. Une empreinte
fausse ne produit aucune erreur visible — le lien s'ouvre simplement dans le navigateur.
### 2. L'app n'écoute pas les deep links entrants
`app_links` est bien au `pubspec.yaml:89`, mais :
```dart
void _listenGlassesDeepLinks() {
if (!kEnableGlasses || !Platform.isAndroid) return; // ← main.dart:68
_deepLinkSub = AppLinks().uriLinkStream.listen(
(uri) => MetaGlassesService.instance.handleDeepLink(uri.toString()),
);
}
```
Tout part vers `MetaGlassesService`, le listener est **coupé sur iOS** et conditionné à
`kEnableGlasses`. Il faut router **par motif d'URL**, sans casser le retour de registration Meta.
### 3. La résolution d'un code est enfermée dans le dialogue du scanner
`ScannerDialog._onDetect` (`ScannerDialog.dart:107`) fait déjà **tout** ce qu'il faut : parsing
des trois formats de QR du terrain, section appartenant ou non à la visite en cours,
`_findConfigurationIdOfSection` quand l'id est brut, proposition d'ouvrir une **autre visite** du
site, et télémétrie. Mais c'est un `State` : la méthode manipule `context`, `Navigator`,
`ScaffoldMessenger`, et capture le navigator **avant** le `pop` pour survivre au démontage.
**Extraire cette résolution dans un service** est le vrai morceau de travail — et il **profite
au QR autant qu'au NFC**. À faire sans perdre les cas limites, qui sont tous des corrections de
bugs réels documentées en commentaires dans le fichier.
### 4. `IsNFCEnabled`, sur le modèle exact de `IsQRCodeEnabled`
`ApplicationInstance.IsQRCodeEnabled` (`ApplicationInstance.cs:57`, défaut `true`) est piloté par
un switch dans `app_configuration_link_screen.dart:400` et consommé là où le bouton scanner
s'affiche : `home_3.0.dart:651`, `configuration_page.dart:433`, `menu_page.dart:302`, et
`visitapp-web` (`[slug]/page.tsx:44`). On ajoute `IsNFCEnabled` au même endroit, même défaut,
même switch.
⚠️ **Ce que le flag pilote, et ce qu'il ne pilote pas.** Il masque le **bouton « Scanner un
tag »** in-app et sert de déclaration commerciale. Il **ne peut pas** empêcher un tag posé au mur
d'ouvrir l'app : le deep link vient de l'OS, et l'app le reçoit sans savoir s'il vient d'un tag
ou d'un QR. Ne pas lui prêter un rôle de verrou qu'il n'aura pas.
⚠️ `Configuration.IsQRCode` (`Configuration.cs:52`) existe aussi mais **n'est lu par aucune app**
code mort. Ne pas dupliquer l'erreur en posant un flag NFC au niveau `Configuration`.
### 5. Sans quoi le NFC sera compté comme du QR dans les stats
`_onDetect` émet `VisitEventType.qrScan`. Un tag qui ouvre l'app par deep link n'a aucun moyen de
se distinguer — sauf à ajouter un paramètre à l'URL encodée sur le tag (`?src=nfc`) et une valeur
`NfcScan` à l'enum (`VisitEvent.cs:48`). ⚠️ L'enum passe par le client généré de
`manager_api_new`, **à éditer à la main**. Sans ça, le canal NFC est invisible dans les stats et
on ne saura jamais s'il sert.
*(À traiter avec :* la page de téléchargement dit « scannez à nouveau le QR code depuis
l'application » — faux pour un tag. Le `?src=nfc` permet aussi d'adapter ce texte.*)*
---
## Découpage
| Lot | Contenu | Estimation |
|---|---|---|
| **0 — Déployer `app.myinfomate.be`** | Carte existante, déjà `critical`. **Prérequis dur** : c'est ce domaine qui sert les `.well-known` | (carte 210) |
| **1 — Liens universels** | `assetlinks.json` + `apple-app-site-association` servis sur `app.myinfomate.be`, intent-filter `autoVerify` Android, Associated Domains iOS. Demande le SHA-256 de la clé de release et le Team ID | **1 j** |
| **2 — Router le deep link entrant** | Découpler `main.dart:68` de `MetaGlassesService`, activer le listener sur iOS, router par motif | **0,5 j** |
| **3 — Extraire la résolution du scanner** | Sortir `_onDetect` de `ScannerDialog` vers un service, sans perdre les cas limites. ✅ **Profite au QR** | **1,5 j** |
| **4 — `IsNFCEnabled`** | Colonne + DTO + client généré à la main + switch manager + i18n FR/EN/NL, et le bouton « Scanner un tag » qu'il pilote (`nfc_manager`, permission Android, entitlement iOS `NDEF`) | **1 j** |
| **5 — Distinguer le canal** | `?src=nfc` sur l'URL encodée, `VisitEventType.NfcScan`, texte de la page de téléchargement | **0,5 j** |
| **6 — Encoder les tags** | **NFC Tools**, 2 s par tag, une URL par point. Geste d'exploitation | **0 j de dev** |
**Total : 4,5 j**, dont les lots 2 et 3 **réparent aussi le QR** — aujourd'hui un QR scanné à
l'appareil photo n'ouvre jamais l'app, même installée.
---
## À savoir avant de le vendre
- **iPhone XS et plus** lisent un tag sans aucune app. Les **iPhone 7 / 8 / X**, non : il leur faut une app ouverte. Android : le NFC doit être activé dans les réglages, ce qui n'est pas universel.
- **L'ouverture n'est pas automatique** : l'OS affiche une **bannière que le visiteur tape**. C'est l'expérience de l'appareil photo sur un QR, pas mieux.
- **Un tag à plat sur du métal ne se lit pas** — tags sur mousse ou NTAG « on-metal ». NTAG213 (144 o) suffit largement pour l'URL.
- **Aucune lecture NFC sur `visitapp-web`** : WebNFC est Chrome/Android uniquement, jamais Safari. Le plan **Essentiel étant web-only**, le NFC ne s'y vend pas — sauf comme « le tag ouvre la page web », qui reste vrai et gratuit.
- **Le NFC ne remplace pas le beacon** : geste volontaire à 1-2 cm, aucun déclenchement à l'approche. Les déclencheurs se cumulent.