Aller au contenu

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).

3. Conformité aux invariants contractuels

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.

4. Conformité aux interdits contractuels

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 AuthContextrejeté 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 à ShareCreateScreenreporté : 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).