diff --git a/.project/concepts/app-security/knowledge_authentication.md b/.project/concepts/app-security/knowledge_authentication.md
index 74f6c11..bb344bd 100644
--- a/.project/concepts/app-security/knowledge_authentication.md
+++ b/.project/concepts/app-security/knowledge_authentication.md
@@ -11,6 +11,7 @@ summary: L'identité d'un utilisateur = son wallet NextGraph ; tous les utilisat
- La **barrière d'accès** (`AccessGateScreen`, rendue par `src/app/AuthGate.tsx`) est le vrai login NextGraph : elle ouvre le wallet partagé via la redirection broker. **Dans le même acte**, l'utilisateur saisit un **identifiant** qui nomme son espace virtuel (`onEnter`). Il n'y a **plus d'écran « login perçu » séparé** (l'ancien `ConnexionScreen` « choisissez un nom d'utilisateur » a été retiré — cf. [[decision_2026-07-06_identifier-at-access-barrier]] ; supersede le flux à deux écrans de [[decision_2026-06-15_shared-wallet-login-flow]]).
- Cet **identifiant est un id technique** (un pseudo en pratique, **pas** un username Festipod) : il est **normalisé** (trim, `@` retiré, **minuscules**) puis persisté (`AccountContext` → `IdentityStore`), donc un rechargement — ou un autre appareil rouvrant le même wallet partagé — retombe sur le même espace. C'est cet id qui est donné au SDK (`setCurrentUser`) et sur lequel les caps et le compte shim sont clés.
+- **Saisi UNE SEULE FOIS au premier accès + prérempli au retour.** Au rechargement / retour du round-trip broker, l'identifiant est déjà stocké, mais la session NG n'est pas restaurée d'office (`NextGraphContext` repart en `disconnected`) : `AuthGate` réaffiche donc la barrière tant que `status !== 'connected'`. Le champ d'`AccessGateScreen` est alors **prérempli** avec la valeur stockée (prop `initialIdentifier`, passée par `AuthGate` depuis `useAccount().username`) — l'utilisateur ne le retape jamais et ne voit jamais un champ nu et vide à l'arrivée. Il n'est réellement saisi qu'au **premier accès** (aucune valeur stockée). Régression gardée par `src/modules/auth/features/barriere-acces-identifiant.feature` (@ui) — d'autant plus utile que le flux de barrière est **désactivé** dans les tests @e2e (`__FESTIPOD_ACCESS_GATE_DISABLED__`), donc invisible à cette couche.
- Une fois la session ouverte, l'utilisateur courant et son accès aux stores par scope sont fournis par `NextGraphContext`.
## Le wallet de test
diff --git a/src/app/AuthGate.tsx b/src/app/AuthGate.tsx
index 8b6a245..a3d1e24 100644
--- a/src/app/AuthGate.tsx
+++ b/src/app/AuthGate.tsx
@@ -49,12 +49,23 @@ export function AuthGate({ children }: { children: ReactNode }) {
// Access barrier — shown until BOTH the wallet is open AND the space is named.
// "Entrer" records the identifier (persisted immediately, so it survives the
// broker redirect) and, if the wallet isn't open yet, triggers the connect.
+ //
+ // On return (reload / broker round-trip) the identifier is already stored, so
+ // we PREFILL the field with it (`initialIdentifier`) — the user never sees a
+ // bare empty prompt they must re-type. It is captured ONCE, at first access.
if (status !== 'connected' || !username) {
const onEnter = (identifier: string) => {
login(identifier);
if (status !== 'connected') connect();
};
- return ;
+ return (
+
+ );
}
// The app.
diff --git a/src/modules/auth/features/barriere-acces-identifiant.feature b/src/modules/auth/features/barriere-acces-identifiant.feature
new file mode 100644
index 0000000..17dc23f
--- /dev/null
+++ b/src/modules/auth/features/barriere-acces-identifiant.feature
@@ -0,0 +1,27 @@
+# language: fr
+@AUTH @priority-1
+Fonctionnalité: Barrière d'accès — l'identifiant se saisit une seule fois
+ En tant qu'utilisateur qui revient dans Festipod
+ Je veux retrouver l'identifiant que j'ai déjà choisi, pré-rempli
+ Afin de ne jamais avoir à le retaper à l'arrivée
+
+ # Garde-fou contre la régression rapportée : au retour (rechargement / round-trip
+ # broker) la barrière re-demandait un identifiant NU et VIDE alors qu'il était
+ # déjà stocké. L'identifiant est capturé UNE FOIS au premier accès, persisté,
+ # puis pré-rempli. Voir AuthGate + AccessGateScreen.
+
+ @ui
+ Scénario: Le champ identifiant est pré-rempli avec la valeur déjà stockée
+ Étant donné que la barrière d'accès s'affiche avec l'identifiant stocké "alice"
+ Alors le champ identifiant contient "alice"
+
+ @ui
+ Scénario: Un premier accès sans identifiant stocké affiche un champ vide
+ Étant donné que la barrière d'accès s'affiche sans identifiant stocké
+ Alors le champ identifiant est vide
+
+ @ui
+ Scénario: Entrer remonte l'identifiant saisi
+ Étant donné que la barrière d'accès s'affiche avec l'identifiant stocké "alice"
+ Quand je clique sur "Entrer" dans la barrière
+ Alors l'identifiant remonté à l'application est "alice"
diff --git a/src/modules/auth/screens/AccessGateScreen.tsx b/src/modules/auth/screens/AccessGateScreen.tsx
index ddfce2f..63099bc 100644
--- a/src/modules/auth/screens/AccessGateScreen.tsx
+++ b/src/modules/auth/screens/AccessGateScreen.tsx
@@ -28,6 +28,12 @@ import { SHARED_WALLET_PASSWORD, SHARED_WALLET_FILE_URL, WALLET_IMPORT_URL, hasS
interface AccessGateScreenProps {
status: 'disconnected' | 'connecting' | 'connected' | 'error';
error?: string;
+ /**
+ * The identifier already stored for this space (the persisted one), used to
+ * PREFILL the field so a returning user never re-types it. Empty on a truly
+ * first access. Normalized upstream; shown verbatim.
+ */
+ initialIdentifier?: string;
/** Enter the space: the raw identifier the user typed (normalized upstream). */
onEnter: (identifier: string) => void;
}
@@ -48,13 +54,15 @@ function Step({ n, title, children }: { n: number; title: string; children: Reac
);
}
-export function AccessGateScreen({ status, error, onEnter }: AccessGateScreenProps) {
+export function AccessGateScreen({ status, error, initialIdentifier, onEnter }: AccessGateScreenProps) {
const connecting = status === 'connecting';
const [copied, setCopied] = useState(false);
// The identifier that names this virtual space (a technical id — a pseudo in
// practice, but not a Festipod username). Entered HERE, at wallet access, so a
// single act both names the space and opens it. Normalized (lowercased) upstream.
- const [identifier, setIdentifier] = useState('');
+ // PREFILLED from the stored identifier so a returning user (reload / broker
+ // round-trip) sees the value they already chose and never re-types it.
+ const [identifier, setIdentifier] = useState(initialIdentifier ?? '');
const copyPassword = async () => {
try {
diff --git a/src/modules/auth/steps/ui/barriere-acces.steps.ts b/src/modules/auth/steps/ui/barriere-acces.steps.ts
new file mode 100644
index 0000000..3f37471
--- /dev/null
+++ b/src/modules/auth/steps/ui/barriere-acces.steps.ts
@@ -0,0 +1,101 @@
+/**
+ * @ui steps for the access barrier (AccessGateScreen).
+ *
+ * These render the prop-driven AccessGateScreen directly (via renderElement) —
+ * it is NOT a registry/route screen, its state comes from props (status,
+ * initialIdentifier, onEnter). We assert on the rendered DOM: the identifier
+ * field is PREFILLED from the stored value, and "Entrer" reports the identifier.
+ *
+ * Guards the reported regression: on return the barrier used to re-ask for a
+ * bare, empty identifier despite one being stored. See AuthGate.tsx.
+ */
+import { Given, When, Then } from '@cucumber/cucumber';
+import { expect } from 'chai';
+import React from 'react';
+import { renderElement } from '../../../../shared/test-harness/renderHelper';
+import { AccessGateScreen } from '../../screens/AccessGateScreen';
+import type { FestipodWorld } from '../../../../shared/support/world';
+
+// Local per-scenario state (kept off the World to avoid touching its type).
+interface GateState {
+ doc: Document | null;
+ entered: string | null;
+}
+const gateStates = new WeakMap