Skip to content

fix(payments): show specific error messages and retry button#200

Open
kumudranjan6127-debug wants to merge 6 commits into
hrx01-dev:mainfrom
kumudranjan6127-debug:fix/issue-139-quote-server-limits
Open

fix(payments): show specific error messages and retry button#200
kumudranjan6127-debug wants to merge 6 commits into
hrx01-dev:mainfrom
kumudranjan6127-debug:fix/issue-139-quote-server-limits

Conversation

@kumudranjan6127-debug

@kumudranjan6127-debug kumudranjan6127-debug commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Closes #198

What changed

  • useClientPayments.ts: Added paymentErrorMessage() to map Firestore
    error codes to user-friendly messages instead of surfacing the raw
    err.message string.
  • PaymentManagement.tsx: Error state now renders the specific message
    from the hook, and a "Try again" button lets the client reload without
    navigating away.

Files changed

  • src/dashboard/hooks/useClientPayments.ts
  • src/dashboard/pages/PaymentManagement.tsx

Summary by CodeRabbit

  • New Features
    • Added “Load more” pagination across admin lists (projects, clients, audits, messages, project billing, project updates, and admin users) to progressively fetch more results.
  • Bug Fixes
    • Improved payment subscription error handling with clearer, code-based messages; Payment Management now displays the actual error and offers a Try again button.
  • Tests
    • Added unit tests to validate error-code-to-message mappings and guard against leaking technical Firestore text.
  • Security / Configuration
    • Tightened estimates API CORS to use a configured allowed origin (with a safe fallback) and added the related environment variable.

Closes hrx01-dev#198

Map Firestore error codes (permission-denied, unauthenticated, unavailable)
to user-friendly messages instead of surfacing raw err.message. Add a
'Try again' button to the error state so the client can reload without
navigating away.
@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown

@kumudranjan6127-debug is attempting to deploy a commit to the hrx01-dev's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Firestore billing errors now map to code-based messages in the client dashboard, the payment error view shows that message with a reload action, the estimate API uses a configured CORS origin, and admin lists gain load-more pagination.

Changes

Client payment error handling

Layer / File(s) Summary
Error mapping in the payments hook
src/dashboard/hooks/useClientPayments.ts
Adds Firestore error-code mapping in useClientPayments and stores the mapped string when the subscription error callback runs.
Payment error panel update
src/dashboard/pages/PaymentManagement.tsx
Renders the current error text in the payment error panel and adds a button that reloads the page.
Error message tests
src/dashboard/hooks/useClientPayments.test.ts
Covers the Firestore error-code mapping, fallback output, message contents, and distinct outputs for known codes.

Estimate API origin restriction

Layer / File(s) Summary
Allowed origin configuration
.env.example
Adds ALLOWED_ORIGIN guidance for configuring the estimate endpoint origin.
Estimate API CORS origin
api/estimate.ts
Replaces the wildcard CORS origin with process.env.ALLOWED_ORIGIN and a fixed fallback origin.

Admin list pagination

Layer / File(s) Summary
Collection pagination state
src/admin/hooks/useAdminData.ts
Adds page-limited Firestore queries plus hasMore and loadMore to the shared collection state.
Admin load-more controls
src/admin/pages/Audit.tsx, src/admin/pages/Clients.tsx, src/admin/pages/Messages.tsx, src/admin/pages/ProjectBilling.tsx, src/admin/pages/ProjectUpdates.tsx, src/admin/pages/Projects.tsx, src/admin/pages/Settings.tsx
Renders conditional “Load more” controls across admin pages when the shared collection state reports additional results.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • hrx01-dev/Servio#101: Modifies src/admin/pages/Settings.tsx, which this PR also updates with admins.hasMore/admins.loadMore pagination controls.
  • hrx01-dev/Servio#170: Touches src/admin/hooks/useAdminData.ts, the shared hook this PR extends with pagination state and load-more behavior.
  • hrx01-dev/Servio#187: Updates the same useClientPayments payment flow and dashboard error handling area that this PR changes again.

Suggested reviewers

  • hrx01-dev

Poem

🐰 I hopped through errors, clear and bright,
With load-more buttons lit alight.
One origin gate, one billing tune,
The burrow feels a little newer soon.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also adds unrelated admin pagination and API CORS/env changes that are outside the payments error-handling scope. Split the unrelated admin pagination and API CORS/env changes into separate PRs focused on the payments error-handling fix.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the payments error-message and retry-button changes.
Linked Issues check ✅ Passed The payments error handling changes and added test coverage address the issue's request for clearer errors and verification.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Export paymentErrorMessage so it is testable, then cover all four
branches: permission-denied, unauthenticated, unavailable, and the
generic fallback. Also assert that no raw Firebase strings are ever
surfaced to the user and that each known code maps to a distinct message.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/dashboard/hooks/useClientPayments.test.ts (1)

