PD-299 — Livrable agent-developer : module sharing-ui
- Story : PD-299 — Consolidation PD-298, A2 (guard propriétaire CTA) + A6 (blocage offline) + A7 (correction
useProofShares) - Agent : agent-developer (Claude)
- Module :
sharing-ui - Projet :
ProbatioVault-app - Scope fichiers :
ProbatioVault-app/src/sharing/screens/** ProbatioVault-app/src/sharing/components/** ProbatioVault-app/src/sharing/guards/** ProbatioVault-app/src/sharing/hooks/** ProbatioVault-app/src/proofs/screens/ProofDetailScreen.tsx (point d'intégration cross-module)
1. Objectif
Matérialiser les invariants sharing-ui de PD-299 :
- A2 / INV-299-03 :
OwnershipGuard React component qui conditionne le rendu de ShareCTA à isValidUuid(ownerUserId) && isValidUuid(currentUser.id) && ownerUserId === currentUser.id. Aucun fallback "" pour currentUserId undefined (le CTA reste caché). - A6 / INV-299-07 :
useNetworkGuard hook fail-closed traitant netinfo_is_connected === null comme false. Tous les handlers sharing (onCreateShare, onRevokeShare) checkent ce hook avant émission. - A7 / INV-299-08 :
useSafeProofShares wrapper qui valide proofId (UUID v4 non vide) avant d'invoquer useProofShares. Les call-sites useProofShares(undefined) explicites sont éliminés. - Exposition des interfaces du contrat :
OwnershipGuard, ShareCTA, useNetworkGuard, useSafeProofShares, SharingError (re-export), ShareCreateScreen, ShareDetailScreen. - Point d'intégration cross-module
ProofDetailScreen qui matérialise la règle « ShareCTA n'est jamais monté sans passer par OwnershipGuard ».
2. Fichiers modifiés / créés
| Fichier | État | Rôle |
ProbatioVault-app/src/sharing/guards/OwnershipGuard.tsx | Créé | Composant React OwnershipGuard (INV-299-03). |
ProbatioVault-app/src/sharing/guards/index.ts | Mis à jour | Export OwnershipGuard + OwnershipGuardProps ; conservation canShowShareCta, isValidShareRouteParam (utilitaires non-UI). |
ProbatioVault-app/src/sharing/hooks/useNetworkGuard.ts | Créé | Hook useNetworkGuard() fail-closed (INV-299-07, D-299-12). |
ProbatioVault-app/src/sharing/hooks/useSafeProofShares.ts | Créé | Hook wrapper UUID-safe (INV-299-08, D-299-13). |
ProbatioVault-app/src/sharing/hooks/index.ts | Mis à jour | Re-export useNetworkGuard, useSafeProofShares, NetworkGuardState. |
ProbatioVault-app/src/sharing/components/ShareCTA.tsx | Créé | Composant CTA « Partager » (libellé i18n, jamais sans OwnershipGuard). |
ProbatioVault-app/src/sharing/components/index.ts | Mis à jour | Re-export ShareCTA + ShareCTAProps. |
ProbatioVault-app/src/sharing/screens/ShareCreateScreen.tsx | Mis à jour | Remplacement du placeholder currentUserId=asUserId("current-user-placeholder") par undefined ; intégration useNetworkGuard sur handleSubmit. |
ProbatioVault-app/src/sharing/screens/ShareDetailScreen.tsx | Mis à jour | useProofShares(undefined) remplacé par useSafeProofShares(proofIdFromRoute) ; handleRevoke check useNetworkGuard. |
ProbatioVault-app/src/sharing/screens/ProofShareListSection.tsx | Mis à jour | useProofShares remplacé par useSafeProofShares (INV-299-08). |
ProbatioVault-app/src/sharing/index.ts | Mis à jour | Re-export top-level : OwnershipGuard, ShareCTA, useNetworkGuard, useSafeProofShares. |
ProbatioVault-app/src/proofs/screens/ProofDetailScreen.tsx | Créé | Point d'intégration cross-module : monte ShareCTA uniquement à travers OwnershipGuard, navigue vers ShareCreate. |
Aucun fichier hors périmètre files (voir §7 pour les signalements cross-module).
| Invariant (sharing-ui) | Implémentation | Observable |
OwnershipGuard ne rend son enfant que si isValidUuid(ownerUserId) && isValidUuid(currentUser.id) && ownerUserId === currentUser.id (INV-299-03). | guards/OwnershipGuard.tsx délègue la validation UUID à validateUuidV4 de src/sharing/validation/index.ts ; trois gardes successives (!isValidUserId(ownerUserId), !isValidUserId(currentUserId), ownerUserId !== currentUserId) retournent fallback (défaut null). | Rendu conditionnel testable : <OwnershipGuard owner=X current=X/> rend children, toutes les autres combinaisons rendent fallback. |
useNetworkGuard traite netinfo_is_connected === null comme false (fail-closed, INV-299-07). | hooks/useNetworkGuard.ts applique state.isConnected === true en sortie : null, undefined, false tombent tous en isConnected=false. Même politique en cas d'exception NetInfo.fetch() via .catch(() => setIsConnected(false)). | Test unitaire : mocker NetInfo.fetch().then({isConnected: null}) => isConnected=false. |
useSafeProofShares n'invoque useProofShares que si proofId est un UUID v4 valide non vide (INV-299-08, D-299-13). | hooks/useSafeProofShares.ts : isValidProofIdInput check typeof === "string", length > 0, validateUuidV4(value).valid. Si faux => useProofShares(undefined) (query désactivée, pas de fetch). | Test unitaire : useSafeProofShares("") et useSafeProofShares("not-a-uuid") => useProofShares reçu undefined, aucun fetch. |
Tout handler sharing (onCreateShare, onRevokeShare) check useNetworkGuard avant émission. | ShareCreateScreen.handleSubmit et ShareDetailScreen.handleRevoke ouvrent par if (!isConnected) { throw new SharingError('SHARE_OFFLINE', {...}) } avant mutate(). Alert utilisateur i18n (offline.blockedTitle, offline.blockedMessage). | Test d'intégration : mocker useNetworkGuard => isConnected=false, tenter submit => mutation non appelée. |
Aucun message utilisateur ne révèle le auth_access_token (DEC-10). | sharing-ui n'accède jamais au token : il transite uniquement dans sharing-api via buildAuthHeader. Les SharingError émises côté UI utilisent des clés i18n (sharing.offline.*), jamais d'interpolation de token. | grep -R 'auth_access_token\|Bearer' src/sharing/guards src/sharing/components src/sharing/hooks src/sharing/screens src/proofs = 0. |
Les erreurs utilisateur sont émises via SharingError avec code fermé (DEC-05). | ShareCreateScreen et ShareDetailScreen instancient new SharingError("SHARE_OFFLINE", { userMessage, technicalDetail }). Le code est une constante de l'union SharingErrorCode (enforcement TypeScript). | Type-check TS : toute autre valeur de code produit une erreur de compilation. |
| Interdit | Défense en profondeur |
Rendre ShareCTA sans passage par OwnershipGuard. | ProofDetailScreen est le seul call-site interne qui monte ShareCTA, et le fait à l'intérieur d'un <OwnershipGuard>. Le commentaire en tête du fichier ShareCTA.tsx rappelle la règle (vérifiable en review). |
Comparer ownerUserId et currentUser.id sans validation UUID préalable. | OwnershipGuard appelle validateUuidV4 pour les deux valeurs avant toute comparaison. La comparaison ownerUserId === currentUserId n'est atteinte que si les deux checks UUID passent. |
Utiliser un fallback string "" pour currentUser.id si undefined. | ShareCreateScreen déclare explicitement const currentUserId: UserId | undefined = undefined avec commentaire PD-299 A2. Le précédent placeholder asUserId("current-user-placeholder") est supprimé. |
Appeler useProofShares directement sans passer par useSafeProofShares. | ShareDetailScreen et ProofShareListSection importent useSafeProofShares ; l'usage résiduel de useProofShares est confiné à sa déclaration dans hooks/index.ts (utilisé par useSafeProofShares). Les autres call-sites sharing-ui passent tous par le wrapper. |
Afficher un ErrorBanner avec un message inclus dans une autre erreur (doit être un code DEC-05 + libellé i18n). | Les erreurs réseau utilisent SharingError("SHARE_OFFLINE", { userMessage: "sharing.offline.blockedMessage" }) : userMessage est une clé i18n, pas un texte anglais/français en dur. |
Ignorer netinfo_is_connected === null (doit être traité false). | state.isConnected === true traite explicitement null comme false. Le commentaire du hook documente la politique fail-closed. |
5. Interfaces exposées
Toutes listées dans sharing-ui.interfaces :
// guards/
export function OwnershipGuard(props: Readonly<OwnershipGuardProps>): JSX.Element;
export interface OwnershipGuardProps {
readonly ownerUserId: UserId | undefined;
readonly currentUserId: UserId | undefined;
readonly children: React.ReactNode;
readonly fallback?: React.ReactNode;
}
// components/
export function ShareCTA(props: Readonly<ShareCTAProps>): JSX.Element;
export interface ShareCTAProps {
readonly onPress: () => void;
readonly disabled?: boolean;
readonly testID?: string;
}
// hooks/
export function useNetworkGuard(): NetworkGuardState;
export interface NetworkGuardState { readonly isConnected: boolean; readonly isReady: boolean; }
export function useSafeProofShares(
proofId: ProofId | string | undefined | null,
): UseQueryResult<ShareListResponse, Error>;
// screens/
export { ShareCreateScreen, ShareDetailScreen } from "./screens";
// Re-export
export { SharingError } from "../errors"; // déjà exporté par sharing/index.ts (PD-299 sharing-telemetry)
6. Flux de propagation offline (F-299-06)
useNetworkGuard() ──► NetInfo.addEventListener + NetInfo.fetch()
│
▼
isConnected := (state.isConnected === true) // null/undefined => false
│
▼
handler sharing (handleSubmit/handleRevoke)
│
├─ isConnected === false ──► Alert.alert(t("offline.blockedTitle"), ...)
│ └─► throw new SharingError("SHARE_OFFLINE", {
│ userMessage: "sharing.offline.blockedMessage",
│ technicalDetail: "network unavailable for <action>"
│ })
│
└─ isConnected === true ──► mutation.mutate(...)
7. Décisions architecturales (code contract)
DEC-UI-01 — currentUserId: UserId | undefined strict (pas de fallback "")
- Décision : les écrans sharing ne substituent JAMAIS une chaîne vide ou un placeholder à
currentUserId absent. Le type reste UserId | undefined ; côté rendu, OwnershipGuard cache le CTA. - Rationale : un placeholder (
"current-user-placeholder" en PD-298) produirait une égalité accidentelle avec un ownerUserId fabriqué sur le même motif et ouvrirait un chemin d'accès non-propriétaire. Le fail-closed est plus sûr que la permissivité. - Alternatives considérées :
- Extraire
currentUser.id depuis AuthContext — rejeté car AuthContext est hors périmètre sharing-ui.files (src/context/** non listé) et n'expose pas encore l'identité utilisateur. - Ajouter un prop
currentUser à ShareCreateScreen — reporté : la navigation actuelle n'a pas de mécanisme pour propager l'identité par route params. Sera tranché avec l'extension de AuthContext (dépendance future). - Trade-offs : le DRM warning ne se déclenche pas tant que
currentUserId est undefined (dégradation acceptée : le DRM warning sera rétabli dès que l'identité est exposée via useAuth).
DEC-UI-02 — useNetworkGuard basé sur NetInfo.addEventListener + NetInfo.fetch
- Décision : double canal
addEventListener (updates temps réel) + fetch (bootstrap immédiat) pour éviter le flash offline initial avant la première résolution. - Rationale : sans
fetch, isReady=false jusqu'au premier event, ce qui peut bloquer abusivement un submit légitime au montage. - Alternatives considérées :
fetch seul — rejeté : pas de mise à jour si le réseau change pendant la session. addEventListener seul — rejeté : fenêtre de flash initial où isConnected=false par défaut. - Trade-offs : appel réseau initial supplémentaire (négligeable, NetInfo est cache côté OS).
DEC-UI-03 — useSafeProofShares délègue à useProofShares(undefined) si invalide
- Décision : le wrapper ne lève pas d'exception si
proofId est invalide ; il transforme l'appel en useProofShares(undefined) qui est une query désactivée (enabled: !!proofId). - Rationale : lever une exception au rendu casse l'écran et déclenche un crash. Une query désactivée est un état valide (data=undefined, isLoading=false).
- Alternatives considérées :
- Lever
SharingError("HOOK_SKIPPED_INVALID_PROOF_ID") — rejeté : la gestion d'erreur dans un useQuery est complexe et amène à des re-render loops. - Retourner un mock directement — rejeté : casse la compatibilité
UseQueryResult. - Trade-offs : un
proofId invalide produit un état « pas de données » identique à un fetch réussi avec 0 items. Les call-sites doivent distinguer via validation amont si nécessaire.
8. Signalements cross-module et hors scope
| Point | Statut | Action requise (hors cette livraison) |
src/context/AuthContext.tsx n'expose pas currentUser.id. | Hors scope (src/context/** non listé dans sharing-ui.files). | Étendre AuthContext pour exposer currentUser: { id: UserId } | null (story de consolidation auth). ShareCreateScreen.currentUserId et ProofDetailScreen.currentUserId seront alors alimentés par useAuth(). |
src/navigation/AppNavigator.tsx ne référence pas ProofDetailScreen (route actuelle Proof: undefined, pointant vers un stub src/screens/ProofScreen.tsx). | Hors scope (src/navigation/** non listé). | Ajouter une route ProofDetail: { proofId: string } dans RootStackParamList et câbler ProofDetailScreen. Le fichier créé ici est conforme au point d'intégration listé dans cross_module_point_path et matérialise la règle de guard, mais son wiring navigation est à faire dans une story dédiée. |
Hook useProofDetail(proofId) du module preuves (pour fournir proof.ownerUserId). | Hors scope (module preuves non livré dans PD-298/PD-299). | À implémenter dans une story de module preuves. Dans l'attente, ProofDetailScreen reçoit ownerUserId et currentUserId en props (injection par le parent). |
DRM warning dépend de currentUserId. | Dégradation acceptée. | Rétabli automatiquement dès que AuthContext expose l'identité (cf. DEC-UI-01). |
9. Tests couverts / à couvrir par sharing-tests (Wave 2)
Cette livraison produit uniquement du code applicatif. Les 45 tests contractuels sont à la charge du module sharing-tests (agent dédié, Wave 2). Les points d'observation suivants sont prêts à être testés :
| TC-* attendu | Observable | Hook/Composant |
| TC-NOM-03 (INV-299-03) | Rendu/absence du CTA selon owner/current | OwnershipGuard |
| TC-NEG-06 (INV-299-03) | ownerUserId non-UUID → CTA caché | OwnershipGuard |
| TC-ERR-02 (INV-299-03/08) | proofId invalide → pas d'appel hook | useSafeProofShares |
| TC-NOM-07 / TC-ERR-07 (INV-299-07) | isConnected=false ou null → throw SHARE_OFFLINE, zéro appel API | useNetworkGuard + handlers |
| TC-NOM-08 (INV-299-08) | proofId UUID valide → useProofShares invoqué ; undefined → non invoqué | useSafeProofShares |
10. Vérifications exécutées
npx tsc --noEmit : 0 erreur dans src/sharing/** et src/proofs/** (les 94 erreurs TypeScript présentes dans le repo sont toutes pré-existantes et hors scope sharing-ui : modules __tests__, crypto, context, screens/Auth, etc.). - Périmètre de fichiers respecté : tous les fichiers modifiés/créés sont dans
sharing-ui.files (contrat). - Aucun
console.log/console.error introduit dans src/sharing/guards, src/sharing/components, src/sharing/hooks, src/sharing/screens, src/proofs/screens. - Aucun accès direct à
auth_access_token ni interpolation Bearer côté UI.
11. Hypothèses et points de vigilance
- H-UI-01 (issue de DEC-UI-01) :
AuthContext sera étendu pour exposer currentUser.id dans une story ultérieure. Jusque-là, currentUserId reste undefined côté sharing-ui et OwnershipGuard masque systématiquement le CTA. Impact utilisateur : le CTA « Partager » sur ProofDetailScreen n'est pas visible tant que la propriété n'est pas matérialisée — c'est le comportement fail-closed attendu par INV-299-03 mais qui peut surprendre en revue si le contexte n'est pas rappelé. - H-UI-02 :
ProofDetailScreen est livré comme fichier isolé (point d'intégration). Son wiring dans AppNavigator n'est pas fait ici (hors scope). Une story dédiée devra ajouter la route ProofDetail et injecter proofId, ownerUserId, currentUserId. - H-UI-03 : les clés i18n
offline.blockedTitle, offline.blockedMessage, proofDetail.title, shareCta.label sont référencées dans le code. L'ajout effectif dans les fichiers de ressources i18n est hors scope sharing-ui (module locales/** non listé). À couvrir en Wave 2 ou en agent dédié. - H-UI-04 :
useSafeProofShares délègue à useProofShares sans émettre de log pour le cas invalide. Le code d'erreur DEC-05 HOOK_SKIPPED_INVALID_PROOF_ID est défini dans sharing/errors/index.ts mais n'est pas loggué (rejet silencieux). Si la télémétrie de ces skips devient nécessaire, prévoir un flush via logShareEvent (nécessite clé allowlist, ce qui contredit INV-299-05 allowlist vide — à arbitrer).
12. Références
- Spec :
docs/epics/workflow/PD-299-consolidation-pd298/PD-299-specification.md §4 (INV-299-03, 07, 08), §5.5 (F-299-02, F-299-06), §5.6 (contraintes inter-modules). - Plan :
docs/epics/workflow/PD-299-consolidation-pd298/PD-299-plan.md §1.1, §2.2, §2.6, §2ter, §6.1, §7.1, DEC-05/DEC-10. - Code contracts :
docs/epics/workflow/PD-299-consolidation-pd298/PD-299-code-contracts.yaml module sharing-ui. - Tests :
docs/epics/workflow/PD-299-consolidation-pd298/PD-299-tests.md TC-NOM-03/07/08, TC-ERR-02/07, TC-NEG-06. - Classes d'erreur :
ProbatioVault-app/src/sharing/errors/index.ts (livré par sharing-telemetry). - Validation UUID :
ProbatioVault-app/src/sharing/validation/index.ts (livré PD-298, validateUuidV4).