PD-298 — Plan d'implémentation¶
Projet cible :
ProbatioVault-app(React Native + Expo SDK 54 + TypeScript) Backend consommé :ProbatioVault-backendvia endpoints PD-287 (DONE) Dépendances natives validées PO :react-native-safe-area-context,@react-native-community/datetimepickerTraçabilité : specPD-298-specification.mdv2026-04-22, testsPD-298-tests.md, review step3PD-298-review-step3.md.
1. Découpage en composants¶
La story est décomposée en 10 modules ; la liste correspond 1→1 aux entrées du fichier PD-298-code-contracts.yaml (un module = un agent step 6b).
| # | Module | Rôle | Racine fichiers |
|---|---|---|---|
| C01 | sharing-types | Types TypeScript, branded types (ShareId, ProofId, UserId), énums ShareState/EventType, payloads API, schémas Zod partagés. | src/sharing/types/ |
| C02 | sharing-validation | Validation locale : email (regex D-287-03 via hypothèse HT-01), TTL [15..43200], UUID v4, bornes numériques, normalisation email contractualisée (lowercase + trim, pas de dot-removal). | src/sharing/validation/ |
| C03 | sharing-masking | Fonctions déterministes : maskIp(raw) v4 x.x.*.* et v6 (4 premiers hextets + *:*:*:*), scrubEmail(value) pour télémétrie. | src/sharing/masking/ |
| C04 | sharing-drm-prefs | Lecture/écriture de drmWarningSeen(userId) en AsyncStorage, clé @pv/sharing/drm-seen/<userId>, purge au logout via hook onLogout existant. | src/sharing/drm-prefs/ |
| C05 | sharing-api-client | Client HTTP typé pour les 4 endpoints PD-287 : POST /shares, GET /shares?offset&limit&state, POST /shares/:id/revoke, GET /shares/:id/events. Validation Zod à la réponse. Timeout 30 s. Idempotency-key UUID v4 sur POST (HT-07). | src/sharing/api/ |
| C06 | sharing-hooks | Hooks React : useCreateShare, useShareList, useProofShares, useShareDetail, useShareEvents, useRevokeShare, useOwnership, useDrmWarning. Cache-busting pour forcer l'appel réseau à chaque mount (no-cache INV-298-05). | src/sharing/hooks/ |
| C07 | sharing-components | Composants UI : ShareCreateForm, TtlPicker (presets + custom @react-native-community/datetimepicker), StateBadge, RgpdNotice, DrmWarningModal, RevokeConfirmModal (ARB-7), ShareListItem, EventListItem, EmptyJournalMessage. Tous 100 % i18n. | src/sharing/components/ |
| C08 | sharing-screens | 5 points d'entrée écran : ShareCreateScreen, ProofShareListSection (embarquée dans l'écran preuve existant), MyShareScreen (infinite scroll), ShareDetailScreen, ShareEventsScreen. | src/sharing/screens/ |
| C09 | sharing-guards | Cross-module : guard de propriété UI sur fiche preuve (CTA caché si proof.owner_user_id !== currentUser.id), guard de navigation React Navigation (route share/:id validée UUID v4 avant mount), purge state au 401. | src/sharing/guards/ + override dans src/screens/proofs/ProofDetailScreen.tsx |
| C10 | sharing-telemetry | Journalisation structurée sans PII : logShareEvent({shareId, action, …}), interdiction d'émettre recipientEmail ; wrappers Sentry beforeSend pour scrub PII des breadcrumbs réseau/rendu (S-01, S-02). | src/sharing/telemetry/ |
1.1 i18n (transverse)¶
Toutes les chaînes sont introduites dans le bundle unique src/i18n/fr/sharing.json. Aucune chaîne literal n'est autorisée dans C07/C08/C09. Les clés sont typées via le générateur i18next existant.
1.2 Définitions de placeholders contractuels¶
Les textes normatifs absents (écarts bloquants review step 3 : A-01, A-04, A-05) sont injectés via clés i18n dédiées à valeurs placeholder ; la bascule vers le texte officiel se fait par simple update du bundle FR sans recompilation logique :
| Clé i18n | Défaut placeholder (MVP dev/test) | À fournir par |
|---|---|---|
sharing.arb7.revoke.title | « À définir — PD-287 ARB-7 » | Product/Legal (HT-03) |
sharing.arb7.revoke.body | idem | idem |
sharing.arb8.drm.title | « À définir — PD-287 ARB-8 » | Product/Legal (HT-03) |
sharing.arb8.drm.body | idem | idem |
sharing.rgpd.inline_notice | Template RGPD structuré (finalité/base/rétention/destinataires/droits) avec {{retention_days}} | Legal (HT-02) |
sharing.validation.email.pattern_version | "fallback-rfc5322" si D-287-03 non fourni ; sinon "d-287-03" | Backend PD-287 (HT-01) |
2. Flux techniques¶
F1 — Création d'un partage (F-298-01)¶
ProofDetailScreenpasseproof.id,proof.owner_user_idau bouton « Partager ».useOwnership(proofId)compare àauth.user.id; sifalse, le CTA n'est jamais monté (INV-298-12).- Navigation vers
ShareCreateScreen.useDrmWarning(userId): - lit
drmWarningSeenen AsyncStorage ; - si
false, monteDrmWarningModal(contenu ARB-8) ; à l'acquittement,setDrmWarningSeen(true); - si
true, rend le formulaire directement. - Formulaire
ShareCreateForm: email(validé viavalidateEmail) ;ttlMinutesviaTtlPicker(presets{15,60,1440,10080,43200}+ custom datetimepicker, validation[15..43200]) ;maxViewsentier optionnel ≥1 ;notificationsEnabledbooléen ;RgpdNoticemonté au-dessus du CTA submit, visibilité vérifiée avant soumission par mesuremeasureInWindow> 0 (INV-298-06, ERR-298-10).- À la soumission :
- validation locale ; si échec, aucun appel (INV-298-01, ERR-298-01, ERR-298-08).
- génération
idempotencyKey = uuidv4(); sharesApi.create({proofId, recipientEmail, ttlMinutes, maxViews?, notificationsEnabled}, {idempotencyKey});- bouton submit
disableddurant in-flight (anti double-tap, H-03) ; - validation Zod de la réponse 201 ; mapping vers
ShareSummary; invalidationqueryClient.invalidateQueries(['proof-shares', proofId]). - Retour fiche preuve + toast succès + refresh liste locale via
useProofShares.
F2 — Liste par preuve (F-298-02)¶
ProofShareListSection monte useProofShares(proofId) : - appelle GET /shares?proofId=... si endpoint PD-287 le supporte (HT-07) ; sinon filtre côté client à partir de GET /shares?offset=0&limit=20 paginé jusqu'à couverture (à défaut backend, documenté en §9). - pull-to-refresh re-exécute la query avec cacheTime: 0, staleTime: 0 → appel réseau frais garanti (INV-298-05).
F3 — Écran global « Mes partages » (F-298-03)¶
MyShareScreen : - Utilise useInfiniteQuery de @tanstack/react-query avec limit=20 fixe, getNextPageParam = pages.length * 20. - Tri serveur created_at desc (contract API) ; tri additionnel côté client si nécessaire pour stabilité. - Filtres d'état exposés via segmented control → passe state en query param si supporté, sinon filtre post-réponse et refetch. - cacheTime: 0 pour toutes les queries ['shares', ...] (INV-298-05).
F4 — Détail et révocation (F-298-04)¶
ShareDetailScreen(shareId) : 1. Validation UUID v4 au mount (rejet navigation si invalide, TC-NEG-01). 2. useShareDetail(shareId) → GET /shares/:id. À chaque focus écran, refetch forcé. 3. Matrice StateActions: | État | Révoquer | Voir journal | Partager à nouveau | Détails | |------|----------|-------------|--------------------|---------| | PENDING_ACTIVATION | ✅ | ✅ | ❌ | ✅ | | ACTIVE | ✅ | ✅ | ❌ | ✅ | | OTP_BLOCKED | ✅ | ✅ | ❌ | ✅ + message contextuel | | REVOKED | ❌ | ✅ | ❌ | ✅ lecture seule | | EXPIRED | ❌ | ✅ | ❌ | ✅ lecture seule | 4. Sur « Révoquer » : - monte RevokeConfirmModal avec sharing.arb7.revoke.body (i18n, HT-03) ; - confirmation explicite (bouton distinct du bouton de fermeture) ; - avant POST, re-fetch état courant (anti-race H-07) ; si état devenu terminal, abort + toast ; - sinon sharesApi.revoke(shareId, {idempotencyKey}).
F5 — Journal d'accès (F-298-05)¶
ShareEventsScreen(shareId) : 1. GET /shares/:id/events sans cache. 2. Mapping eventType backend → label i18n via EVENT_TYPE_LABELS (HT-04). Valeur inconnue → UNKNOWN_EVENT (tests TC-NEG-06). 3. Chaque ligne : eventAt (format ISO local via Intl.DateTimeFormat), eventType i18n, recipientEmail, maskIp(eventIpRaw), deviceType. 4. EmptyJournalMessage si events.length === 0 : le message distingue « jamais activé » / « pas d'événement récent » via l'état courant du share (PENDING/ACTIVE sans event, OTP_BLOCKED, REVOKED/EXPIRED sans event). 3 clés i18n dédiées (A-10).
2bis. Diagramme de dépendances agents¶
graph LR
subgraph "Wave 1 — Fondations (parallèle)"
A01[agent-types<br/>C01 sharing-types]
A02[agent-i18n<br/>i18n bundle FR]
A03[agent-masking<br/>C03 sharing-masking]
A04[agent-drm<br/>C04 sharing-drm-prefs]
A05[agent-validation<br/>C02 sharing-validation]
end
subgraph "Wave 2 — Infra métier (parallèle)"
A06[agent-api<br/>C05 sharing-api-client]
A07[agent-telemetry<br/>C10 sharing-telemetry]
end
subgraph "Wave 3 — Composition (parallèle)"
A08[agent-hooks<br/>C06 sharing-hooks]
A09[agent-components<br/>C07 sharing-components]
A10[agent-guards<br/>C09 sharing-guards]
end
subgraph "Wave 4 — Intégration"
A11[agent-screens<br/>C08 sharing-screens]
end
subgraph "Wave 5 — Qualité"
A12[agent-tests<br/>tests UT + integration]
end
A01 --> A06
A01 --> A05
A01 --> A08
A05 --> A08
A03 --> A09
A02 --> A09
A04 --> A08
A06 --> A08
A06 --> A07
A08 --> A11
A09 --> A11
A10 --> A11
A11 --> A12
A07 --> A11 Parallélisation : Wave 1 (5 agents en parallèle), Wave 2 (2 agents en parallèle après Wave 1), Wave 3 (3 agents en parallèle après Wave 2), Wave 4 (1 agent), Wave 5 (1 agent). Temps de chemin critique : 5 waves séquentielles.
2ter. Diagramme de séquence enrichi (révocation, manquant dans la spec — C-07)¶
sequenceDiagram
participant U as Propriétaire
participant SD as ShareDetailScreen
participant H as useRevokeShare
participant M as RevokeConfirmModal
participant API as sharing-api-client
participant BE as Backend PD-287
U->>SD: tap "Révoquer"
SD->>M: monter modale + i18n(arb7)
U->>M: lire ARB-7, confirmer explicitement
M->>H: onConfirm()
H->>API: getShare(shareId) (re-check anti-race)
API->>BE: GET /shares/:id
BE-->>API: {state}
alt état terminal (REVOKED/EXPIRED)
H->>SD: toast "État déjà modifié"
SD->>SD: refetch forcé
else état actif
H->>API: revoke(shareId, idempotencyKey)
API->>BE: POST /shares/:id/revoke
BE-->>API: 204 | 409
alt succès
API-->>H: ok
H->>SD: refetch + toast succès
else 409
H->>SD: toast "État déjà modifié"
SD->>SD: refetch forcé
end
end 3. Mapping invariants → mécanismes¶
| Invariant | Exigence | Mécanisme | Composant | Observable | Risque |
|---|---|---|---|---|---|
| INV-298-01 | Email valide avant tout appel. | validateEmail() synchrone avant sharesApi.create ; bouton submit disabled si invalide ; aucune requête émise si !valid. | C02, C07, C05 | Snapshot réseau : 0 POST /shares avec email invalide. | Divergence regex D-287-03 (HT-01). |
| INV-298-02 | ARB-7 exact avant revoke. | RevokeConfirmModal affiche t('sharing.arb7.revoke.body') figé dans le bundle FR ; onConfirm est le seul trigger revoke. Snapshot test de rendu. | C07, C06 | Snapshot string === i18n key ; 0 POST sans confirmation. | Texte ARB-7 non fourni (HT-03, placeholder). |
| INV-298-03 | DRM warning 1ʳᵉ ouverture puis mémorisé. | useDrmWarning(userId) lit AsyncStorage ; si absent → rend modal et await ack ; puis setItem(true). | C04, C07 | Spy AsyncStorage + spy render modal. | Multi-device (S-05, documenté §9). |
| INV-298-04 | TTL ∈ [15..43200], défaut 10080. | TtlPicker presets figés + slider custom clampé ; validateTtl(v) bloque submit. | C02, C07 | Tests bornes (15, 43200, 14, 43201). | — |
| INV-298-05 | Pas de cache persistant. | React Query : cacheTime: 0, staleTime: 0, gcTime: 0 sur clés ['shares', *], ['share-events', *] ; aucun persistQueryClient sur ces clés ; désactivation select/memoization agressive. | C06 | Spy réseau : n ouvertures écran → n appels. | Collision avec drmWarningSeen (C-01) : scope différent (préférence UX). |
| INV-298-06 | Encart RGPD visible avant submit. | RgpdNotice rendu avant SubmitButton ; onLayout + measureInWindow enregistrent la visibilité ; submit bloqué tant que isVisible non true au moins une fois. | C07 | Spy measureInWindow retourne y + h dans le viewport. | Texte RGPD absent (HT-02, placeholder). |
| INV-298-07 | Emails non loggés. | C10 : logger transport ignore champs recipientEmail, eventRecipientEmail ; Sentry beforeSend scrub email dans request.data et breadcrumbs ; console.log banni via ESLint no-console. | C10 | Test scan logs + Sentry mock : 0 email brut. | Analytics tierces non scrubées (S-01, §9). |
| INV-298-08 | Badge = état backend strict. | Enum ShareState = union littérale 5 valeurs ; Zod z.enum rejette autre ; StateBadge = map exhaustif ; état inconnu → erreur bloquante + toast + actions disabled. | C01, C07 | Test Zod parse error ; test rendu erreur. | Valeur inattendue backend (TC-NEG-05). |
| INV-298-09 | IP partiellement masquée. | maskIp(raw) : IPv4 regex → o1.o2.*.* ; IPv6 parse via ipaddr.js (canonical form) → 4 premiers hextets + *:*:*:* (HT-02). Fallback → t('sharing.events.ip_masked_unavailable'). | C03, C07 | Tests unitaires pour v4, v6 canonical, v6 zero-compressed, input invalide. | Règle IPv6 non canonique (HT-03). |
| INV-298-10 | 100 % i18n. | ESLint règle react/no-literal-string activée sur src/sharing/** ; audit i18n-unused en CI. | C07, C08 | Linter vert sur PR ; snapshot no-literal. | Littéraux techniques (HT-09). |
| INV-298-11 | Actions selon état. | Table ALLOWED_ACTIONS: Record<ShareState, readonly ShareAction[]> exhaustive, exposée via canPerform(state, action). Composants testent if (!canPerform(...)) return null. | C06, C07 | Tests matrice 5 états × 5 actions (25 cas). | Divergence backend (HT-07). |
| INV-298-12 | CTA partage owner-only. | useOwnership(proofId) → proof.owner_user_id === auth.user.id ; sinon null rendu. Deep-link share/create/:proofId idem (C09). | C09, C08 | Tests render : composant null si non-owner. | Navigation programmatique (S-11). |
| INV-298-13 | Pagination offset/limit=20. | Constante SHARE_PAGE_SIZE = 20 ; useInfiniteQuery pages.length * 20 comme offset. | C05, C06 | Tests getNextPageParam ; snapshot request URL. | Backend cursor-based (HT-06). |
| INV-298-14 | Transitions hors §5.5 interdites. | Mapping ALLOWED_ACTIONS n'expose que les transitions UI-initiables (revoke uniquement) ; pour les transitions serveur, Zod valide la nouvelle réponse ; transition détectée client inconnue → log + erreur + refetch. | C06, C10 | Test : transition non listée dans response → erreur. | Mal attribuée UI (C-03, cf. §9). |
| INV-298-15 | REVOKED/EXPIRED terminaux. | ALLOWED_ACTIONS : pour ces deux états, actions: ['view_events'] uniquement. Aucune action modifiante rendue. | C06, C07 | Tests matrice (INV-298-11). | — |
| INV-298-16 | Pas d'appels exploratoires. | C05 ne prend en entrée que des ShareId validés UUID v4 ; refuse de concaténer un input utilisateur brut. Pas d'iteration/probing d'IDs. | C05 | Test fuzz : IDs non-UUID → 0 appel. | Critère subjectif (A-09). |
| INV-298-17 | Pas de mode offline. | useNetInfoOnline() ; si !online → toutes les actions sharing renvoient OfflineError immédiat ; pas d'enqueue local. Écrans concernés affichent banner hors-ligne. | C06, C08 | Tests avec NetInfo mock. | UX 3G lente (H-06, §9). |
4. Mapping critères d'acceptation → mécanismes¶
| Critère | Mécanisme(s) | Composant | Observable | Risque |
|---|---|---|---|---|
| CA-298-01 | F1 complet + StateBadge sur PENDING_ACTIVATION. | C05, C06, C07, C08 | HAR : POST /shares 201 + badge rendu. | — |
| CA-298-02 | Validation locale email (INV-298-01). | C02, C07 | 0 appel réseau si email invalide. | HT-01. |
| CA-298-03 | validateTtl (INV-298-04). | C02, C07 | Submit disabled hors bornes. | — |
| CA-298-04 | RevokeConfirmModal + trigger onConfirm uniquement (INV-298-02). | C07, C06 | Modal rendu + 1 appel post-confirm. | HT-03. |
| CA-298-05 | useInfiniteQuery + tri serveur (INV-298-13). | C05, C06, C08 | Requêtes offset=0,20,40…. | HT-06. |
| CA-298-06 | stateFilter param + refetch. | C06, C08 | Appel ?state= observable. | HT-08. |
| CA-298-07 | cacheTime=0 sur clés partage (INV-298-05). | C06 | n affichages → n requêtes. | — |
| CA-298-08 | ShareEventsScreen + EventListItem. | C07, C08 | Colonnes présentes. | HT-04. |
| CA-298-09 | maskIp (INV-298-09). | C03, C07 | Regex rendu côté UI. | HT-03. |
| CA-298-10 | useDrmWarning first-open (INV-298-03). | C04 | Spy AsyncStorage + render. | S-05. |
| CA-298-11 | RgpdNotice + visibilité (INV-298-06). | C07 | measureInWindow > 0. | HT-02. |
| CA-298-12 | useOwnership (INV-298-12). | C09 | CTA null. | — |
| CA-298-13 | ESLint no-literal-string + i18n-unused. | C07, C08 | CI verte. | A-10. |
| CA-298-14 | C10 scrubbing. | C10 | Logs scrubés. | S-01. |
| CA-298-15 | ALLOWED_ACTIONS (INV-298-11). | C06, C07 | Snapshot matrice. | — |
| CA-287-27 | Alias de CA-298-11 (RGPD). | C07 | idem CA-298-11. | HT-02. |
5. Mapping tests (TC-*) → mécanismes + observables¶
| Test | Référence spec | Mécanisme(s) | Observation | Niveau |
|---|---|---|---|---|
| TC-NOM-01 | INV-298-04, INV-298-08, CA-298-01 | F1 complet, StateBadge, default TTL=10080. | Mock HTTP + render tree. | Integration |
| TC-NOM-02 | INV-298-01, CA-298-02 | validateEmail + submit disabled. | Spy fetch = 0. | Unit |
| TC-NOM-03 | INV-298-03, CA-298-10 | useDrmWarning + AsyncStorage spy. | Spy getItem/setItem. | Integration |
| TC-NOM-04 | INV-298-06, CA-287-27, CA-298-11 | RgpdNotice visibility. | measureInWindow mock. | Integration |
| TC-NOM-05 | INV-298-13, CA-298-05 | useInfiniteQuery fixture 45 items. | Requêtes ordonnées offset. | Integration |
| TC-NOM-06 | INV-298-08, CA-298-06 | stateFilter + refetch. | URL capture. | Integration |
| TC-NOM-07 | INV-298-12, CA-298-12 | useOwnership false → null. | Render tree. | Unit |
| TC-NOM-08 | INV-298-11, CA-298-15 | ALLOWED_ACTIONS pour 5 états. | Snapshot. | Unit |
| TC-NOM-09 | INV-298-02, CA-298-04 | Modal ARB-7 + i18n key sharing.arb7.revoke.body. | Render assert + call count. | Integration |
| TC-NOM-10 | ERR-298-02 | Modal dismiss sans confirm. | Spy API = 0. | Integration |
| TC-NOM-11 | CA-298-08, INV-298-09 | EventListItem rendu + maskIp. | Colonnes + regex. | Integration |
| TC-NOM-12 | INV-298-09, CA-298-09 | maskIp v4 + v6. | Tests unitaires. | Unit |
| TC-NOM-13 | INV-298-05, CA-298-07 | cacheTime=0. | Requête fresh à chaque mount. | Integration |
| TC-NOM-14 | INV-298-10, CA-298-13 | ESLint + i18n-unused. | CI job. | Lint |
| TC-NOM-15 | INV-298-07, CA-298-14 | C10 scrubbing. | Spy transport logger. | Unit |
| TC-NOM-16 | INV-298-14, INV-298-15, CA-298-15 | Zod rejet + ALLOWED_ACTIONS vide. | Snapshot. | Unit |
| TC-NOM-17 | INV-298-17 | useNetInfoOnline false. | Render banner + actions disabled. | Integration |
| TC-ERR-01..12 | ERR-298-01..12 | Voir §6. | Tests dédiés par code erreur. | Unit/Integration |
| TC-NR-01..08 | Non-régression | Snapshots + ESLint + tests matrices. | CI verte. | Unit/Integration |
| TC-NEG-01 | INV-298-16 | UUID v4 guard C05. | 0 appel pour IDs invalides. | Unit |
| TC-NEG-02 | INV-298-13 | SHARE_PAGE_SIZE = 20 constante. | URL capture. | Unit |
| TC-NEG-03 | INV-298-01 | validateEmail. | Idem TC-NOM-02. | Unit |
| TC-NEG-04 | INV-298-15 | ALLOWED_ACTIONS terminaux. | Snapshot. | Unit |
| TC-NEG-05 | INV-298-08 | Zod z.enum sur shareState. | Parse error. | Unit |
| TC-NEG-06 | HT-04 (eventType inconnu) | Mapping avec fallback UNKNOWN_EVENT. | Label rendu. | Unit |
| TC-NEG-07 | INV-298-09 | maskIp fallback. | Message i18n. | Unit |
| TC-NEG-08 | INV-298-16 | C05 ne concatène pas d'input. | Audit code. | Static |
Couverture : 17 TC-NOM, 12 TC-ERR, 8 TC-NR, 8 TC-NEG = 45 tests. Aucune exclusion.
6. Gestion des erreurs¶
| Code | Condition | Mécanisme | Observable |
|---|---|---|---|
| ERR-298-01 | Email invalide. | validateEmail bloque submit. | Bouton disabled, 0 appel. |
| ERR-298-02 | Révocation non confirmée. | Modal onConfirm seul déclencheur. | 0 appel revoke. |
| ERR-298-03 | État UI obsolète. | Refetch forcé à chaque focus + invalidation post-mutation. | Appel réseau observable. |
| ERR-298-04 | Email loggé (bug). | C10 + CI lint + Sentry scrub. | 0 occurrence dans logs. |
| ERR-298-05 | Warning DRM non affiché. | Submit bloqué si drmWarningSeen && !ackInSession (premier mount). | Bouton disabled. |
| ERR-298-06 | Journal vide. | EmptyJournalMessage contextuel (3 variantes i18n, cf. A-10). | Texte rendu non vide. |
| ERR-298-07 | Preuve non possédée. | Guard C09 + backend 403 mappé → message générique t('sharing.errors.generic_403') (pas de fuite S-08). | CTA absent, 403 → toast. |
| ERR-298-08 | TTL hors bornes. | validateTtl. | Submit disabled. |
| ERR-298-09 | OTP_BLOCKED. | ShareDetailScreen affiche explication + CTA « Révoquer » visible. | Render conditionnel. |
| ERR-298-10 | Encart RGPD absent avant submit. | Visibility gate (INV-298-06). | Submit disabled. |
| ERR-298-11 | 401 session expirée. | Axios interceptor existant → redirect flux auth ; C10 purge toutes les queries ['shares', *] + in-memory state (S-04). | Navigation auth + queryClient.clear partiel. |
| ERR-298-12 | Timeout/offline. | useNetInfoOnline + axios timeout 30 s → NetworkError + toast i18n. | Toast rendu, 0 faux succès. |
7. Impacts sécurité¶
| ID | Risque | Mitigation |
|---|---|---|
| SEC-01 | PII email dans Sentry/Datadog (S-01). | beforeSend scrub recipientEmail, eventRecipientEmail ; beforeBreadcrumb masque request.data.recipient_email ; test d'intégration mock Sentry. |
| SEC-02 | Artefacts de test contiennent emails/IPs brutes (S-02). | Fixture test utilise pool d'emails factices déterministes ; HAR post-traité par script de scrubbing CI avant archivage ; captures Detox générées sur comptes de test dédiés. |
| SEC-03 | Enumeration anti-pattern côté backend (S-03). | Rappel explicite dans §9 : INV-298-16 est complémentaire à une protection backend PD-287 ; C05 se contente de ne pas itérer. |
| SEC-04 | Résidus en mémoire au logout (S-04). | Hook onLogout (existant) branché sur : queryClient.removeQueries(['shares']), queryClient.removeQueries(['share-events']), C04 purgeAllDrmFlags(), queryClient.removeQueries(['proof-shares']). |
| SEC-05 | DRM warning multi-device faible (S-05). | Documenté §9 comme limitation MVP ; option produit future : déplacer drmWarningSeen côté backend (user-level flag). |
| SEC-06 | Deep-link de création non gardé (S-11). | Route share/create/:proofId valide UUID v4 + appelle useOwnership au mount ; si non-owner, redirect ProofDetailScreen + toast générique. |
| SEC-07 | Pas de rate-limit / anti-spam (S-07). | Submit disabled + debounce 2 s au premier tap ; idempotency-key UUID envoyée au backend pour dedup serveur (H-03). |
| SEC-08 | 403 backend sans fuite (S-08). | Mapping HTTP → i18n générique sharing.errors.generic_403 ; message backend jamais affiché. |
| SEC-09 | Clipboard/sharesheet fuite email (S-06). | ShareListItem email rendu en Text selectable={false} ; pas de Share.share() exposant l'email. |
8. Hypothèses techniques¶
| ID | Hypothèse | Source review | Impact si faux |
|---|---|---|---|
| HT-01 | Regex email D-287-03 est fournie par PD-287 avant step 6b ; sinon fallback RFC 5322 simplifié intégré en C02, pattern_version tracée dans le contrat C05. | A-01, PC-298-01 | Divergence backend/UI silencieuse. Test d'intégration contre backend dev OBLIGATOIRE à Gate 8. |
| HT-02 | Contenu normatif RGPD (finalité, base légale, rétention chiffrée, droits) est fourni par Product/Legal et inséré dans sharing.rgpd.inline_notice ; défaut placeholder explicite jusque-là. | A-04, PC-298-06 | Non-conformité RGPD. Escalade PO avant Gate 8. |
| HT-03 | Textes ARB-7 et ARB-8 sont fournis par PD-287/Legal et insérés dans le bundle i18n. | A-05, NT-03 | INV-298-02 / INV-298-03 non vérifiables contractuellement. Escalade PO avant Gate 8. |
| HT-04 | Mapping eventType backend→UI est aligné sur {CREATION, ACTIVATION, CONSULTATION, EXPORT, ECHEC_OTP, REVOCATION} ; sinon UNKNOWN_EVENT. | A-07, PC-298-03 | Libellés UNKNOWN_EVENT en production. |
| HT-05 | Borne max maxViews n'est pas validée côté client ; l'erreur backend (400) est rendue telle quelle. | A-06, PC-298-02 | Validation partielle, UX dégradée. |
| HT-06 | GET /shares expose offset/limit (pas cursor). À vérifier via Swagger PD-287 avant step 6b. | H-01, H-02 | Réécriture de la liste globale, glissement de périmètre. |
| HT-07 | Matrice transitions backend compatible §5.5 ; POST /shares/:id/revoke renvoie 204/409 documenté. | H-02, PC-298-04 | Divergence machine d'états ; refetch + message État déjà modifié. |
| HT-08 | GET /shares?state= accepté par le backend ; sinon filtre client-side après pagination complète. | CA-298-06 | Pagination moins efficace, documenté §9. |
| HT-09 | ESLint react/no-literal-string toléré sur testID, clés techniques et noms d'icônes ; une allowlist est documentée dans eslint-plugin-i18n.config.js. | NT-01 | Faux positifs lint vs i18n scope. |
| HT-10 | @react-native-community/datetimepicker supporte iOS et Android pour le choix TTL custom, avec fallback modal react-native-modal-datetime-picker si nécessaire. | PO 2026-04-22 | UX incohérente entre OS. |
| HT-11 | ipaddr.js (librairie npm) fournit la normalisation IPv6 canonique nécessaire à maskIp. | A-02 | Règle de masquage IPv6 non déterministe. |
8.1 Plan de levée des hypothèses avant Gate 8¶
| Hypothèse | Action | Deadline |
|---|---|---|
| HT-01 | Récupérer la regex D-287-03 du code backend (ProbatioVault-backend/src/sharing/dto/*.ts). | Wave 1 fin. |
| HT-02, HT-03 | Escalade PO via gov_ask_po ; sans réponse, step 6b démarre sur placeholders mais Gate 8 NON_CONFORME. | Avant Wave 1. |
| HT-06, HT-07, HT-08 | Lire Swagger PD-287 + tests d'intégration contre backend dev. | Wave 2 fin. |
9. Points de vigilance¶
| ID | Vigilance | Source | Mitigation |
|---|---|---|---|
| V-01 | INV-298-05 (no-cache) crée friction sur 3G/edge (H-06). | Review H-06 | Skeleton + spinner ≤ 1 s ; timeout réseau 30 s ; UX documentée. |
| V-02 | drmWarningSeen par device → garantie faible (S-05). | Review S-05 | Option future : flag côté user backend. MVP : documenté. |
| V-03 | INV-298-14/15 sont mal attribués à l'UI (C-03) : la FSM est serveur. | Review C-03 | UI ne fait que refléter l'état. Les tests TC-NOM-16 vérifient que l'UI rejette toute réponse à état inattendu (via Zod). Pas de simulation FSM complète. |
| V-04 | Self-loops state → state : INTERDITE (diagramme) absents de la matrice §5.5. | Review C-06 | UI ne traite pas comme erreur (un refetch peut renvoyer le même état). Documenté dans ALLOWED_ACTIONS : pas d'auto-transition considérée. |
| V-05 | Journal → captures d'écran possibles (fuite PII destinataire). | Review S-09 | Pas de mitigation stricte en MVP ; documenté dans la policy utilisateur. |
| V-06 | TC-NOM-13 non-déterministe en CI (mutation multi-device). | Review NT-07 | Remplacé par mock HTTP qui change shareState entre 2 appels GET successifs. |
| V-07 | ESLint no-literal-string peut introduire des faux positifs. | NT-01, HT-09 | Allowlist testID, identifiants techniques documentée. |
| V-08 | Clock skew UI vs backend sur expiresAt (H-05). | Review H-05 | Afficher « Expire le |
| V-09 | Pagination cursor-based non supportée par plan actuel (HT-06). | Review H-01 | Si backend est cursor-based, adaptation isolée à C05 + C06 ; ne remet pas en cause INV-298-13 si la sémantique (tri desc + page stable) est préservée. |
| V-10 | Test fuzz UUID (TC-NEG-01) doit couvrir lowercase/uppercase. | Spec §5.1 (case-insensitive) | Regex UUID insensible à la casse avec /i. |
10. Hors périmètre¶
- Portail destinataire, deep-linking destinataire (spec §2).
- Push notifications (spec §2) — seul
notificationsEnabledest transmis au backend. - Mode hors-ligne / queue offline (INV-298-17).
- Modification des endpoints backend PD-287.
- RGAA complet (MVP sur i18n + a11y minimale).
- Qualification juridique RGPD (H-298-08).
- Partage multi-preuves.
Mécanismes cross-module (OBLIGATOIRE)¶
La story modifie 1 module externe : ProofDetailScreen (module preuves).
| Élément | Valeur |
|---|---|
| Routes de l'autre module à protéger | Écran ProofDetailScreen (route interne, non exposée backend) — section CTA « Partager ». Route additionnelle share/create/:proofId (nouveau, C08). |
| Controller et méthode | src/screens/proofs/ProofDetailScreen.tsx — ajout d'un bloc conditionnel rendant <ShareCta proof={proof} /> (composant de C09). |
| Effet du guard | ShareCta retourne null si proof.owner_user_id !== auth.user.id. La route share/create/:proofId redirige + toast si non-owner. |
| Mécanisme de jointure cross-schéma | N/A (UI only). Mapping logique : proof.owner_user_id (déjà exposé par l'API preuves) comparé à auth.user.id (store auth existant). |
| Scope d'enregistrement | Local : le guard est un composant React monté par ProofDetailScreen uniquement. Pas de HOC global, pas de middleware navigation global. La route share/create/:proofId est déclarée dans src/navigation/SharingStack.tsx uniquement. |
| Exceptions d'accès | Aucune exception de rôle (admin, support, etc.) spécifiée dans le besoin. Le CTA est strictement owner-only. |
Périmètre de test (OBLIGATOIRE)¶
| Niveau | In scope | Hors scope (justification) |
|---|---|---|
| Unitaire | C01 (types + Zod), C02 (validateEmail, validateTtl, validateUuid, normalizeEmail), C03 (maskIp v4/v6/fallback), C04 (DRM prefs AsyncStorage), C05 (wrappers API + Zod response), C06 (ALLOWED_ACTIONS, useOwnership, pagination, cache-busting), C07 (composants UI en isolation), C09 (guards), C10 (scrubbing logger). Couverture ≥ 80 % Istanbul. | — |
| Intégration | Flux F1 complet (F-298-01), F2 (F-298-02), F3 (F-298-03), F4 (F-298-04), F5 (F-298-05) sur MSW (Mock Service Worker) + @testing-library/react-native. React Query + AsyncStorage + NetInfo mockés. | — |
| E2E | Flux utilisateur principal (création → liste → révocation) via Detox sur builds iOS/Android | Hors scope MVP : infrastructure Detox n'est pas configurée dans ProbatioVault-app à date (pas de job CI Detox). Ticket de suivi : à créer après Gate 8 pour story dédiée « E2E sharing ». Les flux sont couverts en intégration + manuel QA. |
| Lint / statique | ESLint no-literal-string sur src/sharing/**, i18n-unused, tsc --noEmit incrémental post-agent. | — |
| Tests de contrat | Mock responses validées par Zod ; fixtures alignées sur Swagger PD-287 (à vérifier Wave 2, HT-06/07). | — |
Stubs inter-PD : aucun. Toutes les dépendances externes (PD-287 backend) sont DONE et consommées via API. Les placeholders textuels (HT-02, HT-03) ne sont pas des stubs de code mais des valeurs de bundle i18n à mettre à jour ; STUB: PD-287-arb7-content n'est donc pas un stub inter-PD mais une dépendance éditoriale tracée dans §8.
Couverture minimale attendue : 80 % lignes / 80 % branches sur src/sharing/** + src/screens/proofs/ProofDetailScreen.tsx (bloc sharing uniquement). Les placeholders et les UNKNOWN_* fallback sont couverts.
Code contracts similaires (réutilisation décisions)¶
{{SIMILAR_CONTRACTS}}