Skip to content

Après la suppression du compte, l'app reste connectée et appelle GET /me en boucle (Android) #475

Closed

Description

@PaulThiberville

Contexte

Epic : #6
Maquette : E-09 · validée le 2026-09-24 (D-023, D-025, D-028), reprise de #387
Ecrans › E-09 · Compte-et-Monetisation · E-09 · D-025, D-027

Bug trouvé par Paul en recette le 2026-10-15 : build preview EAS 858b93ac (main 1a4845d), Redmi Note 9 (Android), API de staging à jour avec la PR #461.

Après « Supprimer mon compte » (DELETE /v1/me répond 204 à 09:19:01Z) :

  • l'app reste connectée côté session : « Créer un compte ou se connecter » et « Programmer » ne font plus rien, alors que PostHog reçoit auth_prompted à chaque appui (intention plan, puis profile) ;
  • le téléphone envoie GET /v1/me en boucle, environ 8 requêtes par seconde (499 refus 401 par minute sur l'API de staging, user-agent okhttp), sans s'arrêter ;
  • aucune erreur JavaScript n'arrive à Sentry (seule une erreur haptique, sans rapport : voir le ticket haptique lié).

Contournement constaté : forcer l'arrêt de l'app et effacer ses données.

Code en cause probable :

  • apps/mobile/src/profile/me.ts : deleteAccount appelle api.deleteMe() puis session.signOut() ;
  • apps/mobile/src/auth/firebase.ts : signOut appelle GoogleSignin.signOut() puis signOut(auth) ;
  • apps/mobile/src/auth/rights.ts : signOutOnDeletedAccount rappelle signOut() à chaque 401 « Account deleted » ;
  • apps/mobile/src/auth/flow.ts : requireAccount, si isSignedIn(), appelle getFirstName() → GET /me, et en cas d'erreur lance action() sans ouvrir la feuille ;
  • useMe : enabled: uid !== null.

Hypothèse : la déconnexion Firebase ne change pas l'état de session (onUserChanged n'est jamais appelé avec null). Le store reste signedIn, chaque écran relance GET /me, le 401 relance signOut(), qui n'aboutit pas, et la boucle tourne.

Comportement attendu

  • Après un DELETE /me réussi, la session passe à l'état anonyme localement, tout de suite, même si la déconnexion Firebase ou Google échoue ; la déconnexion Firebase suit, sans conditionner l'état local.
  • Un 401 « Account deleted » coupe les requêtes authentifiées : la query me est désactivée et ne se relance pas, et la déconnexion n'est déclenchée qu'une fois.
  • Une fois anonyme, « Créer un compte ou se connecter » et « Programmer » rouvrent la feuille de connexion (E-10).