15-35: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the exact user-facing copy here.

These checks are too permissive for a string that PaymentManagement renders verbatim. A regression to a different generic message would still pass as long as it contains "permission", "sign in", or "unavailable". Pinning the exact strings from src/dashboard/hooks/useClientPayments.ts:7-18 will give this PR the contract coverage it claims.

Proposed tightening
   it("returns a permission hint for permission-denied", () => {
-    const msg = paymentErrorMessage("permission-denied");
-    expect(msg).toContain("permission");
-    expect(msg).not.toBe("");
+    expect(paymentErrorMessage("permission-denied")).toBe(
+      "You don't have permission to view payment data. Try signing out and back in.",
+    );
   });

   it("returns a sign-in prompt for unauthenticated", () => {
-    const msg = paymentErrorMessage("unauthenticated");
-    expect(msg.toLowerCase()).toContain("sign in");
+    expect(paymentErrorMessage("unauthenticated")).toBe(
+      "Your session has expired. Please sign in again.",
+    );
   });

   it("returns a temporary-outage message for unavailable", () => {
-    const msg = paymentErrorMessage("unavailable");
-    expect(msg.toLowerCase()).toContain("unavailable");
+    expect(paymentErrorMessage("unavailable")).toBe(
+      "Payment service is temporarily unavailable. Please try again in a moment.",
+    );
   });

   it("returns a generic fallback for any unknown code", () => {
-    const msg = paymentErrorMessage("internal");
-    expect(msg).toBeTruthy();
-    expect(msg.toLowerCase()).toContain("couldn't load");
+    expect(paymentErrorMessage("internal")).toBe(
+      "Couldn't load your payments. Please check your connection and try again.",
+    );
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/hooks/useClientPayments.test.ts` around lines 15 - 35, The
`paymentErrorMessage` tests are too loose for copy that `PaymentManagement`
renders verbatim. Update the assertions in `useClientPayments.test.ts` to match
the exact user-facing strings returned by `paymentErrorMessage` for each
supported code (`permission-denied`, `unauthenticated`, `unavailable`, and the
fallback), using the implementation in `useClientPayments.ts` as the source of
truth. Keep the checks tied to the specific `paymentErrorMessage` helper so the
test fails if the rendered copy changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/dashboard/hooks/useClientPayments.test.ts`:
- Around line 15-35: The `paymentErrorMessage` tests are too loose for copy that
`PaymentManagement` renders verbatim. Update the assertions in
`useClientPayments.test.ts` to match the exact user-facing strings returned by
`paymentErrorMessage` for each supported code (`permission-denied`,
`unauthenticated`, `unavailable`, and the fallback), using the implementation in
`useClientPayments.ts` as the source of truth. Keep the checks tied to the
specific `paymentErrorMessage` helper so the test fails if the rendered copy
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8efb210b-d78a-4fc8-b19d-98e1433637e7

📥 Commits

Reviewing files that changed from the base of the PR and between 1edab8f and 594140b.

📒 Files selected for processing (2)
  • src/dashboard/hooks/useClientPayments.test.ts
  • src/dashboard/hooks/useClientPayments.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/dashboard/hooks/useClientPayments.ts

Replace the wildcard Access-Control-Allow-Origin header with the
Firebase Hosting domain so external sites cannot call the endpoint
and drain the Gemini API quota. Falls back to the production URL
if ALLOWED_ORIGIN is not set. Document the env var in .env.example.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 23-26: The example value for ALLOWED_ORIGIN includes a trailing
slash, which will not match the browser Origin header. Update the .env.example
entry for ALLOWED_ORIGIN to use the Firebase Hosting domain without the trailing
slash so the CORS check in the Gemini estimation endpoint matches real requests.

In `@api/estimate.ts`:
- Line 125: The CORS handling in the estimate endpoint currently falls back to a
hardcoded origin when ALLOWED_ORIGIN is missing, which hides misconfiguration;
update the logic around res.setHeader in api/estimate.ts to fail closed by
omitting the Access-Control-Allow-Origin header or rejecting the request when
process.env.ALLOWED_ORIGIN is unset, and only set the header when a configured
origin is actually present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b2f8af0-ba20-47b4-a593-a3e1e4989017

📥 Commits

Reviewing files that changed from the base of the PR and between 594140b and 8a001a9.

📒 Files selected for processing (2)
  • .env.example
  • api/estimate.ts

Comment thread .env.example Outdated
Comment thread api/estimate.ts Outdated
res.setHeader('Access-Control-Allow-Credentials', 'true');
// In production, you can replace '*' with your Firebase hosting URL for better security
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN || 'https://servio-0.web.app/');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed if ALLOWED_ORIGIN is unset.

The hardcoded fallback hides misconfiguration and still advertises a production origin from any deployment that forgot to set this variable. Since this endpoint is supposed to be restricted by configuration, it should reject the request (or omit the CORS header) when ALLOWED_ORIGIN is missing instead of silently allowing a default origin.

Suggested fix
-  res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN || 'https://servio-0.web.app/');
+  if (!process.env.ALLOWED_ORIGIN) {
+    return res.status(500).json({ error: 'ALLOWED_ORIGIN is not configured on the server.' });
+  }
+  res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN || 'https://servio-0.web.app/');
if (!process.env.ALLOWED_ORIGIN) {
return res.status(500).json({ error: 'ALLOWED_ORIGIN is not configured on the server.' });
}
res.setHeader('Access-Control-Allow-Origin', process.env.ALLOWED_ORIGIN);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/estimate.ts` at line 125, The CORS handling in the estimate endpoint
currently falls back to a hardcoded origin when ALLOWED_ORIGIN is missing, which
hides misconfiguration; update the logic around res.setHeader in api/estimate.ts
to fail closed by omitting the Access-Control-Allow-Origin header or rejecting
the request when process.env.ALLOWED_ORIGIN is unset, and only set the header
when a configured origin is actually present.

…ers — closes hrx01-dev#209

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/admin/hooks/useAdminData.ts (1)

110-124: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

loadMore re-reads the entire expanded window on each call.

Bumping pageLimit tears down and recreates the onSnapshot subscription with a larger limit, so every "Load more" re-reads all previously loaded docs plus the new page (billable reads grow quadratically with pages). For a long-lived admin list this is wasteful. Consider cursor-based pagination (startAfter(lastDoc)) appending to existing data, if read volume becomes a concern.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/admin/hooks/useAdminData.ts` around lines 110 - 124, The current
`useAdminData` pagination strategy recreates the `onSnapshot` query with a
larger `firestoreLimit(pageLimit)` on every `loadMore`, causing previously
loaded documents to be re-read each time. Update the pagination flow in
`useAdminData`/`loadMore` to use cursor-based paging with `startAfter(lastDoc)`
(or an equivalent append strategy) so new pages are fetched without re-reading
the full expanded window. Keep the existing `onSnapshot`, `setState`, and
`parse` flow, but track the last document cursor and append newly fetched items
to the existing state instead of replacing the whole list.
src/admin/pages/Audit.tsx (1)

78-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the repeated “Load more” block into a shared component.

This exact pattern ({x.hasMore && <div className="flex justify-center pt-…"><Button variant="outline" size="sm" onClick={x.loadMore}>Load more</Button></div>}) is duplicated across all seven admin pages. A small <LoadMore hasMore onClick /> helper would centralize markup/labels and reduce drift.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/admin/pages/Audit.tsx` around lines 78 - 84, The repeated Load more UI
block should be extracted into a shared helper component to avoid duplication
across admin pages. Create a reusable component for the hasMore/onClick/button
markup and use it in Audit.tsx instead of inlining the div/Button pattern;
reference the existing auditLogs.hasMore and auditLogs.loadMore usage so the new
shared component preserves the same behavior and label.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/admin/hooks/useAdminData.ts`:
- Around line 110-120: The paginated query in useAdminData/useCollectionData is
only applying firestoreLimit(pageLimit) and then re-sorting client-side, which
can drop items across pages for collections ordered by createdAt or clientEmail.
Update useCollectionData to accept and apply a matching server-side orderBy in
the Firestore query for the sorted call sites, using the existing
compare/parsing flow in useAdminData to locate the pagination logic, and keep
the client-side compare sort only when it matches the same field as the server
order.

In `@src/admin/pages/Messages.tsx`:
- Around line 304-322: The Messages view is paging the unfiltered Firestore
results while rendering a client-side filtered `visible` list, so `hasMore` and
`loadMore` can advance through non-matching documents and produce arbitrary
ordering. Update the data flow in `Messages` so the status filter is applied in
the query that feeds the list, and make `hasMore` reflect that same filtered
query rather than the raw `messages` collection.

---

Nitpick comments:
In `@src/admin/hooks/useAdminData.ts`:
- Around line 110-124: The current `useAdminData` pagination strategy recreates
the `onSnapshot` query with a larger `firestoreLimit(pageLimit)` on every
`loadMore`, causing previously loaded documents to be re-read each time. Update
the pagination flow in `useAdminData`/`loadMore` to use cursor-based paging with
`startAfter(lastDoc)` (or an equivalent append strategy) so new pages are
fetched without re-reading the full expanded window. Keep the existing
`onSnapshot`, `setState`, and `parse` flow, but track the last document cursor
and append newly fetched items to the existing state instead of replacing the
whole list.

In `@src/admin/pages/Audit.tsx`:
- Around line 78-84: The repeated Load more UI block should be extracted into a
shared helper component to avoid duplication across admin pages. Create a
reusable component for the hasMore/onClick/button markup and use it in Audit.tsx
instead of inlining the div/Button pattern; reference the existing
auditLogs.hasMore and auditLogs.loadMore usage so the new shared component
preserves the same behavior and label.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a9ff8ca-8309-4c55-b59e-8a3dde33f7f2

📥 Commits

Reviewing files that changed from the base of the PR and between 8a001a9 and c1ebbdc.

📒 Files selected for processing (8)
  • src/admin/hooks/useAdminData.ts
  • src/admin/pages/Audit.tsx
  • src/admin/pages/Clients.tsx
  • src/admin/pages/Messages.tsx
  • src/admin/pages/ProjectBilling.tsx
  • src/admin/pages/ProjectUpdates.tsx
  • src/admin/pages/Projects.tsx
  • src/admin/pages/Settings.tsx

Comment on lines +110 to +120
const q = query(ref, firestoreLimit(pageLimit));
const unsubscribe = onSnapshot(
ref,
q,
(snapshot) => {
const data: T[] = [];
snapshot.forEach((docSnap) => {
const item = parse(docSnap.id, docSnap.data());
if (item) data.push(item);
});
if (compare) data.sort(compare);
setState({ data, loading: false, error: null });
setState({ data, loading: false, error: null, hasMore: snapshot.size >= pageLimit });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Firestore query limit without orderBy default ordering by document name

💡 Result:

In Firestore, if you do not specify an explicit order using the orderBy clause, the query by default retrieves documents in ascending order by their document ID (the document name) [1][2][3]. However, the behavior regarding order stability depends on your Firestore database edition: 1. Standard Edition: Firestore guarantees a stable ordering [4][5]. If you do not provide an explicit order, Firestore automatically appends an ascending order by document name (name) to your query [4][6][5]. This ensures that your query results remain consistent across executions [4][7]. 2. Enterprise Edition: Firestore does not guarantee a stable ordering for queries without a full, unique order [4][5]. Instead, it selects the most efficient ordering based on the indexes available at the time of execution, which may lead to different document orderings for identical queries [4][5]. To ensure a stable and predictable order in Enterprise edition, you should always explicitly include a unique field—such as the document name (name)—in your orderBy clause [4][5]. If you use a limit clause without an explicit orderBy, Firestore will apply the default ascending sort by document ID before limiting the result set [1][2][3]. Because the default sorting uses lexicographical order, document IDs are compared as strings [8]. This means that numeric strings will be sorted lexicographically (e.g., "10" comes before "2") rather than numerically [8]. To achieve numeric sorting, you must store the values in a numeric field type and use orderBy on that field [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'src/admin/hooks/useAdminData.ts' 'src/admin/hooks/*.ts' 'src/admin/**/*.ts' | sed -n '1,200p'
echo '--- useAdminData outline ---'
ast-grep outline src/admin/hooks/useAdminData.ts --view expanded || true
echo '--- useAdminData excerpt ---'
sed -n '1,220p' src/admin/hooks/useAdminData.ts
echo '--- search useCollectionData / compare / loadMore ---'
rg -n "useCollectionData|compare\\b|loadMore\\b|pageLimit\\b|firestoreLimit\\(" src/admin -g '*.ts' -g '*.tsx'

Repository: hrx01-dev/Servio

Length of output: 9030


Add a matching orderBy for the paginated collections

query(ref, firestoreLimit(pageLimit)) returns a prefix of Firestore’s default doc-id order, then compare re-sorts only that subset. For collections displayed by createdAt or clientEmail, loadMore can skip items that should appear in the current page.

Thread an explicit server-side orderBy through useCollectionData for the sorted call sites, and keep the client sort only if it matches that same field.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/admin/hooks/useAdminData.ts` around lines 110 - 120, The paginated query
in useAdminData/useCollectionData is only applying firestoreLimit(pageLimit) and
then re-sorting client-side, which can drop items across pages for collections
ordered by createdAt or clientEmail. Update useCollectionData to accept and
apply a matching server-side orderBy in the Firestore query for the sorted call
sites, using the existing compare/parsing flow in useAdminData to locate the
pagination logic, and keep the client-side compare sort only when it matches the
same field as the server order.

Comment thread src/admin/pages/Messages.tsx
kumudranjan6127-debug and others added 2 commits June 27, 2026 04:00
…in has no slash

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…re pages the filtered set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add better error handling

1 participant