Skip to content

feat(web): add buy-now checkout flow#317

Merged
onerandomdevv merged 2 commits into
devfrom
feat/web-checkout-buynow
May 22, 2026
Merged

feat(web): add buy-now checkout flow#317
onerandomdevv merged 2 commits into
devfrom
feat/web-checkout-buynow

Conversation

@SAHEED2010
Copy link
Copy Markdown
Collaborator

@SAHEED2010 SAHEED2010 commented May 22, 2026

What does this PR do?

Implements W-09a — Checkout Flow: Product → Buy Now. This adds a focused standalone checkout flow from product detail, including delivery address selection, payment method selection, escrow-protected order summary, direct order creation, and Paystack inline checkout.

Task

W-09a — Checkout Flow: Product → Buy Now

What changed

  • Added /buyer/checkout/[productId] checkout route
  • Added standalone checkout layout without social 3-column chrome
  • Added focused 3-step checkout:
    • Delivery
    • Payment
    • Review
  • Added escrow trust banner and escrow protected amount pill
  • Added saved address loading from GET /users/me/addresses
  • Added manual address entry and persistence via PUT /users/me/addresses
  • Added Paystack v2 inline popup loader
  • Added typed checkout API helpers
  • Wired Product Detail Buy Now to checkout route
  • Added sourced checkout support via ?sourcedProductId=
  • Blocked checkout in store mode
  • Added minimum backend public product response change to expose product id for direct checkout

Backend endpoints verified

  • POST /orders/direct
  • POST /orders
  • POST /orders/sourced/:sourcedProductId
  • POST /payments/initialize
  • GET /payments/verify/:reference
  • GET /users/me
  • GET /users/me/addresses
  • PUT /users/me/addresses
  • GET /products/:code

Files changed

  • apps/backend/src/domains/commerce/product/product.service.ts
  • apps/web/src/app/(public)/p/[code]/page.tsx
  • apps/web/src/app/(public)/p/[code]/ProductDetailClient.tsx
  • apps/web/src/app/(shopper)/buyer/_components/ShopperShell.tsx
  • apps/web/src/app/(shopper)/buyer/checkout/[productId]/page.tsx
  • apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
  • apps/web/src/lib/checkout.ts
  • apps/web/src/lib/paystack.ts

Backend route-contract note

GET /products/:code did not previously expose the product UUID, but CreateDirectOrderDto.productId requires it.

To avoid inventing a frontend contract, this PR adds id to the existing public product response in the smallest possible way:

  • PRODUCT_PUBLIC_SELECT
  • PublicProductResponse
  • toPublicResponse

No new backend endpoint was added and no DTO contract was changed.

Sourced product behavior

  • Checkout supports ?sourcedProductId=
  • Sourced checkout calls POST /orders/sourced/:sourcedProductId
  • Sourced products are not added to cart
  • Shopper UI does not expose dropship internals:
    • no physical store identity
    • no linked order ID
    • no dropshipper cost
    • no digital margin
    • no fulfillment order details
    • no store type

Payment behavior

  • Creates the direct order first
  • Calls POST /payments/initialize
  • Opens Paystack v2 inline popup using backend returned payment data
  • Uses redirect fallback only if popup data is unavailable
  • On Paystack success, currently routes to /buyer/orders/[orderId] as a temporary handoff until W-09b lands

Deferred / follow-ups

  • Google Places autocomplete is deferred because frontend support is not currently present
  • DVA payment UI is deferred even though backend endpoints exist
  • W-09b Order Confirmed Screen is owned by DEV C and remains separate
  • Sourced product detail entry point is deferred; checkout already accepts ?sourcedProductId=

Verification

Web:

  • pnpm run lint — PASS
  • npx tsc --noEmit — PASS
  • pnpm run build — PASS

Backend:

  • npx tsc --noEmit — PASS
  • pnpm run lint — PASS

Type of change

  • New feature
  • Bug fix
  • Refactor / cleanup
  • Database migration included
  • Chore / maintenance
  • Documentation

Area affected

  • Web
  • Backend
  • WhatsApp
  • Shared package
  • Database / Prisma
  • GitHub / CI / infrastructure

How to test this

  1. Open a public product detail page.
  2. Click Buy Now.
  3. Confirm checkout opens at /buyer/checkout/[productId].
  4. Select or add a delivery address.
  5. Select Card / Bank Transfer via Paystack.
  6. Review the order summary and escrow pill.
  7. Confirm order.
  8. Confirm Paystack popup opens.
  9. Confirm store mode cannot proceed with checkout.

Expected result:

  • Buy Now skips cart.
  • Checkout creates a direct order and initializes Paystack payment.
  • Sourced checkout uses the sourced order endpoint when sourcedProductId is present.
  • Shopper UI does not expose dropship/internal fields.
  • Checkout remains mobile-friendly and standalone.

Pre-commit checklist

  • Web lint/type/build pass when web is affected
  • Backend lint/typecheck pass when backend is affected
  • No console.log left in production code
  • No secrets or .env files committed
  • No new any types added
  • No emojis added
  • Tailwind/design tokens used for UI
  • No non-MVP legacy features reintroduced
  • No Prisma migration added

Screenshots

Required for UI changes. Add checkout desktop/mobile screenshots before requesting final review.

Notes for reviewer

This PR intentionally does not implement W-09b. After payment success, it routes to /buyer/orders/[orderId] as the temporary handoff path. DEV C will replace/extend this with the Order Confirmed Screen after W-09a merges.

Summary by CodeRabbit

  • New Features
    • "Buy Now" action now routes to dedicated checkout page (previously showed placeholder message)
    • New checkout experience with delivery address selection, management, and creation
    • Order confirmation and Paystack payment processing integration
    • Product details now include product ID in public data

Review Change Stack

Implements W-09a — Buy Now checkout from product detail with escrow trust
banner, saved-address selection plus inline add-new flow, Paystack inline
payment, and a single-product order summary. Routes /buyer/checkout/[productId]
renders standalone (no nav chrome) via ShopperShell, blocks store-mode
identities, and reuses POST /orders/direct + POST /payments/initialize.
Sourced-product flow is wired behind ?sourcedProductId= for future Quick Buy
entry points. Public product response now exposes id so the checkout page
can call createDirectOrder.
@SAHEED2010 SAHEED2010 requested a review from onerandomdevv as a code owner May 22, 2026 22:01
@codesandbox
Copy link
Copy Markdown

codesandbox Bot commented May 22, 2026

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Warning

Rate limit exceeded

@SAHEED2010 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 59 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fbbd20d7-5128-4c5e-859c-5c9ea0229b30

📥 Commits

Reviewing files that changed from the base of the PR and between e378888 and 1ae84c8.

📒 Files selected for processing (7)
  • apps/backend/src/domains/commerce/product/product.service.ts
  • apps/web/src/app/(public)/p/[code]/ProductDetailClient.tsx
  • apps/web/src/app/(public)/p/[code]/page.tsx
  • apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
  • apps/web/src/lib/api.ts
  • apps/web/src/lib/checkout.ts
  • apps/web/src/lib/paystack.ts
📝 Walkthrough

Walkthrough

This PR implements a complete "Buy Now" to checkout flow: the backend exposes product IDs in the public API contract, the frontend adds navigation from product detail to a new checkout page, and the checkout page orchestrates address selection, order creation, and Paystack payment initialization and confirmation.

Changes

Buy Now Checkout Implementation

Layer / File(s) Summary
Product ID in public contract
apps/backend/src/domains/commerce/product/product.service.ts, apps/web/src/app/(public)/p/[code]/page.tsx
Backend PRODUCT_PUBLIC_SELECT and toPublicResponse() now include product id; frontend PublicProduct type adds the required id field to enable checkout routing.
Paystack inline payment loader and opener
apps/web/src/lib/paystack.ts
Typed module that loads the Paystack inline script via a cached promise, reuses an existing constructor when present, and exposes openPaystackInline to instantiate the popup and handle transaction/success/cancel/error callbacks.
Checkout API helpers and data types
apps/web/src/lib/checkout.ts
Utility module for address CRUD (fetch/save/format), user lookup, direct and sourced order creation with response normalization, Paystack payment initialization, product lookup by code, and safe kobo-based pricing arithmetic.
Standalone route layout detection in ShopperShell
apps/web/src/app/(shopper)/buyer/_components/ShopperShell.tsx
ShopperShell detects /buyer/checkout and nested subpaths as standalone routes and renders them in a full-height centered wrapper instead of the standard three-column AppShell layout.
Product detail Buy Now navigation
apps/web/src/app/(public)/p/[code]/ProductDetailClient.tsx
ProductDetailClient now imports useRouter and builds real navigation to /buyer/checkout/{product.id} with product code and variant query parameters, replacing the prior "checkout coming soon" toast.
Checkout page wrapper
apps/web/src/app/(shopper)/buyer/checkout/[productId]/page.tsx
Next.js App Router page that extracts dynamic productId and optional search params (code, variantId, sourcedProductId), sets no-index metadata, and renders CheckoutClient.
Checkout orchestration and UI
apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
Main checkout component that loads product/user/addresses, manages address draft state with validation, derives pricing and confirmability, handles order creation (direct or sourced), initializes Paystack payment, and renders the full checkout flow UI with step progress, address selection/form, payment method selection, order summary, and sticky confirmation button.

Sequence Diagram

(Included in the review stack artifact above)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • coded-devs/twizrr#305: Both PRs extend the public product detail feature—this PR adds the product id field to contracts and implements "Buy Now" navigation to the new checkout route, building on the product detail page foundation.
  • coded-devs/twizrr#293: Both PRs modify ShopperShell layout logic—this PR adds standalone route detection for /buyer/checkout to bypass the standard three-column layout, extending the shopper layout behavior.

Suggested reviewers

  • onerandomdevv

Poem

🐰 Hops with glee at checkout dreams so grand,
Buy Now buttons lead to payment's promised land,
Addresses saved, totals sum with care,
Paystack pops up—completion's in the air!
From product page to orders, all is fair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(web): add buy-now checkout flow' clearly summarizes the main change—implementing a Buy Now checkout flow on the web platform.
Description check ✅ Passed The description is comprehensive and well-structured, covering all template sections: what changed and why, type of change, areas affected, detailed testing steps, pre-commit checklist with items marked, and notes for reviewer.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-checkout-buynow

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.

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 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 `@apps/backend/src/domains/commerce/product/product.service.ts`:
- Line 120: The public product contract exposes an `id` field but must use the
`*Id` naming convention: change the exposed property from `id` to `productId`
everywhere in this file (replace usages where objects are constructed with `id`
in public DTOs) and update the selection/mapping layers so DB `id` is still
selected (keep `id: true` in Prisma selects) but is mapped into the public shape
as `productId`; specifically update the projection/mapping helpers (e.g., any
function named like mapProductToPublicDto, getPublicProductFields or similar),
all places that build the public product object, and the call sites that expect
the public field name so types/tests remain consistent. Ensure only the public
API field name changes while the DB field `id` is preserved and mapped.

In `@apps/web/src/app/`(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx:
- Around line 332-338: The fallback logic in CheckoutClient for Paystack (the
block checking paystack.accessCode and paystack.reference) ignores an
authorizationUrl returned from initializePayment; update the code to first check
for and redirect to the authorizationUrl provided by the initializePayment
response (e.g., any authorizationUrl on the paystack/init result) before falling
back to order.authorizationUrl, using window.location.href to navigate and
returning immediately if found; keep the existing final throw if neither URL
exists.
- Around line 242-286: Replace the ad-hoc trimming/required checks in
handleSaveDraft with a Zod schema + react-hook-form: define a Zod schema for
NewAddressDraft (use z.preprocess or .transform to trim strings and coerce empty
line2/postalCode to undefined, and mark label, street, city, state as required),
wire useForm({ resolver: zodResolver(schema), defaultValues: EMPTY_DRAFT }) in
the component, replace draft state usage with form register/controlled inputs
and show formState.errors instead of draftError, and change handleSaveDraft to
be the form submit handler that builds the next addresses array, calls
saveAddresses, then uses form.reset(EMPTY_DRAFT) and
setShowNewAddressForm(false)/setSelectedAddressId(...) on success and sets
form-level error via setError or setFormError on failure while still toggling
setSavingAddress accordingly.
- Around line 108-183: Replace the three manual useEffect loaders inside
CheckoutClient that call fetchCheckoutProductByCode, fetchMe, and
fetchSavedAddresses with React Query useQuery hooks keyed by the project's
centralized queryKeys; specifically, remove the useEffect blocks that manage
cancelled flags and local loading/error state (the ones that call
fetchCheckoutProductByCode, fetchMe, and fetchSavedAddresses) and instead create
useQuery entries (e.g., useQuery(queryKeys.product(productCode), () =>
fetchCheckoutProductByCode(productCode), { enabled: !!productCode }) etc.) to
drive product, me, and addresses state; map query results/errors/status to the
existing setters (setProduct, setProductLoading/err state replaced by
query.status/error, setMe, setAddresses) and on successful addresses query
setSelectedAddressId to preferred?.id and setShowNewAddressForm when list is
empty; wire these useQuery keys into the centralized queryKeys module (create or
import queryKeys with functions like product(code), me(), addresses()) so other
components use the same keys.

In `@apps/web/src/lib/checkout.ts`:
- Around line 98-101: In saveAddresses (in apps/web/src/lib/checkout.ts) the
call uses api.post to "/users/me/addresses" but the backend controller defines
`@Put`("me/addresses"); change the HTTP verb from POST to PUT by replacing the
api.post invocation with api.put so the request matches the controller route and
payload shape (ensure the call still expects the same response type of {
deliveryAddresses: RawAddressEntry[] }).

In `@apps/web/src/lib/paystack.ts`:
- Around line 47-75: The loaderPromise stays rejected after any failure so
future calls never retry; update all rejection paths in the loader creation (the
Promise returned by loaderPromise in this module) to clear the cached
loaderPromise before rejecting—i.e. in the existing script error listener, in
script.onerror, and in the branches where PaystackPop/ctor is missing (both
existing load handler and script.onload) set loaderPromise = undefined (or null)
immediately before calling reject(new Error(...)); ensure you reference the
loaderPromise variable and the PAYSTACK_INLINE_SRC script handlers / PaystackPop
attach checks so subsequent calls can re-create the script and retry.
- Around line 94-99: The code currently coerces args.amountKobo to 0 on invalid
input which causes Paystack errors; update openPaystackInline to validate
args.amountKobo before calling popup.newTransaction: parse amount =
Number(args.amountKobo) and require Number.isFinite(amount) &&
Number.isInteger(amount) && amount > 0, and if the check fails throw or return a
rejected error (do not pass 0 to popup.newTransaction); reference the amount
variable, args.amountKobo, and the popup.newTransaction invocation when making
the change so invalid inputs are surfaced upstream instead of silently converted
to zero.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 050b08dc-0961-47a3-a721-eb5b23a3410b

📥 Commits

Reviewing files that changed from the base of the PR and between 086b2ef and e378888.

📒 Files selected for processing (8)
  • apps/backend/src/domains/commerce/product/product.service.ts
  • apps/web/src/app/(public)/p/[code]/ProductDetailClient.tsx
  • apps/web/src/app/(public)/p/[code]/page.tsx
  • apps/web/src/app/(shopper)/buyer/_components/ShopperShell.tsx
  • apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
  • apps/web/src/app/(shopper)/buyer/checkout/[productId]/page.tsx
  • apps/web/src/lib/checkout.ts
  • apps/web/src/lib/paystack.ts

Comment thread apps/backend/src/domains/commerce/product/product.service.ts
Comment on lines +108 to +183
useEffect(() => {
let cancelled = false;
async function load() {
if (!productCode) {
setProductError(
"Missing product reference. Please return to the product page and try again.",
);
setProductLoading(false);
return;
}
try {
const fetched = await fetchCheckoutProductByCode(productCode);
if (cancelled) return;
if (!fetched) {
setProductError("Product could not be loaded.");
} else if (fetched.id !== productId) {
// Code/UUID mismatch — refuse to proceed rather than trust either.
setProductError(
"Product details do not match the checkout link. Please retry from the product page.",
);
} else {
setProduct(fetched);
}
} catch {
if (!cancelled) setProductError("Product could not be loaded.");
} finally {
if (!cancelled) setProductLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, [productCode, productId]);

useEffect(() => {
let cancelled = false;
async function load() {
try {
const user = await fetchMe();
if (!cancelled) setMe(user);
} catch {
if (!cancelled) setMe(null);
}
}
load();
return () => {
cancelled = true;
};
}, []);

useEffect(() => {
let cancelled = false;
async function load() {
try {
const list = await fetchSavedAddresses();
if (cancelled) return;
setAddresses(list);
const preferred = list.find((a) => a.isDefault) ?? list[0] ?? null;
if (preferred?.id) setSelectedAddressId(preferred.id);
if (list.length === 0) setShowNewAddressForm(true);
} catch {
if (!cancelled) {
setAddresses([]);
setShowNewAddressForm(true);
}
} finally {
if (!cancelled) setAddressesLoading(false);
}
}
load();
return () => {
cancelled = true;
};
}, []);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

if [ -f "$FILE" ]; then
  echo "== File exists: $FILE =="
  wc -l "$FILE"
  echo
  sed -n '1,220p' "$FILE" | nl -ba | sed -n '1,220p'
else
  echo "File not found: $FILE"
  exit 1
fi

Repository: coded-devs/twizrr

Length of output: 293


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find React Query usage in the same directory
rg -n "use(Query|InfiniteQuery|Mutation)|useQuery|QueryClient|queryKeys|queryKey" apps/web/src/app/(shopper)/buyer/checkout/[productId] || true

Repository: coded-devs/twizrr

Length of output: 123


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Locate centralized queryKeys (likely in some queryKeys file)
rg -n "queryKeys" apps/web/src | head -n 50

Repository: coded-devs/twizrr

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Identify the fetch functions used in the snippet
rg -n "fetchCheckoutProductByCode|fetchMe|fetchSavedAddresses" apps/web/src | head -n 50

Repository: coded-devs/twizrr

Length of output: 1029


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Check if there is an existing query hook for checkout product/me/addresses
rg -n "Checkout.*Product|fetchCheckoutProductByCode|get.*Checkout|SavedAddresses|fetchSavedAddresses|fetchMe" apps/web/src | head -n 80

Repository: coded-devs/twizrr

Length of output: 2970


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Check TanStack Query version / setup usage
rg -n "`@tanstack/react-query`|tanstack/react-query" apps/web/package.json apps/web/src -S || true

Repository: coded-devs/twizrr

Length of output: 257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

echo "== useQuery/useMutation/queryKeys usage in CheckoutClient ==" 
rg -n "use(Query|Mutation|InfiniteQuery)|queryKeys|queryKey" "$FILE" || true

echo
echo "== Top imports / React Query references in CheckoutClient (lines 1-80) =="
sed -n '1,120p' "$FILE" | nl -ba | head -n 120

Repository: coded-devs/twizrr

Length of output: 259


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== React Query provider =="
sed -n '1,200p' apps/web/src/components/providers/ReactQueryProvider.tsx | nl -ba | head -n 200

Repository: coded-devs/twizrr

Length of output: 149


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for centralized queryKeys definitions in apps/web/src =="
rg -n "queryKeys|queryKey" apps/web/src | head -n 80

Repository: coded-devs/twizrr

Length of output: 128


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for checkout-specific query hooks =="
rg -n "use(Checkout|SavedAddresses|Me)|checkout.*useQuery|useQuery.*checkout|savedAddresses.*useQuery|fetchSavedAddresses.*useQuery" apps/web/src | head -n 120

Repository: coded-devs/twizrr

Length of output: 2727


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Identify whether queryKeys module exists =="
fd -t f "queryKeys*" apps/web/src || true

Repository: coded-devs/twizrr

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

echo "== First 120 lines (no nl) =="
sed -n '1,120p' "$FILE" | nl -ba

Repository: coded-devs/twizrr

Length of output: 152


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# nl missing in this environment; print with line numbers using awk instead
FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

echo "== First 120 lines (awk line numbers) =="
awk '{printf "%5d\t%s\n", NR, $0}' "$FILE" | sed -n '1,120p'

Repository: coded-devs/twizrr

Length of output: 4184


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find whether CheckoutClient uses React Query at all
FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

rg -n "useQuery|useMutation|queryKeys|queryKey|QueryClient|QueryClientProvider" "$FILE" || true

Repository: coded-devs/twizrr

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show React Query provider implementation (for how queryKeys are structured)
FILE="apps/web/src/components/providers/ReactQueryProvider.tsx"

echo "== ReactQueryProvider.tsx =="
sed -n '1,200p' "$FILE" | cat

Repository: coded-devs/twizrr

Length of output: 589


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find how queryKeys are used elsewhere; search broadly but limit output
rg -n -i "querykeys|querykey" apps/web/src | head -n 80

Repository: coded-devs/twizrr

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find any useQuery usage patterns and inspect nearby lines
rg -n "useQuery\\(" apps/web/src | head -n 80

Repository: coded-devs/twizrr

Length of output: 43


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Search for TanStack Query usage patterns even if it's wrapped
rg -n "`@tanstack/react-query`|QueryClientProvider|useMutation\\(|useQuery\\(" apps/web/src | head -n 120

Repository: coded-devs/twizrr

Length of output: 337


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TanStack Query usage in apps/web/src =="
rg -n "`@tanstack/react-query`|useQuery\\(|useMutation\\(|QueryClient|queryClient|QueryClientProvider" apps/web/src | head -n 200

Repository: coded-devs/twizrr

Length of output: 556


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== queryKeys/queryKey definitions in apps/web/src =="
rg -n -i "querykeys|querykey" apps/web/src | head -n 200

Repository: coded-devs/twizrr

Length of output: 114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Files with query/key in name (case-insensitive) =="
fd -t f -i "*query*key*" apps/web/src || true

Repository: coded-devs/twizrr

Length of output: 424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== CheckoutClient has any React Query hooks? =="
rg -n "useQuery\\(|useMutation\\(|queryKeys|queryKey|QueryClient|QueryClientProvider" apps/web/src/app/'(shopper)'/buyer/checkout/'[productId]'/CheckoutClient.tsx || true

Repository: coded-devs/twizrr

Length of output: 109


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== How fetch* functions are used (checkout/me/addresses) =="
rg -n "fetchCheckoutProductByCode\\(|fetchMe\\(|fetchSavedAddresses\\(" apps/web/src/app/'(shopper)'/buyer/checkout/'[productId]'/CheckoutClient.tsx

Repository: coded-devs/twizrr

Length of output: 292


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Any useQuery usage across apps/web/src ? =="
rg -n "useQuery\\(" apps/web/src | head -n 50 || true

echo
echo "== Any useMutation usage across apps/web/src ? =="
rg -n "useMutation\\(" apps/web/src | head -n 50 || true

echo
echo "== Any tanstack/react-query imports besides provider? =="
rg -n "`@tanstack/react-query`" apps/web/src | head -n 50

Repository: coded-devs/twizrr

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Check for any queryKeys-like pattern in repository at all (not just apps/web/src)
rg -n -i "querykeys|querykey" apps/web | head -n 50 || true

Repository: coded-devs/twizrr

Length of output: 748


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find actual queryKeys.ts files =="
fd -t f -i "queryKeys.ts" apps/web || true
fd -t f -i "*queryKeys*.ts" apps/web || true
fd -t f -i "*queryKey*.ts" apps/web || true

Repository: coded-devs/twizrr

Length of output: 703


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for queryKeys usage in TS/TSX (exclude AGENTS.md) =="
rg -n -i "querykeys|querykey" apps/web --glob "*.{ts,tsx}" || true

Repository: coded-devs/twizrr

Length of output: 124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for TanStack Query hooks used anywhere (exclude provider only) =="
rg -n "use(Query|Mutation|InfiniteQuery|SuspenseQuery)\\(" apps/web --glob "*.{ts,tsx}" || true

Repository: coded-devs/twizrr

Length of output: 137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for useQueryClient / invalidateQueries / setQueryData usage =="
rg -n "`@tanstack/react-query`|useQueryClient|invalidateQueries\\(|cancelQueries\\(|setQueryData\\(" apps/web --glob "*.{ts,tsx}" || true

Repository: coded-devs/twizrr

Length of output: 266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect queryKeys.ts if it exists =="
for f in $(fd -t f -i "queryKeys.ts" apps/web 2>/dev/null || true); do
  echo "--- $f ---"
  sed -n '1,220p' "$f"
done

Repository: coded-devs/twizrr

Length of output: 101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for queryKeys.ts with glob (fd) =="
fd -t f --glob '*queryKeys*.ts' apps/web || true
fd -t f --glob '*queryKey*.ts' apps/web || true

echo
echo "== Search for 'queryKeys' string in TS/TSX under apps/web/src (exclude AGENTS.md) =="
rg -n "queryKeys" apps/web/src --glob "*.{ts,tsx}" || true

echo
echo "== Search for 'export const queryKeys' across apps/web/src =="
rg -n "export const queryKeys" apps/web/src || true

echo
echo "== Inspect AGENTS.md references around the mentioned lines =="
sed -n '600,700p' apps/web/AGENTS.md | cat

Repository: coded-devs/twizrr

Length of output: 3555


Refactor CheckoutClient data loading to React Query + centralized queryKeys.

  • In apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx (lines 108–183), product (fetchCheckoutProductByCode), user (fetchMe), and addresses (fetchSavedAddresses) are fetched via useEffect + local useState loading/error handling instead of useQuery.
  • Migrate these to useQuery entries driven by query status, using the project’s centralized queryKeys (no queryKeys implementation was found under apps/web/src; wire into the existing module or introduce it).
🤖 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 `@apps/web/src/app/`(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
around lines 108 - 183, Replace the three manual useEffect loaders inside
CheckoutClient that call fetchCheckoutProductByCode, fetchMe, and
fetchSavedAddresses with React Query useQuery hooks keyed by the project's
centralized queryKeys; specifically, remove the useEffect blocks that manage
cancelled flags and local loading/error state (the ones that call
fetchCheckoutProductByCode, fetchMe, and fetchSavedAddresses) and instead create
useQuery entries (e.g., useQuery(queryKeys.product(productCode), () =>
fetchCheckoutProductByCode(productCode), { enabled: !!productCode }) etc.) to
drive product, me, and addresses state; map query results/errors/status to the
existing setters (setProduct, setProductLoading/err state replaced by
query.status/error, setMe, setAddresses) and on successful addresses query
setSelectedAddressId to preferred?.id and setShowNewAddressForm when list is
empty; wire these useQuery keys into the centralized queryKeys module (create or
import queryKeys with functions like product(code), me(), addresses()) so other
components use the same keys.

Comment on lines +242 to +286
async function handleSaveDraft() {
const trimmed: NewAddressDraft = {
label: draft.label.trim(),
street: draft.street.trim(),
line2: draft.line2.trim(),
city: draft.city.trim(),
state: draft.state.trim(),
postalCode: draft.postalCode.trim(),
};
if (!trimmed.label || !trimmed.street || !trimmed.city || !trimmed.state) {
setDraftError("Label, street, city and state are required.");
return;
}
setDraftError(null);
setSavingAddress(true);
try {
const next: DeliveryAddress[] = [
...addresses.map((address) => ({ ...address, isDefault: false })),
{
label: trimmed.label,
street: trimmed.street,
line2: trimmed.line2 || undefined,
city: trimmed.city,
state: trimmed.state,
postalCode: trimmed.postalCode || undefined,
isDefault: true,
},
];
const saved = await saveAddresses(next);
setAddresses(saved);
const newest = saved.find((a) => a.isDefault) ?? saved[saved.length - 1];
if (newest?.id) setSelectedAddressId(newest.id);
setShowNewAddressForm(false);
setDraft(EMPTY_DRAFT);
toast("Address saved", { variant: "success" });
} catch (err) {
const message =
typeof (err as { message?: string })?.message === "string"
? (err as { message: string }).message
: "Could not save address. Please try again.";
setDraftError(message);
} finally {
setSavingAddress(false);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

echo "== Basic info =="
wc -l "$FILE"
sed -n '200,330p' "$FILE"

echo
echo "== Does file use react-hook-form / zod? =="
rg -n "react-hook-form|useForm\\(|zod|z\\.object|zodResolver" "$FILE" || true

echo
echo "== Find address draft schema usage in file (if any) =="
rg -n "draft|DeliveryAddress|saveAddresses|EMPTY_DRAFT|handleSaveDraft|setDraftError" "$FILE"

echo
echo "== Search for Zod schema definitions in the file =="
rg -n "const .*Schema|schema *=|zodResolver\\(|z\\.object" "$FILE" || true

echo
echo "== Check if there is another handle/submit using RHF in this component =="
rg -n "handleSubmit\\(|<form|FormProvider|Controller" "$FILE" || true

Repository: coded-devs/twizrr

Length of output: 6261


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx"

echo "== Search for Zod schema imports/usages (broader) =="
rg -n "from ['\"]zod['\"]|zodResolver|\\bZod\\b|\\bSchema\\b" "$FILE" || true

echo
echo "== Search for React Hook Form usage (broader) =="
rg -n "react-hook-form|useForm\\b|FormProvider|Controller|useController|watch\\b|handleSubmit\\b" "$FILE" || true

echo
echo "== Inspect address form JSX section =="
sed -n '430,620p' "$FILE"

Repository: coded-devs/twizrr

Length of output: 7739


Replace manual address validation with React Hook Form + Zod schema (apps/web/src/app/(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx, lines 242-286)
The “Add a new address” flow validates by trimming and required-checking fields inside handleSaveDraft (setting draftError), but this component doesn’t use React Hook Form or any Zod schema for validation. Move the trimming/required rules into a Zod schema and wire the inputs through React Hook Form so validation/errors come from the schema rather than ad-hoc state checks.

🤖 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 `@apps/web/src/app/`(shopper)/buyer/checkout/[productId]/CheckoutClient.tsx
around lines 242 - 286, Replace the ad-hoc trimming/required checks in
handleSaveDraft with a Zod schema + react-hook-form: define a Zod schema for
NewAddressDraft (use z.preprocess or .transform to trim strings and coerce empty
line2/postalCode to undefined, and mark label, street, city, state as required),
wire useForm({ resolver: zodResolver(schema), defaultValues: EMPTY_DRAFT }) in
the component, replace draft state usage with form register/controlled inputs
and show formState.errors instead of draftError, and change handleSaveDraft to
be the form submit handler that builds the next addresses array, calls
saveAddresses, then uses form.reset(EMPTY_DRAFT) and
setShowNewAddressForm(false)/setSelectedAddressId(...) on success and sets
form-level error via setError or setFormError on failure while still toggling
setSavingAddress accordingly.

Comment thread apps/web/src/lib/checkout.ts Outdated
Comment thread apps/web/src/lib/paystack.ts Outdated
Comment thread apps/web/src/lib/paystack.ts Outdated
- Rename public product `id` -> `productId` to follow project naming
  convention; DB column stays `id` and is mapped in toPublicResponse.
- Switch saveAddresses to PUT (matches @put("me/addresses") backend
  contract) and add api.put helper to lib/api.ts.
- In checkout confirm, prefer authorizationUrl from /payments/initialize
  over the order-creation response when falling back to a redirect.
- Reset cached Paystack loader promise on failure so transient script
  load errors can be retried instead of poisoning subsequent attempts.
- Validate amountKobo as a positive integer in openPaystackInline; surface
  invalid input as an error rather than silently coercing to 0.

CodeRabbit comments intentionally not actioned:
- Migrating CheckoutClient loaders to React Query: no centralized
  queryKeys module exists in apps/web yet; introducing one is a
  cross-cutting follow-up, not part of W-09a.
- React Hook Form + Zod for the inline address draft: @hookform/resolvers
  is not installed; the form is a five-field inline draft already
  trimmed and required-checked. Will revisit when the project adopts
  RHF+zodResolver more broadly.
@onerandomdevv onerandomdevv merged commit f609b81 into dev May 22, 2026
8 checks passed
@SAHEED2010 SAHEED2010 deleted the feat/web-checkout-buynow branch May 22, 2026 22:26
SAHEED2010 added a commit that referenced this pull request May 23, 2026
- DeliveryCodeInput: stop collapsing positional slots when the user fills
  a non-final box first. The wire-format string is now always 6 chars
  with " " for empty slots; positions are preserved. OrderDetailClient
  extracts the digit-only payload (cleanOtp via /\D/g) for validation
  and submission, so a 6-char value with placeholder spaces no longer
  passes the length check.
- OrderTimeline: terminal-state (CANCELLED / DISPUTE / REFUND_PENDING)
  freeze step is now derived from the order's event history rather than
  hardcoded to step 1. A cancel-at-PENDING_PAYMENT freezes at "Order
  Placed"; a dispute raised after dispatch freezes at "Dispatched"; etc.
- OrderDetailClient: confirm-delivery and cancel error paths no longer
  forward backend err.message to the UI. Both surface generic copy and
  the OTP helper text above the input already gives the buyer context
  for failed attempts.
- OrderDetailClient: tracking effect now has an explicit try/catch as
  defense in depth. fetchOrderTracking already swallows errors and
  returns [], but this guards against future regressions.

Intentionally NOT actioned:
- Migrating order/tracking data loading to TanStack Query (PR #322
  comments 1, 6) and migrating OTP validation to Zod + RHF (comment 3):
  the apps/web codebase has no centralized queryKeys module and no
  @hookform/resolvers dependency. Same posture as PR #317 — these are
  cross-cutting refactors that need their own task to establish the
  patterns.
- Switching `params: Promise<{ id: string }>` to a synchronous shape
  (comment 5): W-09a and W-09b already use the async-params pattern
  successfully on Next 14.2.x; reverting would diverge from established
  routes. The Promise form is also forward-compatible with Next 15
  where async params become required.
onerandomdevv pushed a commit that referenced this pull request May 23, 2026
- DeliveryCodeInput: stop collapsing positional slots when the user fills
  a non-final box first. The wire-format string is now always 6 chars
  with " " for empty slots; positions are preserved. OrderDetailClient
  extracts the digit-only payload (cleanOtp via /\D/g) for validation
  and submission, so a 6-char value with placeholder spaces no longer
  passes the length check.
- OrderTimeline: terminal-state (CANCELLED / DISPUTE / REFUND_PENDING)
  freeze step is now derived from the order's event history rather than
  hardcoded to step 1. A cancel-at-PENDING_PAYMENT freezes at "Order
  Placed"; a dispute raised after dispatch freezes at "Dispatched"; etc.
- OrderDetailClient: confirm-delivery and cancel error paths no longer
  forward backend err.message to the UI. Both surface generic copy and
  the OTP helper text above the input already gives the buyer context
  for failed attempts.
- OrderDetailClient: tracking effect now has an explicit try/catch as
  defense in depth. fetchOrderTracking already swallows errors and
  returns [], but this guards against future regressions.

Intentionally NOT actioned:
- Migrating order/tracking data loading to TanStack Query (PR #322
  comments 1, 6) and migrating OTP validation to Zod + RHF (comment 3):
  the apps/web codebase has no centralized queryKeys module and no
  @hookform/resolvers dependency. Same posture as PR #317 — these are
  cross-cutting refactors that need their own task to establish the
  patterns.
- Switching `params: Promise<{ id: string }>` to a synchronous shape
  (comment 5): W-09a and W-09b already use the async-params pattern
  successfully on Next 14.2.x; reverting would diverge from established
  routes. The Promise form is also forward-compatible with Next 15
  where async params become required.
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.

2 participants