Critères d'acceptation

  • Après DELETE /me, la session passe anonyme même si la déconnexion Firebase échoue (état forcé localement, puis déconnexion Firebase)
  • Un 401 « Account deleted » désactive la query me (pas de relance) et ne déclenche la déconnexion qu'une seule fois
  • Aucune boucle de requêtes mesurable : au plus un GET /me après la suppression (test)
  • La feuille de connexion se rouvre depuis « Créer un compte ou se connecter » et « Programmer », et une reconnexion Google crée un compte neuf (Aligner la suppression de compte sur D-025 et permettre la réinscription #128)
  • Test unitaire qui reproduit la boucle (déconnexion Firebase sans effet sur la session, 401 répétés) et prouve qu'elle ne tourne plus
  • Recette sur émulateur Android : suppression, retour à l'état anonyme, aucune boucle dans les journaux réseau, reconnexion ; consignée dans la PR

Notes techniques

  • Mesure côté API à envisager : limiter le débit des 401 répétés d'un même jeton. Hors du périmètre de ce ticket ; si CODE la juge utile, il la signale sur l'Inbox 📥 Inbox — Triage #4 pour un ticket area:api séparé.
  • Garder en tête iOS : le correctif porte sur l'état de session, pas sur une branche Android.

Activity

  1. added this to the v0.3 milestone on Oct 15, 2026
  2. PaulThiberville commented on Oct 15, 2026

    @PaulThiberville
    CollaboratorAuthor

    🔨 Démarrage — plan :

    • Cause trouvée dans les journaux de staging (lecture seule) : la boucle de GET /v1/me ne vient pas de la déconnexion Firebase. Elle tourne déjà en 200 avant la suppression (53 requêtes entre 09:18:04 et 09:18:58Z, onglet Profil ouvert), puis en 401 juste après (09:19:02Z), s'arrête quand l'onglet Profil n'est plus affiché (09:19:14 → 09:19:56Z) et reprend au retour. Le GET /v1/routes/search en 200 à 09:19:02.547Z, juste après le DELETE, part sans jeton : la session était bien anonyme. En cause : useMe() rend une fonction refetch neuve à chaque rendu, et ProfileScreen la passe à useFocusEffect(useCallback(() => refetch(), [refetch])) : chaque rendu relance l'effet, donc la requête, donc un rendu. Et query.refetch() ignore enabled: uid !== null : anonyme, la requête part sans jeton (401 « Missing bearer token »).
    • Correctif : refetch stable et sans effet sans compte.
    • Garde-fous des critères : session.endSession() force l'état anonyme localement avant la déconnexion Firebase, sans dépendre de son succès (deleteAccount, 401 « Account deleted ») ; une seule déconnexion par compte supprimé ; requireAccount traite un 401 « Account deleted » comme anonyme et ouvre la feuille ; test qui compte les GET /me sur une API simulée.
    • Recette sur simulateur iOS (faux Firebase, API locale sur base de recette à part), puis émulateur Android s'il est libre.
  3. PaulThiberville commented on Oct 15, 2026

    @PaulThiberville
    CollaboratorAuthor

    🔨 Démarrage — plan :

    Reprise d'un travail interrompu par un redémarrage de session (branche issue/475-deleted-account-signout, trois commits locaux relus et gardés). Trois causes, un commit chacune :

    1. La boucle de GET /me : useMe rendait un refetch neuf à chaque rendu, et l'onglet Profil le passe à useFocusEffect : chaque rendu relançait l'effet, donc une requête, donc un rendu. Une fois anonyme, refetch ignorait enabled et partait sans jeton, refusé en 401, en boucle. Correctif : refetch stable, et sans effet sans compte. Un test sur une API simulée compte les requêtes ; sur le code actuel, il échoue (« The focus effect of the Profil tab runs in a loop »).
    2. La fin de session d'un compte supprimé : session.endSession() passe l'app anonyme tout de suite, sans attendre Firebase, puis demande la déconnexion Firebase, une seule fois. Un échec Firebase part dans Sentry. On l'appelle après DELETE /me et sur un 401 « Account deleted ». requireAccount traite un compte supprimé comme une absence de compte et ouvre la feuille ; une connexion réussie pose l'utilisateur dans la session sans attendre Firebase (Aligner la suppression de compte sur D-025 et permettre la réinscription #128).
    3. La feuille de connexion qui ne s'ouvre plus : fermée par le voile ou par un glissement, elle était refermée une seconde fois par l'état fermé du flux, et gorhom ne la présentait plus. D'où un auth_prompted sans feuille. Sur le code actuel, le test échoue.

    Ensuite : lint, typecheck et tests verts commit par commit, puis recette Android et iOS avec un faux Firebase (patch local jamais commité) sur une API locale, base de recette à part. Une PR Fixes #475, niveau 2.

  4. PaulThiberville commented on Oct 15, 2026

    @PaulThiberville
    CollaboratorAuthor

    Recette sur appareil, déclarée par Paul en session le 2026-10-15 vers 13:40 (Paris), consignée par MANAGER. Build preview EAS 98b537be (main 4b766c5), Redmi Note 9 (Android), API de staging. Paul : « tout est bon ».

    • Suppression du compte → app anonyme ; aucun 401 sur staging en 10 minutes (480/min ce matin) ; la feuille de connexion se rouvre, y compris après fermeture sur le voile ; reconnexion Google → compte neuf. Aucun nouvel événement Sentry, pas de « sign-out failed ».
    • Critères cochés : recette de Paul, tests et recette émulateur de la PR fix(mobile): couper la session d'un compte supprimé et la boucle /me #480.

    Status du Project : Terminés.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions