--- sessionId: session-261005-211542-1soe --- # Requirements ### Overview & Goals To maximize application utility and deliver an optimal user experience, users should not have to manually reconnect their Freighter wallet or re-authenticate after every browser refresh. The objective is to enhance `src/composables/useSession.ts` to manage and persist wallet connection data and authentication tokens across page reloads, making session state globally available and reactive across all UI components. ### Scope - **In Scope**: - Expanding `src/composables/useSession.ts` into a reactive session composable managing wallet address, user ID, auth token, and connection lifecycle. - Hydrating wallet and authentication state from `localStorage` on initial application load and browser refresh. - Providing centralized methods in `useSession.ts` for connecting, disconnecting, checking Freighter status, and formatting wallet displays. - Refactoring `src/components/BrandBar.vue` and `src/App.vue` to leverage `useSession()` for unified session state management. - Ensuring outgoing API requests via OpenAPI client continue to resolve persisted tokens seamlessly. - **Out of Scope**: - Modifying backend authentication endpoints or token validation mechanisms. - Modifying Freighter browser extension APIs or Stellar SDK internal logic. ### User Stories - As a platform user, I want my wallet connection and authentication status to persist when I refresh the page so that I do not need to repeatedly click "Connect Wallet" and approve popups. - As a user, I want the header brand bar to immediately reflect my connected wallet address on page load so that I know my active session is valid. - As a developer, I want a single source of truth for wallet session state in `useSession.ts` so that components across the application can reliably access authentication details without duplicate storage or Freighter queries. ### Functional Requirements - **Automatic State Hydration**: On application mount or composable initialization, `useSession` checks `localStorage` for existing `authToken` and cached wallet address, verifies Freighter extension availability, and synchronizes reactive state. - **Unified Connect Flow**: `connectWallet()` requests Freighter permission (`setAllowed()`), retrieves the public key (`getAddress()`), logs in via `AuthService.loginFreighter`, stores the token in `localStorage`, and updates reactive state. - **Clean Disconnect Flow**: `disconnectWallet()` removes authentication and wallet data from `localStorage` and resets reactive variables to unauthenticated state. - **Reactive UI Synchronization**: `BrandBar.vue` dynamically displays either "Connect Wallet" or the truncated address based on `useSession` reactive state. ### Non-Functional Requirements - **Resilience & Fault Tolerance**: If storage is corrupted, inaccessible, or the JWT is invalid/expired, `useSession` safely resets state without throwing unhandled exceptions. - **Performance**: Hydration must occur synchronously/microtask-level during initial render so UI does not flicker between unauthenticated and authenticated states. # Technical Design ### Current Implementation - `src/composables/useSession.ts` is currently a minimal stub containing only `hasFreighter = async () => freighter.isConnected()`. - `src/components/BrandBar.vue` independently executes Freighter login, sets `localStorage.setItem("authToken", ...)`, and updates a local component ref `userText`. When the user refreshes, this component state is lost and resets to `'Connect Wallet'`. - `src/App.vue` calls `freighter.getAddress()` in `onBeforeMount` and stores it in an isolated local ref `address`. - `src/utils/auth.ts` provides `getAuthToken()`, `parseJwt()`, and `getUserIdFromJwt()`, retrieving tokens from `localStorage`. ### Key Decisions - **Shared Module-Level Reactive State in `useSession.ts`**: - *Decision*: Maintain `walletAddress`, `authToken`, `userId`, `isConnecting`, and `isInitialized` as module-level `ref`s within `useSession.ts` (consistent with patterns used in `useModal.ts` and `useLoading.ts`). - *Rationale*: Guarantees all consumer components share the exact same reactive session state without requiring redundant Pinia store overhead or desynchronized state copies. - **Storage Strategy**: - *Decision*: Persist `authToken` under the standard `'authToken'` key in `localStorage` and derive/cache `stellarAddress` and `userId` directly from JWT payload parsing (`parseJwt`) and Freighter sync. - *Rationale*: Keeps `localStorage` minimal, avoids stale divergent data, and maintains full compatibility with `src/main.ts` (`OpenAPI.TOKEN`) and `src/utils/auth.ts`. - **Eager Hydration & Verification**: - *Decision*: Restore state immediately from `localStorage` on initial access, followed by asynchronous verification with Freighter (`freighter.isAllowed()`). - *Rationale*: Eliminates UI layout shifts or "flash of unauthenticated content" while still ensuring the browser extension remains connected. ### Proposed Changes 1. **`src/composables/useSession.ts`**: - Add reactive state: `walletAddress: ref`, `authToken: ref`, `userId: ref`, `isConnecting: ref`. - Add computed helpers: `isAuthenticated: computed(() => !!authToken.value && !!walletAddress.value)`, `truncatedAddress: computed(() => walletAddress.value ? truncateWallet(walletAddress.value) : '')`. - Implement `initSession()` to restore token from `localStorage`, decode JWT for `stellarAddress` / `userId`, and verify with Freighter if available. - Implement `connectWallet()` and `disconnectWallet()`. - Implement `hasFreighter()` to check extension availability. 2. **`src/components/BrandBar.vue`**: - Replace local `userText` logic with `const { isAuthenticated, truncatedAddress, isConnecting, connectWallet, disconnectWallet } = useSession()`. - Compute button text: show `truncatedAddress` if authenticated, `"Connecting..."` if connecting, or `"Connect Wallet"`. 3. **`src/App.vue`**: - Initialize session via `initSession()` in `onBeforeMount` / `onMounted`. - Remove isolated, unused `address` ref. ### Data Models / Contracts ```typescript export interface SessionState { walletAddress: string | null; authToken: string | null; userId: number | undefined; isAuthenticated: boolean; isConnecting: boolean; hasFreighter: () => Promise; connectWallet: () => Promise; disconnectWallet: () => void; initSession: () => Promise; truncatedAddress: string; } ``` ### Components - `src/composables/useSession.ts`: Core session state, persistence, and wallet connection management. - `src/components/BrandBar.vue`: Primary consumer for wallet connect button and truncated address display. - `src/App.vue`: Top-level application shell invoking session initialization during application bootstrap. - `src/utils/auth.ts`: Helper utilities utilized by `useSession.ts` for token resolution and JWT decoding. ### File Structure - `src/composables/useSession.ts` (Modified: add full reactive session management and local persistence) - `src/components/BrandBar.vue` (Modified: wire to `useSession`) - `src/App.vue` (Modified: initialize session on mount and clean up duplicate state) ### Architecture Diagram ```mermaid graph LR subgraph UI Layer BB[BrandBar.vue] APP[App.vue] MOD[Proposal/Amendment Modals] end subgraph State & Session US[useSession.ts Composable] AUTH[src/utils/auth.ts] end subgraph External & Storage LS[(localStorage)] FR[Freighter Wallet Extension] API[AuthService / Backend] end APP -->|initSession| US BB -->|connect / display| US MOD -->|read wallet / auth| US US -->|read / write token| LS US -->|parse JWT| AUTH US -->|getAddress / setAllowed| FR US -->|loginFreighter| API ``` ### Risks - **Freighter Extension Locked / Permission Denied**: If a user locks their Freighter wallet or declines permission, `useSession` will catch the error, log a warning, and keep or revert the state cleanly without crashing the UI. - **Corrupted Token in Storage**: If `localStorage` contains an invalid JWT string, `parseJwt()` returns `null`, prompting `useSession` to clear the bad token and reset state. # Testing ### Validation Approach Verify that the user's wallet connection and JWT authentication remain intact and reactive across page refreshes, and that connection/disconnection lifecycles function properly without regressions in API request authorization. ### Key Scenarios 1. **Wallet Connection & Persistence Across Page Reload**: - Click "Connect Wallet" in `BrandBar.vue` and approve connection. - Verify that the button updates to the truncated wallet address and `authToken` is saved in `localStorage`. - Refresh the browser (F5 / full reload). - Verify that on page load, `BrandBar.vue` immediately displays the truncated wallet address without requiring the user to click "Connect Wallet" again. 2. **Authenticated API Calls After Reload**: - With an existing persisted session, refresh the browser and navigate to proposal details or submit a comment/amendment. - Verify that outgoing requests include the `Authorization: Bearer ` header without authentication failure. 3. **Session Disconnection / Logout**: - Trigger disconnect (or simulate logout). - Verify that `localStorage` items are cleared, reactive state resets, and `BrandBar.vue` updates to "Connect Wallet". - Refresh the page and confirm the unauthenticated state remains. ### Edge Cases - **Page Load with No Token**: Initial load with empty `localStorage` renders "Connect Wallet" cleanly with no errors. - **Freighter Extension Not Installed**: `hasFreighter()` returns `false` and clicking connect displays a descriptive message rather than throwing unhandled rejections. - **Expired / Malformed Token**: Corrupted token in `localStorage` is safely purged during hydration. # Delivery Steps ### ✓ Step 1: Implement reactive wallet session state and persistence in useSession.ts `useSession.ts` provides a centralized reactive store for wallet address, authentication token, user ID, and connection status, with automatic local persistence and hydration. - Define reactive module-level state variables (`walletAddress`, `authToken`, `userId`, `isConnecting`, `isInitialized`) to maintain shared session state across all consumer components. - Implement `initSession()` to restore saved credentials (`authToken` and `walletAddress`) from `localStorage`, validate with `parseJwt()`, and verify Freighter extension availability via `freighter.isConnected()` and `freighter.isAllowed()`. - Implement `connectWallet()` to orchestrate permission request via `freighter.setAllowed()`, fetch public address via `freighter.getAddress()`, execute `AuthService.loginFreighter`, store `authToken` and `walletAddress` in `localStorage`, and update reactive session state. - Implement `disconnectWallet()` to clear stored tokens and wallet data from `localStorage` and reset reactive state. - Expose helper computed properties and functions including `isAuthenticated`, `truncatedAddress`, `hasFreighter`, and connection status flags. ### ✓ Step 2: Integrate useSession into BrandBar.vue and App.vue `BrandBar.vue` and `App.vue` consume `useSession.ts`, automatically restoring and displaying the active wallet address upon reload without user intervention. - Update `App.vue` on-mount lifecycle to invoke `initSession()` from `useSession`, ensuring wallet and auth state are restored immediately during startup. - Refactor `BrandBar.vue` to bind wallet button text and connection trigger directly to `useSession`'s `isAuthenticated`, `truncatedAddress`, `isConnecting`, and `connectWallet` / `disconnectWallet`. - Remove redundant direct Freighter API invocations in `BrandBar.vue` and `App.vue` in favor of centralized composable methods. - Ensure smooth fallback UI handling when Freighter extension is not installed or permissions are revoked. ### ✓ Step 3: Validate wallet persistence across page refreshes and edge cases The complete wallet persistence flow is verified across reloads, active voting/proposal actions, and logout scenarios. - Test connecting a Freighter wallet and refreshing the browser: confirm that wallet address, truncated display text, and JWT bearer token are immediately retained. - Verify that authenticated API requests dispatched through generated service clients (`CommentService`, `ProposalService`, `AmendmentService`) continue to attach the bearer token after page reload. - Test edge cases such as corrupted `localStorage` tokens, expired JWTs, Freighter wallet disconnection, and missing browser extensions to ensure graceful fallback without runtime errors.