Skip to content

feat: wallet connect/disconnect UX, admin DLQ dashboard, notification…#1028

Merged
pope-h merged 3 commits into
Shelterflex:mainfrom
ExcelDsigN-tech:feat/wallet-outbox-notification-prescreener-ux
May 30, 2026
Merged

feat: wallet connect/disconnect UX, admin DLQ dashboard, notification…#1028
pope-h merged 3 commits into
Shelterflex:mainfrom
ExcelDsigN-tech:feat/wallet-outbox-notification-prescreener-ux

Conversation

@ExcelDsigN-tech
Copy link
Copy Markdown

feat: Freighter Wallet UI, Admin Dead-Letter Dashboard, Real-Time Notifications, and Rent Pre-Screener | Closes #1001, Closes #1002, Closes #1005, Closes #1006

Overview

This PR delivers four independent but complementary product features. Freighter wallet
integration adds a complete connect and disconnect UX to the navbar, backed by a React context
and a typed utility wrapper, enabling self-custody authentication and transaction signing for
downstream components. An admin dead-letter queue dashboard exposes failed outbox events via
paginated REST endpoints and a filterable frontend table, giving super admins the ability to
inspect, retry, and dismiss records that previously accumulated silently. Real-time in-app
notifications are wired through a JWT-authenticated WebSocket server attached to the existing
Express instance, with a bell component that updates unread counts instantly and falls back to
polling when the socket is unavailable. A client-only rent affordability pre-screener wizard
lets tenants self-assess eligibility in three steps before submitting a formal application,
reducing wasted applications without requiring account creation or any backend calls.

Feature Summary

  • ConnectWalletButton component triggers the Freighter browser extension prompt and displays
    a truncated public key with a disconnect option when connected
  • WalletStatusBadge component in the navbar reflects disconnected, connecting, and connected
    states with the truncated address
  • lib/freighter.ts utility wrapper exposing connectWallet, disconnectWallet,
    isFreighterInstalled, and signTransaction over the @stellar/freighter-api package
  • WalletContext providing publicKey, connected, connect, disconnect, and signTransaction to
    the component tree, with connection state persisted in localStorage across navigations
  • Freighter-not-installed state handled with an Install Freighter banner and direct link;
    no console errors when the extension is absent
  • GET, POST, and DELETE admin endpoints for dead-letter queue management under
    /api/admin/outbox/dead-letter, all gated behind super_admin role enforcement
  • Bulk retry endpoint accepting an event type filter to re-queue all matching records
    in a single request
  • DeadLetterTable frontend component with paginated rows, collapsible JSON payload preview
    capped at 200 characters, and per-row retry and dismiss actions
  • DeadLetterFilters component for filtering by event type and date range
  • Dismiss action confirmed via a modal with audit log entry written on confirmation
  • NotificationWebSocketServer attached to the Express HTTP server, authenticating
    connections via JWT query parameter and maintaining a per-user connection map
  • notificationService updated to push new notifications to the user's active WebSocket
    connection immediately after DB write, with DB persistence independent of delivery success
  • useNotifications custom hook managing WebSocket lifecycle, reconnect with exponential
    backoff up to five retries, and local notification state
  • NotificationBell navbar component with unread count badge, 10-item dropdown, and
    mark-all-read action; falls back to 30-second polling when the WebSocket is unavailable
  • Three-step rent affordability pre-screener wizard accessible from the homepage and
    listing cards via a listingId query parameter
  • Pure affordability calculation logic in lib/affordabilityCalc.ts with no backend calls
  • EligibilityResultCard displaying a Strong, Moderate, or Low band with tailored CTAs
    and back navigation on all wizard steps

Technical Implementation

