|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- ---
- 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<string | null>`, `authToken: ref<string | null>`, `userId: ref<number | undefined>`, `isConnecting: ref<boolean>`.
- - 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<boolean>;
- connectWallet: () => Promise<boolean>;
- disconnectWallet: () => void;
- initSession: () => Promise<void>;
- 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 <token>` 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.
|