Keyrxng

Payment Portal UX: clearer states, mobile-aware flows

Ubiquity · 2024 · Fixed confusing status states, added mobile-aware guidance, and modernized RPC plumbing without breaking existing flows.

So what?
enabled UX mobile access
Role
Product Engineer
Year
2024
Stack
TypeScript, ethers.js, @ubiquity-dao/rpc-handler, Cypress, GitHub Actions
Read narrative
enabled UX mobile access

Context

The payment portal (pay.ubq.fi) was the public-facing surface shown to investors during demos and the operational path the core team used to actually collect salaries and bounty rewards. Any regression meant blocked compensation and a poor first impression for external stakeholders. I inherited a vanilla TypeScript codebase (no React) after primarily React experience, and adopted a philosophy of minimal, surgical diffs: improve what was broken or confusing without broad rewrites that slow review.

Problem

  1. Mobile users on non-Web3 browsers hit an infinite loading state because prior logic assumed a globally injected ethereum provider. Nothing rendered; no guidance appeared.
  2. Status feedback was semantically wrong: completed claims still displayed a green “pending” style; errors sometimes appeared transiently then vanished, eroding trust.
  3. Reward claim affordances (buttons, pagination) rendered prematurely then disappeared, creating flicker and encouraging premature clicks.
  4. RPC selection logic caused UI stalls and flickers due to slow or unhealthy endpoints; legacy chainlist code was brittle.
  5. CI pipeline (Cypress + Anvil) suffered race conditions (provider not yet ready, accounts unfunded) leading to intermittent “nonce too low” failures.

Goals

Non-Goals

System diagram

flowchart LR
  Request[User Request] --> Mobile[Mobile Detection]
  Mobile --> Web3[Web3 Availability Check]
  Web3 --> RPC[RPC Endpoint Selection]
  RPC --> Errors[Error Handling]
  Errors --> UI[UI State Management]
  UI --> Tx[Transaction Processing]
  Tx --> Status[Status Feedback]

Outcome

Metrics / Signals (qualitative + relative)

Constraints

Additional tacit constraints:

Design choices

Implementation Details

  1. Device-aware provider handling: Wrapped connection logic in a guard that first checks window.matchMedia('(max-width: 768px)'). For mobile contexts lacking window.ethereum, a persistent (non-expiring) toast delivers concrete guidance (“Use a mobile-friendly Web3 browser (MetaMask) to claim this reward”). Desktop fallback likewise communicates absence and hides interactive claim buttons to prevent dead clicks.
  2. State-accurate messaging: Introduced explicit internal status enums and mapped each to a deterministic label & style. Removed ambiguous green “pending” success hybrid by only transitioning to success styling after confirmation.
  3. Safer button & pagination visibility: Centralized button controller logic to hide claim actions until prerequisites (permit data + provider + allowance fetch) resolve. Pagination appears only if rewards length > 1, eliminating layout shifts.
  4. Fastest RPC provider: Integrated @ubiquity-dao/rpc-handler to parallel probe candidate endpoints and choose the lowest latency healthy URL, instantiating a single JsonRpcProvider reused downstream (synergy with later performance work). Defensive checks ensure a URL exists; otherwise surface a structured error.
  5. CI hardening: Added bounded wait loop (30s max) for Anvil availability and serialized funding step before Cypress execution to avoid nonce contention. Eliminated intermittent early test failures tied to provider readiness.
  6. Minimal diff discipline: Isolated unrelated stylistic or cosmetic changes into separate PRs to accelerate review and reduce regression risk during urgent mobile hot-fix.

Edge Cases & Handling

Trade-offs

Risks & Mitigations

Lessons

Proof

Code excerpt — mobile-aware connect error handler

function connectErrorHandler(error: unknown) {
  if (error instanceof Error) {
    if (error?.message?.includes("missing provider")) {
      const mediaQuery = window.matchMedia("(max-width: 768px)");
      if (mediaQuery.matches) {
        toaster.create("warning", "Please use a mobile-friendly Web3 browser such as MetaMask to collect this reward", Infinity);
      } else if (!window.ethereum) {
        toaster.create("warning", "Please use a web3 enabled browser to collect this reward.", Infinity);
        buttonController.hideAll();
      }
    } else {
      toaster.create("error", error.message);
    }
  }
  return null;
}

Code excerpt — fastest RPC provider integration

export async function useRpcHandler(app: AppState) {
  const networkId = app.networkId;
  if (!networkId) throw new Error("Network ID not set");
  const handler = await useHandler(networkId);
  const provider = await handler.getFastestRpcProvider();
  if (!provider.connection.url) throw new Error("Provider URL not set");
  return new ethers.providers.JsonRpcProvider(provider.connection.url);
}

CI excerpt — waits + funding sequence (trimmed)

- name: Wait for Anvil
  run: |
    for i in {1..30}; do curl -s http://localhost:8545 && break; sleep 1; done || exit 1
- name: Fund test accounts
  run: yarn test:fund

References

See also