feat(wallet): test-mode top-up RPC + Add Money UI - #16
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
CodeAnt AI is reviewing your PR. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
📝 WalkthroughWalkthroughAdds an end-to-end wallet top-up flow: DB migrations (idempotency, constraints, RLS), a Zod input schema, a Next.js server action that calls an idempotent Supabase RPC, and a client TopUpDialog integrated into WalletView. ChangesWallet Top-Up Feature
Sequence Diagram(s)sequenceDiagram
participant ComponentA
participant ComponentB
ComponentA->>ComponentB: observable interaction
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/components/features/topup-dialog.tsx (1)
64-67: 🏗️ Heavy liftAdd optimistic wallet cache update with rollback for this mutation path.
This path only invalidates after success; guideline expects optimistic UI with rollback toast on error.
As per coding guidelines,
Implement optimistic UI on mutations with rollback toast on error.🤖 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/components/features/topup-dialog.tsx` around lines 64 - 67, Before calling queryClient.invalidateQueries(["wallet"]) implement an optimistic update: in your mutation's onMutate handler (the mutation that triggers the top-up), read and store the previous wallet state via queryClient.getQueryData(["wallet"]), then immediately update the cache with queryClient.setQueryData(["wallet"], updatedValue) to reflect the optimistic top-up; in onError restore the previous state with queryClient.setQueryData(["wallet"], previous) and show a rollback toast/error notification; finally in onSettled or onSuccess call queryClient.invalidateQueries({ queryKey: ["wallet"] }) to reconcile with the server. Ensure you reference the same queryKey ["wallet"] and keep the previous value variable scoped so onError can restore it.
🤖 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/app/_actions/wallet.ts`:
- Around line 67-70: The catch block in the server action inside
src/app/_actions/wallet.ts currently skips reporting errors to Sentry (there's a
TODO) — update that catch to call Sentry.captureException(err) (or
Sentry.captureException(new Error(msg)) if err is not an Error) before returning
the failure object; ensure the module imports your Sentry client (e.g., import
Sentry from your Sentry wrapper) and preserve the existing behavior of returning
{ success: false, error: msg } after capturing the exception.
In `@src/components/features/topup-dialog.tsx`:
- Around line 26-29: Replace the handwritten amount state/validation (amountStr,
setAmountStr, inFlightRef and related manual checks around submit) with
react-hook-form driven validation using zodResolver and the shared schema:
import topupWalletSchema and use topupWalletSchema.pick({ amount: true }) as the
resolver; wire the amount input into useForm's register (remove local amountStr
state) and run handleSubmit to perform submission inside startTransition; ensure
any inline validation logic in the submit handler (and around inFlightRef) is
removed or replaced by formState.isValid/isSubmitting from react-hook-form so
the client uses the single source-of-truth Zod schema for validation.
- Around line 70-72: Replace the manual rupee interpolation in the toast success
message with the shared INR currency formatter: instead of
`₹${amount.toLocaleString("en-IN")}` and
`₹${newBalance.toLocaleString("en-IN")}`, call the shared formatter (imported
from your common/utils e.g. formatINR or INRCURRENCY_FORMATTER) like
`formatINR(amount)` and `formatINR(newBalance)` inside the `toast.success` call
in topup-dialog.tsx; make the same replacement at the other occurrence (~line
130) and add the appropriate import for the shared formatter at the top of the
file.
In `@supabase/migrations/20260517201500_topup_wallet.sql`:
- Around line 39-43: The current read-before-write idempotency check using
v_ledger_id and p_idempotency_key against the escrow_ledger table is race-prone;
instead enforce atomic idempotency by adding a unique constraint on
(reference_id, type) for escrow_ledger and change the write path to perform an
atomic insert that uses that constraint (e.g., INSERT ... ON CONFLICT DO
NOTHING/DO UPDATE and return the existing row) so concurrent transactions cannot
both apply available_balance + p_amount; update the migration to create a UNIQUE
constraint/index (not a non-unique index) and switch the procedure that
references v_ledger_id/p_idempotency_key to rely on the atomic insert/upsert and
returned id to determine whether to apply the credit.
---
Nitpick comments:
In `@src/components/features/topup-dialog.tsx`:
- Around line 64-67: Before calling queryClient.invalidateQueries(["wallet"])
implement an optimistic update: in your mutation's onMutate handler (the
mutation that triggers the top-up), read and store the previous wallet state via
queryClient.getQueryData(["wallet"]), then immediately update the cache with
queryClient.setQueryData(["wallet"], updatedValue) to reflect the optimistic
top-up; in onError restore the previous state with
queryClient.setQueryData(["wallet"], previous) and show a rollback toast/error
notification; finally in onSettled or onSuccess call
queryClient.invalidateQueries({ queryKey: ["wallet"] }) to reconcile with the
server. Ensure you reference the same queryKey ["wallet"] and keep the previous
value variable scoped so onError can restore it.
🪄 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: 2f4fa7d6-6145-4352-8750-211596427de7
📒 Files selected for processing (5)
src/app/_actions/wallet.tssrc/components/features/topup-dialog.tsxsrc/components/features/wallet-view.tsxsrc/lib/schemas/wallet.tssupabase/migrations/20260517201500_topup_wallet.sql
There was a problem hiding this comment.
Pull request overview
This PR adds the first wallet mutation path: a test-mode wallet top-up RPC, server action, and Add Money dialog integrated into the wallet page.
Changes:
- Adds
topup_walletSQL RPC and relaxesescrow_ledger.job_idfor non-job ledger entries. - Adds wallet top-up input schema and server action.
- Adds an Add Money dialog to wallet views with quick amount chips and cache invalidation.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
supabase/migrations/20260517201500_topup_wallet.sql |
Adds top-up wallet RPC, ledger write, grants, and idempotency lookup index. |
src/lib/schemas/wallet.ts |
Adds Zod validation schema/type for wallet top-up input. |
src/app/_actions/wallet.ts |
Adds server action wrapping the top-up RPC and wallet path revalidation. |
src/components/features/topup-dialog.tsx |
Adds client dialog UI for test-mode wallet top-ups. |
src/components/features/wallet-view.tsx |
Wires the Add Money dialog into the wallet view and includes formatting-only changes. |
Comments suppressed due to low confidence (2)
src/components/features/topup-dialog.tsx:93
- The close guard only checks
isPending, which updates after React renders. BecauseinFlightRef.currentis set synchronously before the transition starts, there is a window where a user can close the dialog with Escape/outside click even though the request is already in flight. Use the synchronous in-flight flag foronOpenChangeas well so the dialog truly cannot be closed mid-request.
<Dialog open={open} onOpenChange={(o) => !isPending && setOpen(o)}>
src/components/features/wallet-view.tsx:259
- This reformatting doesn't match the repository's Prettier style and makes the template literal less readable. Please run the formatter or revert this unrelated formatting-only change.
className={`text-sm font-bold ${isIncoming ? "text-emerald-700" : "text-foreground"
}`}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
CodeAnt AI finished reviewing your PR. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/components/features/wallet-view.tsx (2)
166-169: 💤 Low valueConsider responsive alignment for mobile UX.
The
justify-endclass right-aligns the TopUpDialog button on all screen sizes. On mobile devices, centering or full-width alignment might provide better UX.📱 Suggested responsive alignment
- <div className="flex justify-end"> + <div className="flex justify-center sm:justify-end"> <TopUpDialog /> </div>🤖 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/components/features/wallet-view.tsx` around lines 166 - 169, The TopUpDialog is always right-aligned by the surrounding <div className="flex justify-end"> which hurts mobile UX; change the wrapper around TopUpDialog (the div containing TopUpDialog) to use responsive alignment classes (e.g., use default mobile center and right-align on larger screens) and/or make the button width responsive (e.g., full-width on mobile and auto on larger screens) so the <div> that currently uses justify-end becomes something like mobile-centered and sm:justify-end and the TopUpDialog itself can use sm:w-auto w-full as needed.
251-255: ⚡ Quick winConsider using "released" variant for withdrawals.
The
withdrawtype is currently mapped to the"refunded"variant, which typically implies a transaction reversal. Since a withdrawal represents funds successfully leaving the wallet (similar to how escrow funds are released), the"released"variant would be more semantically appropriate and less confusing to users.🎨 Suggested variant mapping
: entry.type === "topup" ? "funded" : entry.type === "withdraw" - ? "refunded" + ? "released" : "pending"🤖 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/components/features/wallet-view.tsx` around lines 251 - 255, The withdrawal entry currently maps entry.type === "withdraw" to the "refunded" variant which is misleading; update the mapping in the wallet view rendering logic (the code block that computes the variant for entries in src/components/features/wallet-view.tsx, likely inside the WalletView component or its entry renderer where entry.type is checked) to return "released" for withdraw instead of "refunded" so withdrawals are labeled semantically as funds released.
🤖 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/components/features/wallet-view.tsx`:
- Around line 166-169: The TopUpDialog is always right-aligned by the
surrounding <div className="flex justify-end"> which hurts mobile UX; change the
wrapper around TopUpDialog (the div containing TopUpDialog) to use responsive
alignment classes (e.g., use default mobile center and right-align on larger
screens) and/or make the button width responsive (e.g., full-width on mobile and
auto on larger screens) so the <div> that currently uses justify-end becomes
something like mobile-centered and sm:justify-end and the TopUpDialog itself can
use sm:w-auto w-full as needed.
- Around line 251-255: The withdrawal entry currently maps entry.type ===
"withdraw" to the "refunded" variant which is misleading; update the mapping in
the wallet view rendering logic (the code block that computes the variant for
entries in src/components/features/wallet-view.tsx, likely inside the WalletView
component or its entry renderer where entry.type is checked) to return
"released" for withdraw instead of "refunded" so withdrawals are labeled
semantically as funds released.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 2273806c-09bf-4864-8478-f438e4a1eade
📒 Files selected for processing (5)
src/app/_actions/wallet.tssrc/components/features/topup-dialog.tsxsrc/components/features/wallet-view.tsxsrc/lib/schemas/wallet.tssupabase/migrations/20260517223900_topup_wallet_hardening.sql
✅ Files skipped from review due to trivial changes (1)
- src/lib/schemas/wallet.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/_actions/wallet.ts
- src/components/features/topup-dialog.tsx
|
@CodeAnt-AI review |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR adds a test mode wallet top up flow where the client dialog calls a server action that invokes a database function to credit the wallet, write a ledger entry, and refresh wallet views. sequenceDiagram
participant User
participant WalletUI as Wallet dialog
participant Backend as Wallet server action
participant Database as Top up function
User->>WalletUI: Open dialog and enter amount
WalletUI->>Backend: Submit top up request
Backend->>Database: Run topup wallet with amount and idempotency key
Database-->>Backend: Return new balance and ledger id
Backend-->>WalletUI: Respond with top up success
WalletUI->>WalletUI: Invalidate wallet data and show updated balance
Generated by CodeAnt AI |
|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
|
@copilot review |
Reviewed on commit |
|
@claude[agent] review |
|
@codex[agent] review |
|
@CodeAnt-AI review |
|
CodeAnt AI is running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Sequence DiagramThis PR adds a client-side wallet top up flow where the Add Money UI sends a request to a server action, which calls a database function to apply an idempotent balance credit and ledger entry, then refreshes the wallet view. sequenceDiagram
participant User
participant WalletUI
participant ServerAction
participant Database
User->>WalletUI: Open Add Money and submit amount
WalletUI->>ServerAction: Request wallet top up with amount and key
ServerAction->>Database: Call topup_wallet with amount and idempotency key
Database->>Database: Validate user, enforce bounds and idempotency, update wallet and ledger
Database-->>ServerAction: Return new available balance and ledger id
ServerAction-->>WalletUI: Return success with new balance
WalletUI->>WalletUI: Refresh wallet data and show updated balance and top up entry
Generated by CodeAnt AI |
| if (inFlightRef.current) return; | ||
| inFlightRef.current = true; | ||
|
|
||
| startTransition(async () => { |
There was a problem hiding this comment.
Suggestion: startTransition is being passed an async callback, but transition pending state is not reliably tied to the awaited RPC lifecycle. As a result, isPending can flip false before the request finishes, so the dialog/buttons may become interactive mid-request and allow close/cancel while the top-up is still in flight. Use an explicit pending state for the async mutation lifecycle (or a mutation hook) instead of relying on async startTransition. [api mismatch]
Severity Level: Major ⚠️
- ⚠️ Dialog can be closed while top-up still processing.
- ⚠️ Pending label and disabled states not aligned with RPC.
- ⚠️ Users may see success toast after dialog already closed.Steps of Reproduction ✅
1. Render `WalletView` from `src/components/features/wallet-view.tsx:116-136`, which
includes `<TopUpDialog />` at `wallet-view.tsx:166-167`.
2. In the rendered UI, click the "Add Money" button defined at `topup-dialog.tsx:83-86` to
open the dialog controlled by `open` / `setOpen` at `topup-dialog.tsx:27,81-87`.
3. Enter a valid amount (e.g., 1000) in the input at `topup-dialog.tsx:96-109` and click
the primary "Add Money" button at `topup-dialog.tsx:137-139`, which calls `handleTopUp()`
at `topup-dialog.tsx:40-79`.
4. Observe that `handleTopUp()` sets `inFlightRef.current = true` at
`topup-dialog.tsx:50-51` and wraps the async RPC call `topupWalletAction(...)` at
`topup-dialog.tsx:55-58` in `startTransition(async () => { ... })` at
`topup-dialog.tsx:53`. Because the only state updates (`setAmountStr("")` and
`setOpen(false)`) occur after the `await` inside this async callback
(`topup-dialog.tsx:72-73`), React's `useTransition` `isPending` flag at
`topup-dialog.tsx:29` is not reliably true during the RPC. In practice, the
`disabled={isPending}` props on the input, quick-amount buttons, Cancel, and primary
button at `topup-dialog.tsx:108-109,119-120,132,137` remain false while the request is in
flight, and the `onOpenChange={(o) => !isPending && setOpen(o)}` handler at
`topup-dialog.tsx:87` allows the dialog to be closed mid-request. This breaks the intended
"dialog can't be closed mid-request" behavior even though `inFlightRef` still guards
against double submission.Fix in Cursor | Fix in VSCode Claude
(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/components/features/topup-dialog.tsx
**Line:** 53:53
**Comment:**
*Api Mismatch: `startTransition` is being passed an async callback, but transition pending state is not reliably tied to the awaited RPC lifecycle. As a result, `isPending` can flip false before the request finishes, so the dialog/buttons may become interactive mid-request and allow close/cancel while the top-up is still in flight. Use an explicit pending state for the async mutation lifecycle (or a mutation hook) instead of relying on async `startTransition`.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix|
CodeAnt AI finished running the review. Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
Group A (money/runtime): - #7: GRANT EXECUTE withdraw_wallet to authenticated + REVOKE anon/public - #6: restore NULL guard on p_amount in withdraw_wallet - #1-#4: reject NULL idempotency keys in all 4 escrow functions Group B (correctness): - #5: document p_idempotency_key API-consistency in submit_milestone - #8,#9: state-machine doc: submitted->released, annotate dead 'approved' - #20: remove 'approved' from MilestoneStatus union in escrow-machine.ts - #16: fix blob URL leak in worker-form.tsx (useEffect + revokeObjectURL) - #17: fix comment attribution CodeAnt -> CodeRabbit in wallet.ts - #18: add .max(500000) to withdrawWalletSchema Group C (dialog hardening): - #10,#11: change-phone-dialog: Zod validation + inFlightRef/mountedRef - #12,#13: delete-account-dialog: Zod validation + inFlightRef/mountedRef - #14: edit-profile-dialog: inFlightRef/mountedRef guards - #15: sign-out-button: useMutation refactor Group D (doc/consistency): - #22: README: Phase 2->4, Edge Functions->pg_cron, approved->released - #21: reconcile phase status README<->AGENTS.md - #23: ADR-0004: Proposed->Accepted - #19: client account page: race-safe requestIdRef + authError capture
…ardening (#20) * feat(wallet): worker withdrawal flow + auto-release cron * fix(wallet): address PR #19 review feedback - withdraw_wallet: add is_worker() check (A), p_amount NULL guard (F), p_idempotency_key NULL guard (C), insert-first/catch unique_violation pattern mirroring topup (D), public. schema qualifier (I), w. table alias to disambiguate available_balance vs RETURNS TABLE column - worker-form: replace as any cast with typed cast to fix pre-existing lint error surfaced during PR #19 pipeline - guard_milestones_status + guard_jobs_status: replace GUC bypass with direct session_user='postgres' check (E) - auto_release_milestones: drop set_config GUC line (E), add RAISE NOTICE on insufficient-locked-balance skip (H) - wallet.ts: mapWalletRpcError helper routing by error.code + exact RAISE EXCEPTION token (G), defense-in-depth is_worker() pre-check in withdrawWalletAction (A), raw error logging before generic fallback - topup-dialog + withdraw-dialog: stable idempotencyKey via useState initializer + useEffect reset on close (B), normalize 4-space to 2-space indent on withdraw-dialog (Copilot #9) * fix(wallet): PR #19 follow-up review (uuid fail-fast + ADR-0005) - uuid.ts: remove Math.random fallback, throw if secure crypto API unavailable; collision resistance is a correctness requirement for wallet idempotency keys (CodeRabbit follow-up #2) - docs/adr/0005: ADR approving in-place modification of auto_release_milestones per Copilot #1 (drop GUC bypass) and CodeRabbit #3 (RAISE NOTICE on silent skip), as required by ADR 0004 - pr19_review_fixes.sql: add ADR reference comment above auto_release_milestones rewrite for discoverability (CodeRabbit follow-up #3) Deferred to PR #20: Sentry.captureException wiring (CodeRabbit follow-up #1) — Sentry SDK installation is the dedicated scope of PR #20 per the roadmap; adding capture calls now would not compile. * fix: address PR #19 CodeAnt security and correctness findings - onboarding/actions.ts: remove PII console.log (fullName, categories, skillTags) that leaked KYC data into server logs on every onboarding attempt (CodeAnt Critical, #discussion_r3280686499) - onboarding/actions.ts: clean up orphaned selfie from kyc bucket when profiles or worker_profiles upsert fails after upload succeeds (CodeAnt Major, #discussion_r3280686511) - client/jobs/[id]/milestones/client-milestones.tsx: scope wallet fetch by profile_id of authenticated user instead of returning arbitrary first visible wallet row; prevents wrong-balance display and funding decisions if RLS ever surfaces multiple wallets (CodeAnt Critical, #discussion_r3280693926) - worker/feed/worker-feed.tsx: gate useInfiniteQuery on auth-ready via enabled:!!user?.id and scope queryKey by user?.id; prevents spurious unauthorized errors on initial mount before auth propagates (CodeAnt Major, #discussion_r3280707356) - worker/jobs/[id]/worker-job-detail.tsx: surface errors from msRes, matRes, appRes in Promise.all instead of silently coercing to empty arrays; CodeAnt flagged matRes specifically but the same pattern affected msRes and appRes (CodeAnt Major, #discussion_r3280707371) * fix: address PR #19 round-4 review findings - _actions/wallet.ts (topup+withdraw catch): replace raw err.message return with fixed generic copy to prevent backend internals (SQL, Supabase, runtime details) from being toasted to the user; server logs retain the full error via console.error (CodeAnt Major, #discussion_r3283709865, #discussion_r3283709876) - client/jobs/[id]/milestones/client-milestones.tsx: move auth resolution out of the fetcher; ClientMilestones now passes user.id into fetchMilestonesData and the useQuery is gated by enabled on user.id with user.id added to queryKey, matching the worker-feed pattern (CodeAnt Major, #discussion_r3283709880) - client-milestones.tsx: surface msRes.error and walletRes.error (with PGRST116 no-rows exception preserving zero-balance fallback) instead of silently rendering empty milestones / zero wallet on query failure (CodeRabbit Major, outside-diff line 91) - client-milestones.tsx + worker/jobs/[id]/milestones/worker-milestones.tsx: migrate idempotency_key generation from crypto.randomUUID to generateUuid so LAN/HTTP origins without secure context do not TypeError on submission (CodeRabbit Major, outside-diff line 208) - onboarding/actions.ts: capture and console.error storage cleanup failures in profileError and workerError branches; orphan accumulation is now observable in server logs pending Sentry wiring (CodeRabbit Nitpick, lines 148-171) - worker/feed/worker-feed.tsx: remove leftover WorkerFeed DEBUG console.log that fired on every render and dumped paginated job data to the browser console; also dropped the now-unused queryStatus destructure that the log was its only consumer (CodeRabbit Nitpick, line 148) * fix(escrow): mapEscrowRpcError + wallet scope + auth gates + migration style Fix 1 - escrow.ts: add mapEscrowRpcError helper (mirrors mapWalletRpcError); replace raw error.message returns and raw outer-catch msg with user-friendly strings + structured console.error across fund/submit/approve/dispute actions. Fix 2 - worker-milestones.tsx: scope wallet query to profile_id, add enabled on user.id and user.id to queryKey in useQuery (CodeRabbit Major + CodeAnt Critical sibling-pattern follow-up to client-milestones.tsx). Fix 3 - client-job-detail.tsx: add enabled on user.id and user.id to queryKey in useQuery to prevent cold-start firing before auth resolves. Fix 4 - 20260518135600_withdraw_wallet.sql: rename unique index to idx_escrow_ledger_withdraw_owner_reference per repo idx_table_purpose naming convention (Copilot Medium). Fix 5 - 20260518181500_withdraw_wallet_fix_ambiguous_column.sql: add public. schema qualifier to function definition and GRANT; change search_path style to set search_path to 'public' for consistency with other SECURITY DEFINER functions (Copilot Med/Low x3). Fix 6 - worker-form.tsx: replace className block/hidden step mounting with conditional render so only the active Step component is mounted; hooks and subscriptions on inactive steps no longer run (CodeAnt Major perf). All form state hoisted in FormProvider so unmount is safe. Fix 7 - 20260521080000_pr19_review_fixes.sql: expand header comment to explicitly document that pakka.allow_milestone_status_change GUC was DROPPED entirely, and enumerate the three allowed guard bypass conditions (SECURITY DEFINER, postgres superuser/pg_cron, is_admin). * fix(withdraw-wallet): INSERT-first idempotency, doc drift, auth hydration UX, count aggregates Fix 1 - 20260521080000_pr19_review_fixes.sql: reorder withdraw_wallet to INSERT into escrow_ledger BEFORE acquiring the wallet FOR UPDATE lock. Race-losers now deterministically hit the function-level EXCEPTION WHEN unique_violation handler instead of racing past the pre-check and hitting insufficient_balance. Removes the now-redundant pre-check SELECT fast-path (the INSERT/unique_violation handles it). Fix 2 - docs/state-machine.md: update two Edge Function cron references to pg_cron (database scheduler) at transition 5 caller and Key Callers table, matching the scheduling mechanism introduced in 20260518225500. Fix 3 - client-milestones.tsx: add !user?.id guard before isLoading and error checks so auth hydration renders MilestonesSkeleton instead of the error UI. Fix 4 - client-job-list.tsx: replace milestones(id)/job_applications(id) full ID-array fetches with milestones_count:milestones(count) / applications_count:job_applications(count) Supabase aggregate syntax; update mapping to read [0]?.count ?? 0 instead of .length. * chore: strip embedded NULL bytes from .gitignore (was treated as binary) * fix(round-7): 7 in-scope fixes for CR/Copilot/CodeAnt round-7 review - adr/0006-nav-shell.md: strip indent from Decision:/Consequences: labels - worker/account/page.tsx: replace null blank-screen with empty-state card + retry - features/withdraw-dialog.tsx: use formatInr(100) instead of hardcoded '₹100' - _actions/escrow.ts: correct mapEscrowRpcError JSDoc (routes by error.message, not SQLSTATE) - worker-milestones.tsx: throw on msRes.error; throw on walletRes.error unless PGRST116 - worker-milestones.tsx: replace err.message leak in handleSubmit catch with fixed user-safe toast - worker/feed/worker-feed.tsx: milestones(id) full-array -> milestones_count:milestones(count) aggregate 16 other findings deferred: - PR #20 (Sentry): escrow.ts 8 capture sites - PR #22 (canonical dialog migration): apply-modal, change-phone-dialog, delete-account-dialog, edit-profile-dialog, worker-form, client-milestones escrow handlers, withdraw-dialog Zod/optimistic - PR #23 (realtime auth-hydration): client-milestones setAuth - Reply-dismiss: client/worker-milestones auth guards (middleware-guarded routes) * fix(round-8): NaN guard, seed wallet locked, Sentry TODO, auth-hydration x3, idempotency-key map x2 - post-job-form.tsx: extract parseFiniteFloat helper; apply to all 5 numeric onChange (budget, lat, lng, milestone amounts, material qty/amount) so transient '-' or '.' inputs write undefined not NaN into form state. - seed-demo-users.ts: correct wallet locked_balance to match seeded ledger: Priya 40k -> 60k (J1-M2 funded), Rohit 0 -> 20k (J2-M1 submitted), Anjali 120k -> 30k (J3-M2 disputed). Comments reference source ledger rows. - worker-milestones.tsx: add // TODO: Sentry.captureException(err) as first line of handleSubmit catch block (canonical grep target for PR #20 wiring). - client-job-detail.tsx / client-milestones.tsx / worker-milestones.tsx: destructure isAuthLoading from useUser(); gate skeleton on (isAuthLoading || !user?.id) before the data/error branch so the hydration boundary no longer renders a false 'Failed to load' error path. - client-milestones.tsx + worker-milestones.tsx: introduce idempotencyKeysRef (Map<string, string>) with getOrCreateIdempotencyKey and clearIdempotencyKey helpers. fund/approve/dispute (client) and handleSubmit (worker) now reuse the same key across retries within a single user intent and rotate only on definitive success. Closes the client side of the idempotency contract; server-side uniqueness enforcement on the escrow RPCs is tracked separately in the Phase 4.5 escrow idempotency PR. Refs round-8 review: CodeAnt CRITICAL x3 (idempotency), CodeAnt HIGH (auth hydration), CodeAnt Major x3, CR Minor. * docs(adr): heading indent + pg_cron drift (round-9) - adr/0001-auth.md, 0002-escrow.md, 0003-dispute.md, 0005-realtime-contract.md, 0007-rpc-pattern.md: strip 2-space indent on Decision/Consequences headings so they render as top-level sections matching adr/template.md. - adr/0004-auto-release.md: same indent fix + rewrite Decision and Consequences to reflect pg_cron (not Edge Function cron) as the actual scheduler; reference migration 20260518225500_schedule_auto_release_cron.sql as source of truth. Round-9 #32/#33/#34 (CR Realtime setAuth on worker-milestones, client-milestones, client-job-detail): no code change. The singleton in src/lib/supabase/client.ts already calls realtime.setAuth() on both eager-prime and onAuthStateChange (resetting to anon key on sign-out, not null). Per-file setAuth wiring is unnecessary. Round-9 #41 + parallel client-milestones/withdraw idempotency findings: no code change. The escrow RPCs (submit_milestone, fund_escrow, approve_milestone, dispute_milestone) do not accept p_idempotency_key in their SQL signatures, so action-layer forwarding has nothing to bind to. RPC-level uniqueness enforcement is tracked in the Phase 4.5 escrow idempotency PR; the per-intent client-side key from round-8 Fix 7/8 is the application-layer half of that contract. Refs round-9 review: Copilot Medium x1 (auto-release doc drift), Copilot Low x5 (ADR heading indent), CR Nitpick x3 (Realtime setAuth - no-op), CodeAnt Major x1 (idempotency forwarding - deferred). * fix(escrow,wallet): honest idempotency comments + PostgrestError type (round-10) Fix 1/2 - worker-milestones.tsx + client-milestones.tsx: rewrite the per-intent idempotency map comment to remove the overpromising "server can deduplicate" claim. New wording: "forward-compatible with server-side RPC idempotency". Grep confirmed exactly 2 occurrences across the codebase; no helper JSDoc affected. Fix 3 - wallet.ts: tighten mapWalletRpcError error parameter from an ad-hoc structural type to PostgrestError. Mirrors the escrow.ts pattern established in round-5. Import added from @supabase/supabase-js. Pure type-tightening: supabase-js v2 (^2.104.1) types PostgrestError.code as string (non-nullable), so existing error.code === "42501" / "22023" comparisons remain valid without narrowing. Both call sites (mapWalletRpcError("topup", err), mapWalletRpcError("withdraw", err)) already pass a PostgrestError. Refs round-10 review: Copilot Medium x3. Round-10 Low (migration bundling, _pr19_review_fixes.sql) reply-dismissed: the consolidation is an intentional review artifact, section headers A/C/D/F/I/E/H already document the per-concern split internally, and retroactively splitting an applied migration on an active branch is destructive. * fix(round-11): auth error handling, auth-hydration redirect (4 files), withdraw_wallet exception scope, ADR cron alignment - worker/account/page.tsx: destructure error from supabase.auth.getUser(); surface a retryable error state on transient auth/network failure instead of redirecting signed-in users to /login. Sentry TODO marker added. - worker-milestones.tsx, client-job-detail.tsx: split the combined isAuthLoading || !user?.id branch into two — skeleton during hydration, router.replace('/login') + return null once auth has resolved with no user. useRouter import added to both. Belt-and-suspenders over the middleware route guard. - client-milestones.tsx: upgrade the post-round-8 split so the !user?.id branch also router.replace('/login') + return null instead of rendering the skeleton indefinitely. Consistent with the two files above. - 20260521080000_pr19_review_fixes.sql (withdraw_wallet): scope the unique_violation handler to only the INSERT INTO public.escrow_ledger via a nested BEGIN..EXCEPTION block. Other UNIQUE violations (present or future) now surface as errors instead of being misclassified as idempotency replays. - adr/0004-auto-release.md: correct the cron expression in the Decision text to 30 20 * * * (20:30 UTC / 02:00 IST, daily) — matches the actual schedule registered in 20260518225500_schedule_auto_release_cron.sql, which is the source of truth. Refs round-11 review: CodeAnt Major x3 (auth error, auth-hydration x2), Copilot Medium x2 (cron mismatch, withdraw_wallet exception scope). Round-11 CodeAnt CRITICAL x1 + Major x1 (idempotency key forwarding on submit/fund) reply-dismissed in PR threads: see Phase 4.5 escrow idempotency PR. * fix(auth): move render-phase redirects to useEffect + worker/account race guard (round-12) Fix 1 - worker-milestones.tsx, client-milestones.tsx, client-job-detail.tsx: move router.replace("/login") out of the render path and into a useEffect([isAuthLoading, user, router]). Render guard collapses back to the combined `isAuthLoading || !user?.id` -> <Skeleton /> shape, so the user sees a skeleton both during auth hydration and during the post-resolution navigation tick. No flash of stale UI; no React render-phase side-effect warning. Dependency array uses `user` (whole object) rather than `user?.id` to avoid the exhaustive-deps lint warning about optional chaining in deps. Closes the regression from round-11 Fix 2/3. Fix 2 - worker/account/page.tsx: clear local profile and worker state (setProfile(null), setWorker(null)) before router.replace("/login") in load()'s !user branch, so stale private data cannot flash between sign-out and navigation. Fix 3 - worker/account/page.tsx: add a requestIdRef race-condition guard to load(). Increments at call start; checks before every setState after each await (post-getUser, post-Promise.all). Concurrent triggers (initial mount, Retry, onSaved, onChanged) can no longer have an older in-flight request overwrite fresher state. Pattern is dependency-free; no AbortController used because supabase-js v2 doesn't support signal forwarding on .from() queries. Refs round-12 review: CodeAnt Major x4 (3 render-phase redirects, 1 stale state, 1 race condition; the render-phase finding was applied proactively to worker-milestones.tsx as well even though CodeAnt only flagged it on the two client files). Round-12 CodeAnt CRITICAL x1 (client-milestones.tsx:238 fund idempotency forwarding) reply-dismissed and conversation resolved in PR thread: see Phase 4.5 escrow idempotency PR. * fix(nav): use explicit job-id hrefs for milestone back-links (ADR-0036) * feat(escrow): wire idempotency keys end-to-end (ADR-0006) * fix(pr20): address CodeRabbit + Copilot review feedback Group A (money/runtime): - #7: GRANT EXECUTE withdraw_wallet to authenticated + REVOKE anon/public - #6: restore NULL guard on p_amount in withdraw_wallet - #1-#4: reject NULL idempotency keys in all 4 escrow functions Group B (correctness): - #5: document p_idempotency_key API-consistency in submit_milestone - #8,#9: state-machine doc: submitted->released, annotate dead 'approved' - #20: remove 'approved' from MilestoneStatus union in escrow-machine.ts - #16: fix blob URL leak in worker-form.tsx (useEffect + revokeObjectURL) - #17: fix comment attribution CodeAnt -> CodeRabbit in wallet.ts - #18: add .max(500000) to withdrawWalletSchema Group C (dialog hardening): - #10,#11: change-phone-dialog: Zod validation + inFlightRef/mountedRef - #12,#13: delete-account-dialog: Zod validation + inFlightRef/mountedRef - #14: edit-profile-dialog: inFlightRef/mountedRef guards - #15: sign-out-button: useMutation refactor Group D (doc/consistency): - #22: README: Phase 2->4, Edge Functions->pg_cron, approved->released - #21: reconcile phase status README<->AGENTS.md - #23: ADR-0004: Proposed->Accepted - #19: client account page: race-safe requestIdRef + authError capture * fix(pr20): row_number dedupe-delete + regen db types + type clients + per-request verify * fix(pr20): H1 shared release reference_id + ON CONFLICT, M3 idempotency error mapper, M1/N1/N5 hardening * fix(pr20): hard-constraint release ledger (remove ON CONFLICT) + per-row isolated auto-release + milestone-aware fund replay (CR-1) * fix(pr20): NULL-safe authz in dispute_milestone + audit NOT IN class + remove scratch files from PR
User description
First wallet mutation flow. Unblocks Phase 3 smoke (no more direct SQL workarounds) and dogfooding.
Migration (
supabase/migrations/20260517201500_topup_wallet.sql)escrow_ledger.job_idto nullable. Theledger_typeenum already includestopup/withdrawwhich aren't job-scoped; the NOT NULL was an oversight.topup_wallet(p_amount numeric, p_idempotency_key uuid)—SECURITY DEFINER, locked toauthenticated.auth.uid()is non-null.100 ≤ amount ≤ 100000(test-mode bounds).p_idempotency_keyvia existingescrow_ledger.reference_id.wallets.available_balanceand writes atype='topup'audit row.escrow_ledger_topup_reference_idxkeeps the idempotency lookup fast.Server action (
src/app/_actions/wallet.ts)escrow.tspattern exactly:getAuthUserId()helper, Zod parse,supabase.rpc("topup_wallet", ...), try/catch wrapper,revalidatePath(..., "layout").invalid_amount/not_authenticatedto friendly strings.UI (
src/components/features/topup-dialog.tsx+ wired intowallet-view.tsx)<TopUpDialog />rendered after the balance cards.inFlightRefsynchronous gate +mountedRefwith strict-mode reset.blurOnWheelon the amount input (carrying the earlier PR fix(post-job): prevent scroll-wheel from altering number inputs #13 fix pattern).["wallet"]query prefix on success — matches both client and worker wallet views.Build
pnpm build✅ green (only pre-existing unrelateduseEffect 'load'warnings)Test mode disclaimer
Real payments (Razorpay/Stripe) come later. This unblocks smoke testing and dogfooding by giving us a clean Add Money UI instead of direct SQL updates.
Summary by CodeRabbit
CodeAnt-AI Description
Add test-mode wallet top-ups from the wallet page
What Changed
Impact
✅ Faster wallet funding✅ Fewer duplicate top-up charges✅ Clearer wallet transaction history✅ Safer test-mode payments💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.