Freighter Wallet Integration (#1001):

  • lib/freighter.ts wraps @stellar/freighter-api; isFreighterInstalled checks for the
    extension object on window before any other call to prevent reference errors in
    environments where the extension is absent
  • connectWallet calls requestAccess and returns the public key on success; the key is
    written to localStorage under a fixed key so WalletContext can rehydrate on mount
  • disconnectWallet clears the localStorage entry and resets context state; no API call
    is required as Freighter manages extension-side session state independently
  • signTransaction accepts a Soroban XDR string, delegates to the Freighter signTransaction
    API, and returns the signed XDR for submission by the calling component
  • WalletContext initializes by reading localStorage on mount; if a stored key is found
    it sets connected to true without re-prompting the user
  • Public key display truncation: first four characters, ellipsis, last four characters
  • ConnectWalletButton checks isFreighterInstalled on render; if false it renders an
    Install Freighter anchor tag pointing to the official extension install URL rather than
    triggering the connect flow

Admin Dead-Letter Dashboard (#1002):

  • backend/src/routes/adminOutbox.ts added with four route handlers; a super_admin role
    middleware guard is applied to the router before any handler is reached, returning 403
    for callers without the required role
  • GET /api/admin/outbox/dead-letter queries the outbox table filtered to
    status = 'dead_lettered', ordered by last_attempted_at descending, with limit and
    offset derived from page and pageSize query params defaulting to page 1 and 20 per page
  • POST retry handler sets the record's status back to pending and resets retry_count to 0;
    the record is then picked up by the existing outbox processor on its next poll cycle
  • Bulk retry queries all dead-letter records matching the provided event_type and applies
    the same reset in a single transaction
  • DELETE handler sets status to dismissed and writes an audit log entry with the admin's
    user ID, the record ID, and the UTC timestamp before removing the record from the
    dead-letter view
  • DeadLetterTable renders rows with optimistic UI: retry sets the row status to pending
    and removes it from the list on success; dismiss fades the row out after modal
    confirmation; both revert on API error
  • Payload preview truncates the JSON string to 200 characters with a toggle to expand
    the full payload in a pre-formatted block

Real-Time Notification Bell (#1005):

  • NotificationWebSocketServer is instantiated in the Express server entry point and
    attached to the existing HTTP server instance via the server option of the ws Server
    constructor, avoiding a second port
  • On connection, the server extracts the token query parameter from the WebSocket handshake
    URL, verifies the JWT using the shared auth secret, and closes the connection with code
    4001 if verification fails; authenticated connections are stored in a
    Map<userId, WebSocket>
  • notificationService.ts updated so that after the DB insert it looks up the user's entry
    in the connection map; if a socket is found and its readyState is OPEN it sends a JSON
    message matching the agreed format; the DB write is not conditional on socket availability
  • useNotifications hook constructs the WebSocket URL with the auth token appended as a
    query param; on open it sets connected state; on message it parses the payload and
    prepends the notification to the local list, incrementing the unread count; on close it
    schedules a reconnect using Math.pow(2, attempt) * 1000ms delay up to five attempts
    before switching to the 30-second polling fallback
  • NotificationBell reads unread count from the hook state and renders a numeric badge;
    the dropdown lists the ten most recent items; mark-all-read calls the existing read
    endpoint and resets the local unread counter to zero

Rent Affordability Pre-Screener (#1006):

  • lib/affordabilityCalc.ts exports three pure functions: checkIncomeAffordability
    (monthlyRent / monthlyNetIncome <= 0.4), mapEmploymentLikelihood returning a band string
    per employment status enum, and checkDepositReadiness comparing the user's deposit
    percentage against the listing minimum or the generic 20 to 40 percent range
  • A computeOverallBand function combines the three step results: all pass maps to Strong,
    one fail maps to Moderate, two or more fails maps to Low
  • app/pre-screen/page.tsx manages a three-element step array and a currentStep index in
    local state; back navigation decrements the index; the result screen is rendered when
    currentStep equals three
  • The listingId query parameter is read on mount using useSearchParams and passed to the
    deposit step to fetch the listing's minimum deposit requirement from the listings store
    if the listing is already cached client-side; no network request is made
  • EligibilityResultCard receives the overall band and renders the appropriate headline,
    explanation, and CTA button; the Strong CTA links to the application form with the
    listing ID pre-populated if available

Test Coverage

  • freighter.ts unit tests mock the @stellar/freighter-api module and cover: successful
    connect returning public key, connect with extension absent returning a typed error,
    disconnect clearing localStorage, isFreighterInstalled returning false when window object
    lacks the extension, and signTransaction passing through the signed XDR
  • WalletContext tests verify rehydration from localStorage on mount and state reset on
    disconnect
  • adminOutbox route tests use a mocked DB layer and cover: paginated list returns correct
    shape, retry resets status and retry count, bulk retry updates all matching records,
    delete writes audit log entry, and all four endpoints return 403 for a caller without
    super_admin role
  • DeadLetterTable component tests cover optimistic retry row removal, dismiss modal
    confirmation flow, and error state revert
  • NotificationWebSocketServer tests cover JWT rejection closing the socket with code 4001,
    authenticated connection stored in the map, message delivery to an open socket, and
    no delivery attempt when no socket is present for the user
  • useNotifications hook tests cover initial connection, message appended to state,
    unread count increment, exponential backoff reconnect scheduling across five attempts,
    and polling fallback activation after max retries
  • affordabilityCalc.ts unit tests cover income pass and fail at the 0.4 boundary,
    each employment status mapping to the correct band, deposit pass and fail, and
    computeOverallBand for all three result combinations
  • Pre-screener wizard tests cover step progression, back navigation, listingId query param
    seeding the deposit step, and correct band rendered on the result screen for Strong,
    Moderate, and Low input combinations
  • All suites run in CI on each push; merge is blocked on any failure

Checklists

  • ConnectWalletButton triggers Freighter prompt and displays truncated public key
  • WalletStatusBadge reflects disconnected, connecting, and connected states in navbar
  • lib/freighter.ts exports connectWallet, disconnectWallet, isFreighterInstalled,
    and signTransaction
  • WalletContext persists connection state in localStorage across page navigations
  • Freighter not installed state renders Install Freighter banner with no console errors
  • signTransaction usable by other components via WalletContext
  • GET dead-letter endpoint returns paginated list with correct fields
  • POST retry resets record status to pending and retry count to zero
  • POST bulk-retry accepts event type filter and re-queues all matching records
  • DELETE dismiss writes audit log entry before removing record from dead-letter view
  • All admin outbox endpoints return 403 for non-super_admin callers
  • DeadLetterTable renders collapsible JSON payload preview capped at 200 characters
  • Retry and dismiss actions provide optimistic UI feedback with error revert
  • Dismiss confirmed via modal before audit log write
  • NotificationWebSocketServer authenticates via JWT query param on handshake
  • Unauthenticated WebSocket connections rejected with code 4001
  • notificationService pushes to active socket after DB write; DB write is unconditional
  • useNotifications reconnects with exponential backoff up to five retries
  • NotificationBell falls back to 30-second polling after max reconnect attempts
  • Unread count badge updates immediately on new notification arrival
  • Pre-screener wizard completes without account creation or backend calls
  • listingId query parameter seeds deposit step minimum when accessed from listing card
  • All three wizard steps support back navigation
  • affordabilityCalc.ts is pure and has no side effects
  • EligibilityResultCard renders correct band and CTA for all three outcome states
  • Pre-screener is mobile-responsive
  • All test suites pass in CI with no regressions

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

Someone is attempting to deploy a commit to the pope-h's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 30, 2026

@ExcelDsigN-tech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@pope-h pope-h merged commit 4424cfe into Shelterflex:main May 30, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants